dsh-tg-bridge
A Telegram bridge for DeepSeek Harness: drive the agent from a bot with text, voice and photos, without opening the desktop app.
Each forum topic is a separate project with its own session and its own working directory. Replies arrive with progress, a "Stop" button and copyable code blocks.
Features
- Text: a regular task goes to the agent, the answer comes back in the same topic.
- Voice: local faster-whisper, the transcript is shown before the answer.
- Photos: sent to a vision-capable model; text and UI are recognized.
- Project per topic:
/cd my-app, and all turns run from that directory. - Goal and checklist per topic:
/goal,/task; both survive restarts and are prepended to every turn. - Live progress: "⏳ Working..." with steps, a timer and the cache hit rate at the end.
- No lost messages: a journal with acknowledgement and replay.
- Fail-fast: when the quota runs out it switches to a free model right away.
dsh-tg check: diagnostics in one command, no changes to the system.- Languages: menus and service messages in
en,ru,zh;/langswitches the topic language, a new user gets theirs from the Telegramlanguage_code(anything outside ru/zh →en). - "Stop"/"Retry" buttons, tool confirmations: right in the message, no commands to type.
Requirements
- Node.js ≥ 20
- DeepSeek Harness
0.2.1-rc.2or newer in the0.2.xline (>=0.2.1-rc.2 <0.3.0) - DeepSeek Harness installed (
~/.dsh/profiles/headless) - A Telegram bot token from @BotFather
The supported harness range is declared in engines.dsh; that field decides
compatibility on its own (DSH ignores peerDependencies when it is present).
The range includes an explicit prerelease branch, because a plain <0.3.0
silently excludes 0.2.2-* builds. A future 0.3.0 needs an explicit bump:
the bridge relies on the 0.2.x CLI flags and profile layout.
Ecosystem
The Chinese DSH community maintains the largest plugin catalogs and the in-app markets that read them; this bridge follows their packaging rules so it can be listed next to them:
- awesome-dsh-plugin — bilingual (English /
中文) plugin list and the
dsh-plugin-catalognpm package - dsh-market — the Chinese-built in-app market with one-click install and themes
- dsh-find-plugin — finds a plugin from chat instead of browsing a catalog
Languages
- Menus (
/in chat): three sets,ruby default, plusenandzh. - Topic language:
/lang en|ru|zh; stored instate.jsonand survives a restart. - A new user gets the language from the Telegram
language_code; anything that is notru/zh→en. - Translated so far: menus, the start message, turn status, errors,
/cd,/new,/stop,/lang, the reply to an unknown command. Long reports (/status,/sessions,/modelsand similar) are still in Russian, that is the next pass.
Installation
One command: your bot token and your numeric id. Nothing to edit by hand.
Any platform with Node.js ≥ 20
npx dsh-tg-bridge@latest init --token 123456789:AA... --user 123456789
npx dsh-tg-bridge@latest setup
init writes the token, your id and the working directory; setup registers
autostart and starts the bridge.
As a DSH plugin (the package carries a dsh.bundle; the catalog entry is
pending review):
dsh plugin --profile desktop add dsh-tg-bridge
The plugin starts one bridge when DSH boots and stops it on shutdown; if a
bridge is already running (for example from the scheduled task), it leaves it
alone. Run npx dsh-tg-bridge@latest init ... first so a config exists.
What init does:
| What | Where |
|---|---|
| bot token | ~/.dsh/.credentials.yaml → refs.DSH_TELEGRAM_BOT_TOKEN; other keys are left alone |
| your id and working directory | ~/.dsh/tg-bridge/config.json; an existing file is not overwritten |
headless profile |
checked, with a hint on how to create it if missing |
init flags: --cwd <path> (default ~/Projects), --voice (enable
voice transcription), --dry-run (show what would be written, change nothing).
Get your id from @userinfobot, the token from @BotFather.
Bot settings in Telegram
Three things without which the bot will stay silent in a group or work only halfway:
- Privacy mode → Disable (
/setprivacyin BotFather). Otherwise the bot sees only commands, not regular text in a topic. - Topics enabled in the group settings (Edit → Topics). Without them all work goes to a single chat and per-topic sessions are not created.
- Pin Messages permission for the bot. Only needed to pin the first message of a topic; without it the bridge keeps working but will not pin.
You do not need to set up the command menu by hand: the bridge installs it on start, in Russian, English and Chinese at once. The full settings table, permissions, checks and common errors are in docs/TELEGRAM-SETUP.md.
Autostart is installed by the setup: launchd on macOS, systemd --user on
Linux. On Windows setup registers the DSH-TG-Bridge task, which runs the
guarded supervisor (the same launcher on logon and every five minutes) so a
crashed bridge comes back without a second poller.
Manual install (if you want full control):
npm install -g dsh-tg-bridge
dsh-tg init --token 123456789:AA... --user 123456789
dsh-tg check # report only, changes nothing
dsh-tg setup # stop the old bridge and start the new one
If the bridge is already installed, setup restarts it on the new code and reinstalls
the command menu.
Troubleshooting
Error replies end with a code, which tells you what to fix:
| Code | Meaning |
|---|---|
INSUFFICIENT_QUOTA |
provider balance is empty: top up or stay on free models (/prices, /models) |
TG_CONFLICT |
a second process is polling the same bot: /doctor, then /restart |
MISSING_CREDENTIAL |
no provider key in the profile: add it in the DSH app |
NO_DSH_CLI |
DeepSeek Harness CLI not found: check the app installation |
NETWORK |
no network to the provider: retry, check VPN/DNS |
SANDBOX_DENY |
DSH denied access to a file or directory: check the project path and permissions |
UNKNOWN_MODEL |
the model from the config is unavailable: /models lists the working ones |
EMPTY_RESPONSE |
the model returned nothing: retry or switch model (/model) |
TURN_FAILED |
cause not recognized: /doctor and /logs errors |
Checks
Send the bot:
| Command | Expected |
|---|---|
/doctor |
report: pid, Telegram, disk, directory, session |
/help |
commands grouped by category |
/cd my-app |
topic bound to the project |
/prices |
model prices and the cost of the current one |
Commands
Work: /cd · /new · /stop · /send · /seed
Goals: /goal · /task
Sessions: /status · /sessions · /resume
Models: /models · /model · /prices
Extensions: /skills · /plugins · /plugin · /soul
Diagnostics: /doctor · /logs [errors|N|text]
Service: /app · /restart · /help
How it works
Telegram → bridge.mjs → dsh --profile headless --json → reply
│
├─ pending/ journal + ack (messages are not lost)
├─ per-topic queue (turns in topics run in parallel)
├─ progress + buttons (you can see the work is going)
└─ fail-fast on quota (free models as a fallback)
The bridge is a single process with long polling. Each turn spawns a separate CLI process that exits after the answer, so "no DSH process" between turns is normal, not a failure.
Steering during a turn
If you write in a topic while a turn is still running, the message does not queue
behind it: the bridge drops the task into ~/.dsh/tg-bridge/steer/, the dsh-steer-inbox
plugin hands it to the live agent on its next step, and the agent immediately replies with a short
line in the chat: what it is doing now and where it put the addition. If the agent stays silent
for 90 seconds, the bridge writes one neutral line itself so there is no silence.
This needs the plugin from this repository:
dsh plugin --profile headless add link:<path-to-repository>/plugin/steer-inbox
and a line in ~/.dsh/profiles/headless/cordis.patch.yml:
- insert:
- id: steer-inbox
name: 'dsh-steer-inbox'
The bridge checks the profile itself: if the line is missing, it does not write the task (it would just expire) and behaves as before, queueing the message.
Live thoughts
The agent can leave a short decision trail inside the progress message instead of sending extra chat messages:
node ~/.dsh/tg-bridge/say.mjs --thought "checking why the watchdog fired twice"
The note is appended to ~/.dsh/tg-bridge/thoughts/<chat>_<thread>.jsonl; the progress
message shows the last two as 💭 lines. Inside a turn the chat and thread come from
DSH_TG_CHAT / DSH_TG_THREAD, so the command needs no ids. --thought never sends a
Telegram message on its own.
Goals and tasks
A topic carries its own goal and checklist, so the aim survives /new, a
restart and a different session:
/goal ship the release set or replace the goal
/goal show the goal and the checklist
/task write the migration add a task
/task done 2 tick it off
/task rm 2 remove it
/goal done close the goal (all tasks done)
/goal clear delete the goal and its tasks
The bridge prepends the goal and the list to every turn, with the exact command
that updates it (goal.mjs). The agent ticks tasks as it finishes them, and the
next turn sees the new state. The bridge is the only writer of state.json: the
agent drops a job file into goal-jobs/ and the bridge applies it, so there is
no lock race and no lost update.
Security
config.json,state.json,pending/, logs are in.gitignoreand never end up in the repository.- The token is stored in the DSH credentials store, not in code or config.
- The bridge only replies to ids in
allowedUserIds.
Documentation
- docs/TELEGRAM-SETUP.md: BotFather settings, group and bot permissions
- docs/ROADMAP.md: what comes next
- docs/UX-ANALYSIS.md: interactive UX review
- docs/COST.md: what one turn costs and how to reduce it
- docs/CHANGELOG.md: change history
- docs/RELEASE-CRITERIA.md: release readiness criteria
License
MIT, see LICENSE.
No comments yet. Be the first to write one.