@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.
- package/LICENSE +1 -2
- package/NOTICE +19 -0
- package/README.md +589 -2
- package/cordis.patch.yml +14 -0
- package/lib/access.d.ts +76 -0
- package/lib/access.d.ts.map +1 -0
- package/lib/access.js +137 -0
- package/lib/access.js.map +1 -0
- package/lib/client.js +835 -0
- package/lib/commands.d.ts +51 -0
- package/lib/commands.d.ts.map +1 -0
- package/lib/commands.js +83 -0
- package/lib/commands.js.map +1 -0
- package/lib/config.d.ts +125 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +97 -0
- package/lib/config.js.map +1 -0
- package/lib/diagnostics.d.ts +58 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +59 -0
- package/lib/diagnostics.js.map +1 -0
- package/lib/failures.d.ts +71 -0
- package/lib/failures.d.ts.map +1 -0
- package/lib/failures.js +139 -0
- package/lib/failures.js.map +1 -0
- package/lib/harness/host.d.ts +106 -0
- package/lib/harness/host.d.ts.map +1 -0
- package/lib/harness/host.js +142 -0
- package/lib/harness/host.js.map +1 -0
- package/lib/harness/message.d.ts +45 -0
- package/lib/harness/message.d.ts.map +1 -0
- package/lib/harness/message.js +28 -0
- package/lib/harness/message.js.map +1 -0
- package/lib/harness/model-selection.d.ts +82 -0
- package/lib/harness/model-selection.d.ts.map +1 -0
- package/lib/harness/model-selection.js +103 -0
- package/lib/harness/model-selection.js.map +1 -0
- package/lib/harness/questions-seam.d.ts +34 -0
- package/lib/harness/questions-seam.d.ts.map +1 -0
- package/lib/harness/questions-seam.js +45 -0
- package/lib/harness/questions-seam.js.map +1 -0
- package/lib/harness/types.d.ts +108 -0
- package/lib/harness/types.d.ts.map +1 -0
- package/lib/harness/types.js +23 -0
- package/lib/harness/types.js.map +1 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +977 -0
- package/lib/index.js.map +1 -0
- package/lib/interact/approvals.d.ts +59 -0
- package/lib/interact/approvals.d.ts.map +1 -0
- package/lib/interact/approvals.js +132 -0
- package/lib/interact/approvals.js.map +1 -0
- package/lib/interact/pending.d.ts +56 -0
- package/lib/interact/pending.d.ts.map +1 -0
- package/lib/interact/pending.js +89 -0
- package/lib/interact/pending.js.map +1 -0
- package/lib/interact/questions.d.ts +100 -0
- package/lib/interact/questions.d.ts.map +1 -0
- package/lib/interact/questions.js +255 -0
- package/lib/interact/questions.js.map +1 -0
- package/lib/interact/surface.d.ts +33 -0
- package/lib/interact/surface.d.ts.map +1 -0
- package/lib/interact/surface.js +39 -0
- package/lib/interact/surface.js.map +1 -0
- package/lib/interact/text-capture.d.ts +38 -0
- package/lib/interact/text-capture.d.ts.map +1 -0
- package/lib/interact/text-capture.js +75 -0
- package/lib/interact/text-capture.js.map +1 -0
- package/lib/media/collect.d.ts +157 -0
- package/lib/media/collect.d.ts.map +1 -0
- package/lib/media/collect.js +235 -0
- package/lib/media/collect.js.map +1 -0
- package/lib/media/extractor.d.ts +117 -0
- package/lib/media/extractor.d.ts.map +1 -0
- package/lib/media/extractor.js +207 -0
- package/lib/media/extractor.js.map +1 -0
- package/lib/media/intake.d.ts +57 -0
- package/lib/media/intake.d.ts.map +1 -0
- package/lib/media/intake.js +115 -0
- package/lib/media/intake.js.map +1 -0
- package/lib/media/limits.d.ts +69 -0
- package/lib/media/limits.d.ts.map +1 -0
- package/lib/media/limits.js +97 -0
- package/lib/media/limits.js.map +1 -0
- package/lib/media/ocr.d.ts +66 -0
- package/lib/media/ocr.d.ts.map +1 -0
- package/lib/media/ocr.js +131 -0
- package/lib/media/ocr.js.map +1 -0
- package/lib/media/screenshot.d.ts +57 -0
- package/lib/media/screenshot.d.ts.map +1 -0
- package/lib/media/screenshot.js +101 -0
- package/lib/media/screenshot.js.map +1 -0
- package/lib/media/vision.d.ts +60 -0
- package/lib/media/vision.d.ts.map +1 -0
- package/lib/media/vision.js +74 -0
- package/lib/media/vision.js.map +1 -0
- package/lib/render/clamp.d.ts +34 -0
- package/lib/render/clamp.d.ts.map +1 -0
- package/lib/render/clamp.js +65 -0
- package/lib/render/clamp.js.map +1 -0
- package/lib/render/escape.d.ts +15 -0
- package/lib/render/escape.d.ts.map +1 -0
- package/lib/render/escape.js +17 -0
- package/lib/render/escape.js.map +1 -0
- package/lib/reply/activity.d.ts +31 -0
- package/lib/reply/activity.d.ts.map +1 -0
- package/lib/reply/activity.js +88 -0
- package/lib/reply/activity.js.map +1 -0
- package/lib/reply/rich-stream.d.ts +173 -0
- package/lib/reply/rich-stream.d.ts.map +1 -0
- package/lib/reply/rich-stream.js +286 -0
- package/lib/reply/rich-stream.js.map +1 -0
- package/lib/reply/split-markdown.d.ts +21 -0
- package/lib/reply/split-markdown.d.ts.map +1 -0
- package/lib/reply/split-markdown.js +87 -0
- package/lib/reply/split-markdown.js.map +1 -0
- package/lib/reply/turn-bridge.d.ts +143 -0
- package/lib/reply/turn-bridge.d.ts.map +1 -0
- package/lib/reply/turn-bridge.js +182 -0
- package/lib/reply/turn-bridge.js.map +1 -0
- package/lib/router.d.ts +415 -0
- package/lib/router.d.ts.map +1 -0
- package/lib/router.js +663 -0
- package/lib/router.js.map +1 -0
- package/lib/secrets.d.ts +30 -0
- package/lib/secrets.d.ts.map +1 -0
- package/lib/secrets.js +51 -0
- package/lib/secrets.js.map +1 -0
- package/lib/session/bindings.d.ts +81 -0
- package/lib/session/bindings.d.ts.map +1 -0
- package/lib/session/bindings.js +170 -0
- package/lib/session/bindings.js.map +1 -0
- package/lib/session/history.d.ts +50 -0
- package/lib/session/history.d.ts.map +1 -0
- package/lib/session/history.js +127 -0
- package/lib/session/history.js.map +1 -0
- package/lib/session/models.d.ts +99 -0
- package/lib/session/models.d.ts.map +1 -0
- package/lib/session/models.js +123 -0
- package/lib/session/models.js.map +1 -0
- package/lib/session/permission.d.ts +83 -0
- package/lib/session/permission.d.ts.map +1 -0
- package/lib/session/permission.js +107 -0
- package/lib/session/permission.js.map +1 -0
- package/lib/session/picker.d.ts +49 -0
- package/lib/session/picker.d.ts.map +1 -0
- package/lib/session/picker.js +107 -0
- package/lib/session/picker.js.map +1 -0
- package/lib/session/preferences.d.ts +55 -0
- package/lib/session/preferences.d.ts.map +1 -0
- package/lib/session/preferences.js +102 -0
- package/lib/session/preferences.js.map +1 -0
- package/lib/session/recovery.d.ts +66 -0
- package/lib/session/recovery.d.ts.map +1 -0
- package/lib/session/recovery.js +112 -0
- package/lib/session/recovery.js.map +1 -0
- package/lib/session/runner.d.ts +223 -0
- package/lib/session/runner.d.ts.map +1 -0
- package/lib/session/runner.js +260 -0
- package/lib/session/runner.js.map +1 -0
- package/lib/session/workspaces.d.ts +28 -0
- package/lib/session/workspaces.d.ts.map +1 -0
- package/lib/session/workspaces.js +53 -0
- package/lib/session/workspaces.js.map +1 -0
- package/lib/telegram/addressing.d.ts +39 -0
- package/lib/telegram/addressing.d.ts.map +1 -0
- package/lib/telegram/addressing.js +81 -0
- package/lib/telegram/addressing.js.map +1 -0
- package/lib/telegram/albums.d.ts +46 -0
- package/lib/telegram/albums.d.ts.map +1 -0
- package/lib/telegram/albums.js +104 -0
- package/lib/telegram/albums.js.map +1 -0
- package/lib/telegram/api.d.ts +238 -0
- package/lib/telegram/api.d.ts.map +1 -0
- package/lib/telegram/api.js +518 -0
- package/lib/telegram/api.js.map +1 -0
- package/lib/telegram/poller.d.ts +59 -0
- package/lib/telegram/poller.d.ts.map +1 -0
- package/lib/telegram/poller.js +94 -0
- package/lib/telegram/poller.js.map +1 -0
- package/lib/telegram/types.d.ts +130 -0
- package/lib/telegram/types.d.ts.map +1 -0
- package/lib/telegram/types.js +9 -0
- package/lib/telegram/types.js.map +1 -0
- package/lib/telegram/typing.d.ts +47 -0
- package/lib/telegram/typing.d.ts.map +1 -0
- package/lib/telegram/typing.js +91 -0
- package/lib/telegram/typing.js.map +1 -0
- package/lib/versions.d.ts +63 -0
- package/lib/versions.d.ts.map +1 -0
- package/lib/versions.js +135 -0
- package/lib/versions.js.map +1 -0
- package/package.json +94 -10
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026
|
|
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
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
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).
|
package/cordis.patch.yml
ADDED
|
@@ -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
|