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

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