@sympoies/dsh-telegram 0.0.0-bootstrap.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/LICENSE +1 -2
  2. package/NOTICE +19 -0
  3. package/README.md +589 -2
  4. package/cordis.patch.yml +14 -0
  5. package/lib/access.d.ts +76 -0
  6. package/lib/access.d.ts.map +1 -0
  7. package/lib/access.js +137 -0
  8. package/lib/access.js.map +1 -0
  9. package/lib/client.js +835 -0
  10. package/lib/commands.d.ts +51 -0
  11. package/lib/commands.d.ts.map +1 -0
  12. package/lib/commands.js +83 -0
  13. package/lib/commands.js.map +1 -0
  14. package/lib/config.d.ts +125 -0
  15. package/lib/config.d.ts.map +1 -0
  16. package/lib/config.js +97 -0
  17. package/lib/config.js.map +1 -0
  18. package/lib/diagnostics.d.ts +58 -0
  19. package/lib/diagnostics.d.ts.map +1 -0
  20. package/lib/diagnostics.js +59 -0
  21. package/lib/diagnostics.js.map +1 -0
  22. package/lib/failures.d.ts +71 -0
  23. package/lib/failures.d.ts.map +1 -0
  24. package/lib/failures.js +139 -0
  25. package/lib/failures.js.map +1 -0
  26. package/lib/harness/host.d.ts +106 -0
  27. package/lib/harness/host.d.ts.map +1 -0
  28. package/lib/harness/host.js +142 -0
  29. package/lib/harness/host.js.map +1 -0
  30. package/lib/harness/message.d.ts +45 -0
  31. package/lib/harness/message.d.ts.map +1 -0
  32. package/lib/harness/message.js +28 -0
  33. package/lib/harness/message.js.map +1 -0
  34. package/lib/harness/model-selection.d.ts +82 -0
  35. package/lib/harness/model-selection.d.ts.map +1 -0
  36. package/lib/harness/model-selection.js +103 -0
  37. package/lib/harness/model-selection.js.map +1 -0
  38. package/lib/harness/questions-seam.d.ts +34 -0
  39. package/lib/harness/questions-seam.d.ts.map +1 -0
  40. package/lib/harness/questions-seam.js +45 -0
  41. package/lib/harness/questions-seam.js.map +1 -0
  42. package/lib/harness/types.d.ts +108 -0
  43. package/lib/harness/types.d.ts.map +1 -0
  44. package/lib/harness/types.js +23 -0
  45. package/lib/harness/types.js.map +1 -0
  46. package/lib/index.d.ts +65 -0
  47. package/lib/index.d.ts.map +1 -0
  48. package/lib/index.js +977 -0
  49. package/lib/index.js.map +1 -0
  50. package/lib/interact/approvals.d.ts +59 -0
  51. package/lib/interact/approvals.d.ts.map +1 -0
  52. package/lib/interact/approvals.js +132 -0
  53. package/lib/interact/approvals.js.map +1 -0
  54. package/lib/interact/pending.d.ts +56 -0
  55. package/lib/interact/pending.d.ts.map +1 -0
  56. package/lib/interact/pending.js +89 -0
  57. package/lib/interact/pending.js.map +1 -0
  58. package/lib/interact/questions.d.ts +100 -0
  59. package/lib/interact/questions.d.ts.map +1 -0
  60. package/lib/interact/questions.js +255 -0
  61. package/lib/interact/questions.js.map +1 -0
  62. package/lib/interact/surface.d.ts +33 -0
  63. package/lib/interact/surface.d.ts.map +1 -0
  64. package/lib/interact/surface.js +39 -0
  65. package/lib/interact/surface.js.map +1 -0
  66. package/lib/interact/text-capture.d.ts +38 -0
  67. package/lib/interact/text-capture.d.ts.map +1 -0
  68. package/lib/interact/text-capture.js +75 -0
  69. package/lib/interact/text-capture.js.map +1 -0
  70. package/lib/media/collect.d.ts +157 -0
  71. package/lib/media/collect.d.ts.map +1 -0
  72. package/lib/media/collect.js +235 -0
  73. package/lib/media/collect.js.map +1 -0
  74. package/lib/media/extractor.d.ts +117 -0
  75. package/lib/media/extractor.d.ts.map +1 -0
  76. package/lib/media/extractor.js +207 -0
  77. package/lib/media/extractor.js.map +1 -0
  78. package/lib/media/intake.d.ts +57 -0
  79. package/lib/media/intake.d.ts.map +1 -0
  80. package/lib/media/intake.js +115 -0
  81. package/lib/media/intake.js.map +1 -0
  82. package/lib/media/limits.d.ts +69 -0
  83. package/lib/media/limits.d.ts.map +1 -0
  84. package/lib/media/limits.js +97 -0
  85. package/lib/media/limits.js.map +1 -0
  86. package/lib/media/ocr.d.ts +66 -0
  87. package/lib/media/ocr.d.ts.map +1 -0
  88. package/lib/media/ocr.js +131 -0
  89. package/lib/media/ocr.js.map +1 -0
  90. package/lib/media/screenshot.d.ts +57 -0
  91. package/lib/media/screenshot.d.ts.map +1 -0
  92. package/lib/media/screenshot.js +101 -0
  93. package/lib/media/screenshot.js.map +1 -0
  94. package/lib/media/vision.d.ts +60 -0
  95. package/lib/media/vision.d.ts.map +1 -0
  96. package/lib/media/vision.js +74 -0
  97. package/lib/media/vision.js.map +1 -0
  98. package/lib/render/clamp.d.ts +34 -0
  99. package/lib/render/clamp.d.ts.map +1 -0
  100. package/lib/render/clamp.js +65 -0
  101. package/lib/render/clamp.js.map +1 -0
  102. package/lib/render/escape.d.ts +15 -0
  103. package/lib/render/escape.d.ts.map +1 -0
  104. package/lib/render/escape.js +17 -0
  105. package/lib/render/escape.js.map +1 -0
  106. package/lib/reply/activity.d.ts +31 -0
  107. package/lib/reply/activity.d.ts.map +1 -0
  108. package/lib/reply/activity.js +88 -0
  109. package/lib/reply/activity.js.map +1 -0
  110. package/lib/reply/rich-stream.d.ts +173 -0
  111. package/lib/reply/rich-stream.d.ts.map +1 -0
  112. package/lib/reply/rich-stream.js +286 -0
  113. package/lib/reply/rich-stream.js.map +1 -0
  114. package/lib/reply/split-markdown.d.ts +21 -0
  115. package/lib/reply/split-markdown.d.ts.map +1 -0
  116. package/lib/reply/split-markdown.js +87 -0
  117. package/lib/reply/split-markdown.js.map +1 -0
  118. package/lib/reply/turn-bridge.d.ts +143 -0
  119. package/lib/reply/turn-bridge.d.ts.map +1 -0
  120. package/lib/reply/turn-bridge.js +182 -0
  121. package/lib/reply/turn-bridge.js.map +1 -0
  122. package/lib/router.d.ts +415 -0
  123. package/lib/router.d.ts.map +1 -0
  124. package/lib/router.js +663 -0
  125. package/lib/router.js.map +1 -0
  126. package/lib/secrets.d.ts +30 -0
  127. package/lib/secrets.d.ts.map +1 -0
  128. package/lib/secrets.js +51 -0
  129. package/lib/secrets.js.map +1 -0
  130. package/lib/session/bindings.d.ts +81 -0
  131. package/lib/session/bindings.d.ts.map +1 -0
  132. package/lib/session/bindings.js +170 -0
  133. package/lib/session/bindings.js.map +1 -0
  134. package/lib/session/history.d.ts +50 -0
  135. package/lib/session/history.d.ts.map +1 -0
  136. package/lib/session/history.js +127 -0
  137. package/lib/session/history.js.map +1 -0
  138. package/lib/session/models.d.ts +99 -0
  139. package/lib/session/models.d.ts.map +1 -0
  140. package/lib/session/models.js +123 -0
  141. package/lib/session/models.js.map +1 -0
  142. package/lib/session/permission.d.ts +83 -0
  143. package/lib/session/permission.d.ts.map +1 -0
  144. package/lib/session/permission.js +107 -0
  145. package/lib/session/permission.js.map +1 -0
  146. package/lib/session/picker.d.ts +49 -0
  147. package/lib/session/picker.d.ts.map +1 -0
  148. package/lib/session/picker.js +107 -0
  149. package/lib/session/picker.js.map +1 -0
  150. package/lib/session/preferences.d.ts +55 -0
  151. package/lib/session/preferences.d.ts.map +1 -0
  152. package/lib/session/preferences.js +102 -0
  153. package/lib/session/preferences.js.map +1 -0
  154. package/lib/session/recovery.d.ts +66 -0
  155. package/lib/session/recovery.d.ts.map +1 -0
  156. package/lib/session/recovery.js +112 -0
  157. package/lib/session/recovery.js.map +1 -0
  158. package/lib/session/runner.d.ts +223 -0
  159. package/lib/session/runner.d.ts.map +1 -0
  160. package/lib/session/runner.js +260 -0
  161. package/lib/session/runner.js.map +1 -0
  162. package/lib/session/workspaces.d.ts +28 -0
  163. package/lib/session/workspaces.d.ts.map +1 -0
  164. package/lib/session/workspaces.js +53 -0
  165. package/lib/session/workspaces.js.map +1 -0
  166. package/lib/telegram/addressing.d.ts +39 -0
  167. package/lib/telegram/addressing.d.ts.map +1 -0
  168. package/lib/telegram/addressing.js +81 -0
  169. package/lib/telegram/addressing.js.map +1 -0
  170. package/lib/telegram/albums.d.ts +46 -0
  171. package/lib/telegram/albums.d.ts.map +1 -0
  172. package/lib/telegram/albums.js +104 -0
  173. package/lib/telegram/albums.js.map +1 -0
  174. package/lib/telegram/api.d.ts +238 -0
  175. package/lib/telegram/api.d.ts.map +1 -0
  176. package/lib/telegram/api.js +518 -0
  177. package/lib/telegram/api.js.map +1 -0
  178. package/lib/telegram/poller.d.ts +59 -0
  179. package/lib/telegram/poller.d.ts.map +1 -0
  180. package/lib/telegram/poller.js +94 -0
  181. package/lib/telegram/poller.js.map +1 -0
  182. package/lib/telegram/types.d.ts +130 -0
  183. package/lib/telegram/types.d.ts.map +1 -0
  184. package/lib/telegram/types.js +9 -0
  185. package/lib/telegram/types.js.map +1 -0
  186. package/lib/telegram/typing.d.ts +47 -0
  187. package/lib/telegram/typing.d.ts.map +1 -0
  188. package/lib/telegram/typing.js +91 -0
  189. package/lib/telegram/typing.js.map +1 -0
  190. package/lib/versions.d.ts +63 -0
  191. package/lib/versions.d.ts.map +1 -0
  192. package/lib/versions.js +135 -0
  193. package/lib/versions.js.map +1 -0
  194. package/package.json +94 -10
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Sympoies contributors
3
+ Copyright (c) 2026 Adam Suchiha Fizullah
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
package/NOTICE ADDED
@@ -0,0 +1,19 @@
1
+ dsh-telegram provenance notice
2
+
3
+ This package is independently maintained by Sympoies.
4
+
5
+ It is derived from @ashafizullah/dsh-telegram 0.5.1, originally published by
6
+ Adam Suchiha Fizullah from:
7
+
8
+ https://github.com/ashafizullah/dsh-telegram
9
+
10
+ Imported source commit:
11
+
12
+ 183c53e1098ab49ee8985fa9631aa39b60a803d4
13
+
14
+ The initial Sympoies release also incorporates the reasoning-effort route fix
15
+ from the downstream contribution at commit:
16
+
17
+ 13c6844995d6cb73aae664495bee456afa76fdf1
18
+
19
+ The original copyright and MIT license are retained in LICENSE.
package/README.md CHANGED
@@ -1,3 +1,590 @@
1
- # @sympoies/dsh-telegram
1
+ <div align="center">
2
2
 
3
- Registry bootstrap only. Install a stable release instead.
3
+ <h1>dsh-telegram</h1>
4
+
5
+ <p><strong>Talk to your agent from Telegram — and actually answer it when it asks something.</strong></p>
6
+
7
+ <p>
8
+ <a href="https://github.com/sympoies/dsh-plugins/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/sympoies/dsh-plugins/actions/workflows/ci.yml/badge.svg"></a>
9
+ <a href="https://www.npmjs.com/package/@sympoies/dsh-telegram"><img alt="npm" src="https://img.shields.io/npm/v/%40sympoies/dsh-telegram?logo=npm&logoColor=white&color=cb3837"></a>
10
+ <a href="LICENSE"><img alt="license" src="https://img.shields.io/npm/l/%40sympoies/dsh-telegram?color=3da639"></a>
11
+ <a href="package.json"><img alt="node" src="https://img.shields.io/node/v/%40sympoies/dsh-telegram?logo=node.js&logoColor=white&color=5fa04e"></a>
12
+ <a href="https://core.telegram.org/bots/api"><img alt="Bot API" src="https://img.shields.io/badge/Bot%20API-10.1%2B-2ca5e0?logo=telegram&logoColor=white"></a>
13
+ <a href="https://github.com/deepseek-ai/deepseek-harness"><img alt="DeepSeek Harness" src="https://img.shields.io/badge/DeepSeek-Harness-4d6bfe"></a>
14
+ </p>
15
+
16
+ </div>
17
+
18
+ A Telegram front end for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
19
+
20
+ This package is independently maintained by Sympoies from Adam Suchiha
21
+ Fizullah's MIT-licensed
22
+ [`@ashafizullah/dsh-telegram`](https://github.com/ashafizullah/dsh-telegram).
23
+ The exact imported source and downstream repair are recorded in [NOTICE](NOTICE).
24
+
25
+ Talk to your agent from your phone — and actually *answer* it when it asks something.
26
+
27
+ ## Why this exists
28
+
29
+ Running an agent from a chat app breaks down at two specific points, and this
30
+ plugin is built around fixing them.
31
+
32
+ **The agent writes markdown; Telegram was getting it raw.** Models answer with
33
+ `**bold**`, headings, tables, task lists and fenced code. Sent as plain text,
34
+ all of that arrives as literal asterisks and pipes.
35
+
36
+ Since Bot API 10.1 Telegram parses markdown itself, so this plugin forwards the
37
+ agent's reply almost verbatim through `sendRichMessage` — tables render as
38
+ tables, checklists as checklists — and the message cap rises from 4096 to
39
+ 32768 characters with it.
40
+
41
+ **The agent asks questions; there was nowhere to answer them.** When the agent
42
+ calls `ask_user_question`, or a tool needs your permission, the harness blocks
43
+ and waits for a UI to answer. Only the browser could. A conversation held
44
+ entirely in Telegram would stall on the first question with no way to clear it.
45
+ This plugin registers itself as that UI, so questions and approvals arrive as
46
+ buttons in the chat.
47
+
48
+ ## Requirements
49
+
50
+ - DeepSeek Harness with a profile you can add plugins to
51
+ - **Bot API 10.1 or later**, for `sendRichMessage` and `sendRichMessageDraft`
52
+ - Node 24 or later
53
+
54
+ There is no HTML fallback. Telegram's rich markdown parser is forgiving — an
55
+ unterminated code fence or a line of stray markers is accepted rather than
56
+ rejected — so a mid-stream frame does not need one.
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ npx @deepseek-ai/dsh plugin --profile web add -w @sympoies/dsh-telegram
62
+ ```
63
+
64
+ Or from a checkout, to develop against it:
65
+
66
+ ```bash
67
+ git clone https://github.com/sympoies/dsh-plugins.git
68
+ cd dsh-plugins
69
+ npm ci
70
+ npm run build --workspace @sympoies/dsh-telegram
71
+
72
+ npx @deepseek-ai/dsh plugin --profile web add -w "$(pwd)/packages/dsh-telegram"
73
+ ```
74
+
75
+ Then give it a bot token. Create a bot with [@BotFather](https://t.me/BotFather)
76
+ and store the token under the credential reference — never in a config file:
77
+
78
+ ```bash
79
+ npx @deepseek-ai/dsh credentials set TELEGRAM_BOT_TOKEN
80
+ ```
81
+
82
+ Start the profile. The console prints a claim code:
83
+
84
+ ```
85
+ [dsh-telegram] this bot has no owner yet. Message @your_bot with:
86
+
87
+ /claim 3f9a2b1c
88
+ ```
89
+
90
+ Send that to your bot and it is yours. Until then it answers nobody.
91
+
92
+ The code is also written to `$DSH_HOME/dsh-telegram/claim-code.txt`, owner-only,
93
+ because several profiles compose no console sink at all and a code nobody can
94
+ read makes the bot permanently unusable.
95
+
96
+ ## Access
97
+
98
+ A Telegram bot is reachable by anyone who knows its handle, and the agent behind
99
+ it can run shell commands on your machine. So the default is closed.
100
+
101
+ - **Claim flow** (default): the first person to send the console-printed code
102
+ becomes the owner. Ownership is durable and single-shot — a later claim is
103
+ refused even with the right code, so a leaked code grants nothing.
104
+ - **Allowlist**: set `allowFrom` to a list of Telegram user ids to skip claiming
105
+ entirely. Use `/whoami` to find your id.
106
+
107
+ The claim code changes on every restart and is never sent over Telegram.
108
+
109
+ ## Commands
110
+
111
+ | Command | What it does |
112
+ | --- | --- |
113
+ | `/start` | What this bot is, and whether you may use it |
114
+ | `/help` | List the commands |
115
+ | `/claim <code>` | Take ownership of an unclaimed bot |
116
+ | `/new` | Start a fresh conversation, forgetting the current one |
117
+ | `/cd [path]` | Show or change the working directory |
118
+ | `/model [what]` | Show the model, `/model list`, or switch to one |
119
+ | `/effort [level]` | Show or change how hard the model thinks |
120
+ | `/vision [what]` | Show, change, or turn off the model that reads images |
121
+ | `/permission [name]` | Show or change what the agent may do here |
122
+ | `/diag` | What the plugin can see about itself, and recent failures |
123
+ | `/screenshot` | Send a picture of the harness machine's screen |
124
+ | `/sessions` | Pick up an earlier conversation from this chat |
125
+ | `/status` | Session id, working directory, and whether it is loaded |
126
+ | `/stop` | Cancel whatever the agent is doing right now |
127
+ | `/whoami` | Your Telegram user id |
128
+
129
+ ### In a group
130
+
131
+ A bot that answers every line is one nobody keeps in the room, so in a group it
132
+ answers only when @mentioned or replied to — the convention people already use.
133
+ Its own mention is stripped before the prompt, because that is addressing
134
+ rather than content, and replying to something it said continues the exchange
135
+ without an @mention on every line. Private chats are untouched. Set
136
+ `requireMentionInGroups` to `false` for the older behaviour.
137
+
138
+ The mention is compared against Telegram's own parsed span rather than searched
139
+ for in the text: `@mybot_staging` contains `@mybot`, and a substring match would
140
+ hand another bot's mentions to this one.
141
+
142
+ ### What the agent is allowed to do
143
+
144
+ A deployment picks one permission default for everything it runs, usually with
145
+ the web UI in mind: loopback-only, with a person watching. A Telegram bot is
146
+ reachable from anywhere and gated by a list of user ids, so the same
147
+ `danger-full-access` reads differently there. `permissionPreset` names one of
148
+ the deployment's own presets for Telegram conversations alone.
149
+
150
+ It also decides whether the approval buttons work: under a preset whose
151
+ approval policy is `never` nothing ever asks, so they can never appear.
152
+ Choosing one that asks is what turns them on.
153
+
154
+ ### A picture of the screen
155
+
156
+ `/screenshot` sends what the harness machine is showing. It is the reason the
157
+ bot exists, applied to the screen itself: the machine is at a desk and you are
158
+ not, so checking what a long build is showing needs a trip back to the keyboard
159
+ otherwise.
160
+
161
+ It is **off by default**, and the switch is a deployment setting rather than a
162
+ chat command on purpose. A screen holds whatever happens to be on it — an open
163
+ password manager, someone else's messages, an unrelated customer's data — and
164
+ this is the one thing here that sends the machine's own contents outward
165
+ without the agent being involved. Turning it on should take the same access as
166
+ configuring the bot.
167
+
168
+ macOS also needs Screen Recording permission for the process running the
169
+ harness. Without it `screencapture` still succeeds and returns the desktop
170
+ picture with no windows, which looks like a broken feature rather than a
171
+ missing permission, so that case is named rather than shrugged at. Grant it in
172
+ System Settings → Privacy & Security → Screen Recording and restart the
173
+ harness.
174
+
175
+ A capture over Telegram's 10 MB photo limit is sent as a document instead,
176
+ which takes 50 MB — a large display's PNG routinely needs it.
177
+
178
+ ### Effort, and what the agent may do
179
+
180
+ `/effort` shows how hard the model thinks and lists what *that model* offers —
181
+ read from the model itself, because `low`/`medium`/`high` is one provider's
182
+ vocabulary rather than everyone's, and offering an effort a model does not have
183
+ would fail the turn instead of the command. `/effort default` gives it back.
184
+
185
+ `/permission` shows what the agent may do here and switches it: `read-only`,
186
+ `workspace-write`, `danger-full-access`, or whatever else the deployment
187
+ defines — the names are read from its own table, not fixed here. Spelling is
188
+ forgiving, so `full access`, `full-access` and `readonly` all land, and a
189
+ shorthand matching two presets is refused rather than guessed at. The change
190
+ applies to the conversation in flight as well as the next one, because the
191
+ reason to tighten it is usually the turn about to run.
192
+
193
+ Each of these is per conversation and sits on top of what the settings page
194
+ configures. That is two surfaces showing related state, so the commands say
195
+ which layer answered: once a conversation has chosen for itself, its reply
196
+ names the deployment default underneath. Without that the page reads as though
197
+ it were lying — it shows one thing while the chat does another, and nothing
198
+ connects them. `/… default` gives a conversation back to the deployment.
199
+
200
+ `/status` answers all of it in one message — session, directory, model, effort,
201
+ permission — since having to run four commands to learn what you are talking to
202
+ is four commands too many.
203
+
204
+ ### Which model, and which conversation
205
+
206
+ `/model` says which model the conversation is on, `/model list` shows what is
207
+ configured, and `/model provider/model` switches. A bare model id works when
208
+ only one provider offers it; when several do, it asks which. Unlike `/cd` this
209
+ does not restart anything — the harness reads a mutable selection while
210
+ assembling each step, so the change lands on the next message with the history
211
+ intact. `/model default` gives the conversation back to the deployment.
212
+
213
+ `/sessions` offers this chat's earlier conversations as buttons. `/new` is
214
+ otherwise a one-way door: the harness keeps every log, but the binding naming
215
+ the current one is replaced, and from a phone there is no other way back. The
216
+ list is this plugin's own, so it holds conversations from this chat rather than
217
+ every session the web UI ever opened.
218
+
219
+ ### Which tools the agent has
220
+
221
+ A preset supplies them. The registries are host-plane, but almost every
222
+ model-facing row — bash, the editor, grep, skills, subagents, todo, plan mode —
223
+ is registered into a *preset's* scope layer, so an agent that joins no preset
224
+ reaches the model with only whatever the host composition registered globally.
225
+ Telegram sessions are composed from the deployment's default preset, or from
226
+ `agentPreset` when one is named, and the choice is recorded in the session
227
+ header so a later reader resolves the same composition.
228
+
229
+ ### Where the agent works
230
+
231
+ `/cd` on its own says where the conversation is; `/cd ~/projects/app` moves it.
232
+ Absolute paths, `~`, and paths relative to where the conversation already is all
233
+ work, and a pasted path keeps its quotes off.
234
+
235
+ Moving starts a fresh conversation, and the bot says so. That is not a shortcut:
236
+ the sandbox derives its writable root from the session's working directory, and
237
+ that root is fixed when the session opens — so a directory change is a new
238
+ session by construction. The choice is remembered per chat and survives both
239
+ `/new` and a restart, which is why it is kept apart from the session binding
240
+ that `/new` discards.
241
+
242
+ A directory that does not exist, one that turns out to be a file, and one that
243
+ cannot be read are three different mistakes and get three different sentences.
244
+ Each leaves the conversation exactly where it was.
245
+
246
+ They are published to Telegram on every connection, so typing `/` in the chat
247
+ offers the list with descriptions. `/claim` drops off it once the bot has an
248
+ owner — it is the one command that stops working the moment it succeeds.
249
+
250
+ Anything else you type is a prompt for the agent.
251
+
252
+ ## What you can send
253
+
254
+ | You send | What the agent gets |
255
+ | --- | --- |
256
+ | Text | The prompt |
257
+ | A photo, or an image sent as a file | What the vision model reads in it, and your caption |
258
+ | Several photos at once | All of them in one message, under your caption |
259
+ | A text file — a log, a stack trace, source | Its contents in the prompt, truncated if very long |
260
+ | A voice note, audio, or video | A note saying it could not be read |
261
+
262
+ Images go through the harness attachment seam, which accepts PNG, JPEG, WebP
263
+ and GIF. Everything else it explicitly defers, so this plugin says so rather
264
+ than accepting the message and quietly dropping what it carried.
265
+
266
+ The seam also refuses an image whose longest side is over `maxImageDimension`,
267
+ 2000 pixels by default — and every full-height phone screenshot is over it:
268
+ 1179×2556 on an iPhone, 1080×2400 on most Android. Telegram sends a photo at
269
+ several rendered sizes, so the largest one that fits is chosen rather than the
270
+ largest one there is, and the seam's published limits are read from the store
271
+ itself so there is no second copy of the number to drift. If the seam refuses a
272
+ size anyway, the next smaller one is tried; when a photo sent uncompressed
273
+ leaves nothing to step down to, the refusal names the limit and says that
274
+ sending it as a photo would let Telegram offer a smaller copy.
275
+
276
+ A file that is too large, or that fails to download, becomes a note in the
277
+ prompt explaining why — your caption still reaches the agent either way.
278
+
279
+ ### Sending several at once
280
+
281
+ Telegram has no "several photos in one message". An album arrives as N separate
282
+ updates tied together only by a shared id, with the caption on exactly one of
283
+ them — so three screenshots used to become three turns, two of them bare images
284
+ the agent had no question for.
285
+
286
+ A message belonging to an album is now held rather than answered, and the group
287
+ goes through as one prompt once it stops growing: your caption, then every
288
+ image. The wait is paid only by albums, and only once each, which beats
289
+ answering the same question three times.
290
+
291
+ ### A model has to be able to look
292
+
293
+ A model that declares no image input rejects the whole request, so an image is
294
+ checked against `inputModalities` before it is sent. **No DeepSeek model
295
+ accepts images** — `deepseek-v4-flash` and `deepseek-v4-pro` are both text-only
296
+ — so out of the box a screenshot is declined with a sentence naming what would
297
+ work, and your caption still reaches the agent.
298
+
299
+ **Settings → Telegram → Attachments** offers a dropdown of the models already
300
+ configured in Settings → Models. Pick one and images become readable.
301
+
302
+ `/vision` chooses which model reads images here, or turns the reading off
303
+ entirely with `/vision off`. Off is a real answer rather than the absence of
304
+ one: a conversation whose own model can see wants no reader at all, and saying
305
+ so outranks whatever the deployment configured. Like the rest, it is per
306
+ conversation and survives `/new`.
307
+
308
+ **If the conversation's own model reads images, none of this happens.** The
309
+ picture goes straight through, and the model looks at it. Since DeepSeek
310
+ shipped `deepseek-v4-flash-vision-exp` that is a real choice rather than a
311
+ hypothetical, and it is the better one when the screenshot is not just text: a
312
+ transcription loses the diagram, the chart, the misaligned layout — everything
313
+ you were actually asking about.
314
+
315
+ The indirection below exists because a provider inspects the whole request
316
+ history, so an image binds a conversation to a model that can see. When that
317
+ model IS the one you chose, there is nothing to be stuck on and nothing to work
318
+ around — so the reading, the refusal, and the sticky routing all stand down
319
+ together.
320
+
321
+ The picture never enters your conversation. It goes to a throwaway session on
322
+ that model, which is asked to transcribe every piece of text in it and describe
323
+ what it is; the reply comes back as ordinary text and *that* is what your
324
+ conversation receives, under your own caption. The session is disposed either
325
+ way — it exists for one turn.
326
+
327
+ The indirection is the point. A provider checks the entire request history for
328
+ images, so an image left in a conversation binds it to a model that can see for
329
+ as long as it lives: one screenshot and every later turn — however plain its own
330
+ text — has to run there too, away from the model you chose and the tools
331
+ configured around it. Reading it elsewhere keeps the history free of images, so
332
+ the conversation stays where it was, keeps its tools, and never gets stuck.
333
+
334
+ With no vision model configured nothing reaches this path at all: the image is
335
+ declined before it is even downloaded, with a sentence naming the models that
336
+ would have worked, and your caption still reaches the agent.
337
+
338
+ If a reading is attempted and fails — the model unreachable, the turn timing out
339
+ after two minutes — the picture goes through as it is and the conversation moves
340
+ onto the vision model instead, durably, until `/new`. That is the fallback
341
+ rather than the design, and the prompt says which happened.
342
+
343
+ The catalog the browser can read carries no modality information, so the
344
+ dropdown cannot mark which models accept images. The host checks that when an
345
+ image is actually sent, which is the one place the answer is certain. Vision
346
+ models reach the harness through a provider that carries them, such as an
347
+ OpenAI-compatible route added in Settings → Models, whose model entry declares
348
+ `input: [text, image]`.
349
+
350
+ ### When no model can look
351
+
352
+ With no vision model configured the image used to be refused outright, and the
353
+ answer was a sentence about model configuration rather than anything about the
354
+ picture. If `tesseract` is installed, its text is read instead.
355
+
356
+ It is a fallback and says so. OCR reads text; it does not see. A screenshot of
357
+ an error, a log or a receipt comes back cleanly — crisp text, high contrast, no
358
+ perspective is exactly its best case — while a whiteboard, an architecture
359
+ diagram or a chart comes back as scattered words with nothing to say what the
360
+ picture was. The reading is therefore labelled as OCR wherever it goes: an
361
+ agent handed unlabelled OCR treats a misread digit as a fact, and a receipt's
362
+ amount is precisely what it gets wrong.
363
+
364
+ Tesseract is never assumed. No operating system this runs on ships it, so its
365
+ absence is the ordinary case: it is probed once, and where it is missing the
366
+ old refusal stands — now naming both ways forward. `/diag` says which of the
367
+ two this machine has.
368
+
369
+ The same fallback covers a vision model that was configured but could not be
370
+ reached, for the same reason: reading the text beats returning nothing.
371
+
372
+ Latin script reads well with `eng` alone — Indonesian, numbers, dates and
373
+ amounts all come through — so `media.ocr.languages` only needs changing for a
374
+ different script. `tesseract --list-langs` says what is installed.
375
+
376
+ ### When a conversation gets stuck
377
+
378
+ A turn can fail in a way no retry clears — most often that one: an earlier
379
+ message carries content the current model will not take, and nothing typed next
380
+ will change it. The bot recognises those, says what failed, and offers a button
381
+ that starts a fresh conversation. Asking the user to remember `/new` would be
382
+ asking them to diagnose the plugin.
383
+
384
+ Failures that may pass on their own are reported without a button, because
385
+ retrying really is the right thing to do with them.
386
+
387
+ ## Configuring it
388
+
389
+ Open **Settings → Telegram** in the harness web UI. The page writes straight to
390
+ the settings document — there is no Save button, because the host applies a
391
+ committed change by reconnecting, and a staged form would let the page and the
392
+ running bot disagree about what is configured.
393
+
394
+ The bot token is the exception. It is a secret, so it never rides the settings
395
+ wire in either direction: the page learns only whether one is stored, writes it
396
+ through the credentials domain, and refuses to offer an edit for a reference the
397
+ environment already supplies (a write there would look like it worked while
398
+ resolution kept returning the shadowing value).
399
+
400
+ Everything on the page is equally settable in a profile patch, for a deployment
401
+ that configures by file:
402
+
403
+ ## Configuration
404
+
405
+ Every field has a working default; an empty config runs.
406
+
407
+ | Key | Default | Meaning |
408
+ | --- | --- | --- |
409
+ | `enabled` | `true` | Whether the connection starts with the harness |
410
+ | `tokenRef` | `TELEGRAM_BOT_TOKEN` | Credential reference holding the token |
411
+ | `baseUrl` | `https://api.telegram.org` | Bot API origin; change only for a proxy |
412
+ | `allowFrom` | `[]` | User ids allowed in; empty enables the claim flow |
413
+ | `cwd` | harness cwd | Directory a conversation starts in until `/cd` moves it |
414
+ | `agentPreset` | `""` | Preset Telegram conversations are composed from; empty takes the deployment default. The preset supplies the tools |
415
+ | `permissionPreset` | `""` | Permission preset Telegram runs under, from the deployment's own table; empty follows the deployment default |
416
+ | `requireMentionInGroups` | `true` | In a group, answer only when @mentioned or replied to |
417
+ | `screenshot.enabled` | `false` | Allow `/screenshot`. Off by default; macOS also needs Screen Recording permission |
418
+ | `streaming.enabled` | `true` | Show the answer as it is written |
419
+ | `streaming.throttleMs` | `1200` | Minimum gap between streamed frames |
420
+ | `timeoutMs` | `30000` | Per-request Bot API timeout |
421
+ | `longPollSeconds` | `25` | How long Telegram holds an empty poll open |
422
+ | `media.enabled` | `true` | Read images and text files the user sends |
423
+ | `media.maxBytes` | `20 MB` | Refuse anything larger; Telegram caps bot downloads there |
424
+ | `media.maxTextChars` | `60000` | Truncate an inlined text file to this many characters |
425
+ | `media.ocr.enabled` | `true` | Read an image's text with tesseract when no vision model can. Does nothing unless tesseract is installed |
426
+ | `media.ocr.languages` | `eng` | Languages tesseract reads; join several with `+`. Only installed ones work |
427
+ | `media.visionModel` | `""` | `provider/model` that reads images in a session of its own; empty sends the image to the conversation itself. Picked from a dropdown on the settings page |
428
+ | `reconnect.baseDelayMs` | `1000` | Delay before the first reconnect attempt |
429
+ | `reconnect.maxDelayMs` | `30000` | Longest delay between reconnect attempts |
430
+
431
+ ## Diagnostics
432
+
433
+ `/diag` reports what the plugin can see about itself: the connection, which
434
+ harness seams this deployment actually composed, and the last twenty things
435
+ that went wrong.
436
+
437
+ It also says which versions are running and whether anything newer is
438
+ published — read-only, and cached for an hour so asking twice costs nothing.
439
+ There is deliberately no `/update` to go with it: updating the harness takes
440
+ effect only after a restart, and restarting it from inside a plugin running in
441
+ it kills the process answering you, with nothing to bring it back on a machine
442
+ with no supervisor. Knowing you are behind is the useful half; acting on it
443
+ belongs where you can watch it.
444
+
445
+ The seam list is the useful part. An absent seam explains a whole class of "why
446
+ does it not do that" without anyone having to guess — a missing `agentPresets`
447
+ is why Telegram agents once reached the model with almost no tools, and nothing
448
+ anywhere said so.
449
+
450
+ `ctx.logger` reaches whatever sink the deployment composed, and several profiles
451
+ compose none — so a plugin that only logs its failures is silent about them.
452
+ This one also writes its state to `$DSH_HOME/dsh-telegram/status.json` on every
453
+ transition:
454
+
455
+ ```json
456
+ { "state": "connected", "bot": "your_bot", "updatedAt": "..." }
457
+ ```
458
+
459
+ `connecting`, `connected`, `idle` with a reason, `failed` with a reason. The bot
460
+ token never appears in it.
461
+
462
+ ## Living alongside the web UI
463
+
464
+ The harness allows exactly one user-questions provider, and in a profile that
465
+ also runs the web app the browser has already claimed it. This plugin takes the
466
+ slot over and keeps the browser's provider as a fallback: a question belonging
467
+ to a browser session is forwarded straight back to it, and one belonging to a
468
+ Telegram conversation becomes buttons in the chat. Unloading the plugin puts
469
+ the previous arrangement back exactly.
470
+
471
+ Approvals compose natively — the harness runs them as a waterfall — so this
472
+ plugin answers for its own sessions and passes every other one along.
473
+
474
+ ## How it fits together
475
+
476
+ ```
477
+ Telegram Bot API
478
+ │ long poll: message + callback_query
479
+
480
+ UpdatePoller ──► UpdateRouter ──┬──► SessionRunner ──► ctx.agents
481
+ │ │
482
+ │ └──► VisionExtractor ──► a throwaway
483
+ │ session
484
+ ├──► MediaCollector ──► ctx.attachments
485
+ ├──► TelegramQuestionProvider ──► ctx.userQuestions
486
+ └──► TelegramApprovalAnswerer ──► approval/request
487
+
488
+ ctx.on('session/event') ──┬──► VisionExtractor (its own reading sessions)
489
+ └──► TurnBridge ──► RichReplyStream ──► sendRichMessage
490
+
491
+ TypingIndicator (held by the router and the bridge until a reply shows)
492
+ ```
493
+
494
+ ### How a reply is streamed
495
+
496
+ Telegram offers two mechanisms, and they are not interchangeable:
497
+
498
+ - **Private chats** use `sendRichMessageDraft` — an ephemeral preview that
499
+ animates between frames sharing a draft id. It expires 30 seconds after its
500
+ last frame, so a heartbeat re-sends the current text during a long tool call;
501
+ otherwise the preview would vanish and the bot would look dead. A draft is
502
+ never persisted, so the turn ends with a real `sendRichMessage`.
503
+ - **Groups have no draft API.** There the finished reply is simply sent when it
504
+ is ready.
505
+
506
+ Both end with one permanent rich message.
507
+
508
+ ### Nothing is posted until there is something to say
509
+
510
+ Telegram's own typing indicator carries the wait, and the reply appears only
511
+ once it has content — the first words, or the name of a tool the agent reached
512
+ for. An ellipsis posted the moment a turn opens tells the user what they
513
+ already know, and in a group it is a permanent message telling them.
514
+
515
+ The indicator is held rather than sent. `sendChatAction` lapses after five
516
+ seconds, which is shorter than almost everything worth waiting for here —
517
+ downloading a file, reading an image on a vision model, a turn queued behind
518
+ the last one, a minute inside a tool call — so one call reads as a bot that
519
+ started and died. Holds are counted per conversation and re-sent inside their
520
+ own expiry, so the router's hold while it reads an attachment and the bridge's
521
+ hold over the turn that follows overlap cleanly, and typing stops when the last
522
+ of them lets go. A ten-minute backstop covers a release that never arrives.
523
+
524
+ Nothing is ever redrawn with less than it showed before. When a tool finishes
525
+ and there is no text yet, the line naming it stays until real text replaces it
526
+ — Telegram refuses an empty draft, so the alternative was trading the last
527
+ thing that happened for a frame that said nothing.
528
+
529
+ While the agent works, the running tool is shown above the reply in a
530
+ `<tg-thinking>` block:
531
+
532
+ ```
533
+ ▸ bash: npm test
534
+
535
+ Here is what I found so far…
536
+ ```
537
+
538
+ Telegram accepts that block in a draft and nowhere else, which matches its
539
+ lifetime exactly — it disappears when the turn is persisted, so the finished
540
+ reply carries the answer rather than the scaffolding that produced it. It is a
541
+ single clipped line: a tool call's arguments can be an entire file, and the
542
+ point is knowing the agent is alive, not reading a transcript.
543
+
544
+ Access is checked before anything else, so no unauthorised text reaches the
545
+ agent — not even a command.
546
+
547
+ ## Development
548
+
549
+ ```bash
550
+ npm ci
551
+ npm test --workspace @sympoies/dsh-telegram
552
+ npm run test:coverage --workspace @sympoies/dsh-telegram
553
+ npm run typecheck --workspace @sympoies/dsh-telegram
554
+ npm run build --workspace @sympoies/dsh-telegram
555
+ ```
556
+
557
+ Releases use this monorepo's package-independent workflow and npm trusted
558
+ publishing. The workflow validates the selected workspace, version and tag
559
+ before GitHub exchanges its OIDC identity for a short-lived npm credential.
560
+ This package stores no npm token and has no package-specific release workflow.
561
+
562
+ Every module runs without a harness, which is what keeps the suite fast: the
563
+ plugin entry is exercised against a real HTTP stub of the Bot API, and the
564
+ browser bundle is materialized exactly as the shell materializes it.
565
+
566
+ ### The browser half
567
+
568
+ `build.client.mjs` wraps an esbuild CJS bundle in the shell's lazy-CJS factory
569
+ envelope (`window.__ModuleLoader__.load({ id, factory })`). That envelope is
570
+ reproduced rather than imported: the harness's `clientBundle` preset is not
571
+ published, which its own documentation lists as a known limitation for plugins
572
+ shipped outside its repository. It is therefore the single place this plugin is
573
+ coupled to an internal format, and `test/client-bundle.test.ts` pins it — the
574
+ test runs the build, materializes the factory with a stub `require`, and checks
575
+ that `apply` claims its settings seat. A harness release that changes the format
576
+ fails there by name instead of showing up as a blank Settings page.
577
+
578
+ React and the shell's own packages are marked external; bundling a second React
579
+ would break every hook the moment the page mounted.
580
+
581
+ ## Known limitations
582
+
583
+ - **One directory per conversation.** `/cd` moves a conversation, but a
584
+ session cannot be moved: the change starts a fresh one.
585
+ - **No voice, audio or video.** The harness attachment seam takes images only.
586
+
587
+ ## License
588
+
589
+ MIT. The original copyright and license are retained in [LICENSE](LICENSE),
590
+ and the imported source is documented in [NOTICE](NOTICE).
@@ -0,0 +1,14 @@
1
+ # dsh-telegram — DSH bundle patch.
2
+ #
3
+ # One loader entry. `inject` lists only the services the plugin cannot run
4
+ # without: an agent registry to drive, and the credential seam holding the bot
5
+ # token. Listing a service here makes it REQUIRED — the entry never activates
6
+ # without it — so the interactive seams are bound at runtime instead, inside
7
+ # apply() with ctx.inject(). That is what lets this load on a profile that
8
+ # provides neither user-questions nor approvals.
9
+ - insert:
10
+ - id: telegram
11
+ name: '@sympoies/dsh-telegram'
12
+ inject:
13
+ - agents
14
+ - credentials