@pedrohnas/opencode-telegram 0.1.0 → 1.2.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/.env.example +17 -0
- package/bunfig.toml +2 -0
- package/docs/PROGRESS.md +327 -0
- package/docs/mapping.md +326 -0
- package/docs/plans/phase-1.md +176 -0
- package/docs/plans/phase-2.md +235 -0
- package/docs/plans/phase-3.md +485 -0
- package/docs/plans/phase-4.md +566 -0
- package/docs/spec.md +2055 -0
- package/e2e/client.ts +24 -0
- package/e2e/helpers.ts +119 -0
- package/e2e/phase-0.test.ts +30 -0
- package/e2e/phase-1.test.ts +48 -0
- package/e2e/phase-2.test.ts +54 -0
- package/e2e/phase-3.test.ts +142 -0
- package/e2e/phase-4.test.ts +96 -0
- package/e2e/runner.ts +145 -0
- package/package.json +14 -12
- package/scripts/gen-session.ts +49 -0
- package/src/bot.test.ts +301 -0
- package/src/bot.ts +91 -0
- package/src/config.test.ts +130 -0
- package/src/config.ts +15 -0
- package/src/event-bus.test.ts +175 -0
- package/src/handlers/allowlist.test.ts +60 -0
- package/src/handlers/allowlist.ts +33 -0
- package/src/handlers/cancel.test.ts +105 -0
- package/src/handlers/permissions.test.ts +72 -0
- package/src/handlers/questions.test.ts +107 -0
- package/src/handlers/sessions.test.ts +479 -0
- package/src/handlers/sessions.ts +202 -0
- package/src/handlers/typing.test.ts +60 -0
- package/src/index.ts +26 -0
- package/src/pending-requests.test.ts +64 -0
- package/src/send/chunker.test.ts +74 -0
- package/src/send/draft-stream.test.ts +229 -0
- package/src/send/format.test.ts +143 -0
- package/src/send/tool-progress.test.ts +70 -0
- package/src/session-manager.test.ts +198 -0
- package/src/session-manager.ts +23 -0
- package/src/turn-manager.test.ts +155 -0
- package/src/turn-manager.ts +5 -0
- package/tsconfig.json +9 -0
package/.env.example
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Required: Telegram bot token from @BotFather
|
|
2
|
+
TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
|
|
3
|
+
|
|
4
|
+
# Optional: OpenCode server URL (default: spawns local server)
|
|
5
|
+
# OPENCODE_URL=http://127.0.0.1:4096
|
|
6
|
+
|
|
7
|
+
# Optional: Project directory for OpenCode (default: cwd)
|
|
8
|
+
# OPENCODE_DIRECTORY=/path/to/project
|
|
9
|
+
|
|
10
|
+
# Optional: Use Telegram test environment
|
|
11
|
+
# TELEGRAM_TEST_ENV=1
|
|
12
|
+
|
|
13
|
+
# E2E test credentials (only needed for automated testing)
|
|
14
|
+
# TELEGRAM_API_ID=12345
|
|
15
|
+
# TELEGRAM_API_HASH=0123456789abcdef0123456789abcdef
|
|
16
|
+
# TELEGRAM_SESSION=saved-session-string
|
|
17
|
+
# TELEGRAM_BOT_USERNAME=my_bot
|
package/bunfig.toml
ADDED
package/docs/PROGRESS.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# OpenCode Telegram Bot — Progress Log
|
|
2
|
+
|
|
3
|
+
## Phase 0 — E2E Infrastructure + Bot Skeleton ✅
|
|
4
|
+
|
|
5
|
+
**Status:** Complete
|
|
6
|
+
**Date:** 2026-02-07
|
|
7
|
+
|
|
8
|
+
### Delivered
|
|
9
|
+
- Project scaffolding inside monorepo (`packages/telegram/`)
|
|
10
|
+
- Grammy bot with `/start` handler
|
|
11
|
+
- Config from env vars with validation
|
|
12
|
+
- Graceful shutdown (SIGINT/SIGTERM)
|
|
13
|
+
- E2E infrastructure: gramjs userbot client, test helpers, runner
|
|
14
|
+
- Session string generation script (`scripts/gen-session.ts`)
|
|
15
|
+
|
|
16
|
+
### Tests
|
|
17
|
+
| Type | Count | Status |
|
|
18
|
+
|------|-------|--------|
|
|
19
|
+
| Unit (bun test src/) | 12 | ✅ all pass |
|
|
20
|
+
| E2E (bun test ./e2e/phase-0.test.ts) | 2 | ✅ all pass |
|
|
21
|
+
| Manual (/start in Telegram) | 1 | ✅ working |
|
|
22
|
+
|
|
23
|
+
### Files Created
|
|
24
|
+
```
|
|
25
|
+
packages/telegram/
|
|
26
|
+
package.json
|
|
27
|
+
tsconfig.json
|
|
28
|
+
bunfig.toml
|
|
29
|
+
.env.example
|
|
30
|
+
.env ← credentials (gitignored)
|
|
31
|
+
.gitignore
|
|
32
|
+
src/
|
|
33
|
+
index.ts ← entry point
|
|
34
|
+
bot.ts ← Grammy bot + /start
|
|
35
|
+
bot.test.ts ← 4 tests
|
|
36
|
+
config.ts ← env parsing
|
|
37
|
+
config.test.ts ← 8 tests
|
|
38
|
+
e2e/
|
|
39
|
+
client.ts ← gramjs wrapper
|
|
40
|
+
helpers.ts ← sendAndWait, assertContains, clickInlineButton, etc.
|
|
41
|
+
runner.ts ← setup/teardown (spawn bot + connect client)
|
|
42
|
+
phase-0.test.ts ← 2 E2E tests
|
|
43
|
+
scripts/
|
|
44
|
+
gen-session.ts ← generate gramjs session string
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Lessons Learned
|
|
48
|
+
- `bunfig.toml`: `preload = []` is invalid, use `root = "./src"` instead
|
|
49
|
+
- Bun auto-loads `.env` — tests for default values need explicit `delete process.env.VAR`
|
|
50
|
+
- E2E runs in ~3s: gramjs connects, spawns bot, sends /start, verifies, tears down
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Phase 1 — Core Loop (MVP) ✅
|
|
55
|
+
|
|
56
|
+
**Status:** Complete
|
|
57
|
+
**Date:** 2026-02-07
|
|
58
|
+
|
|
59
|
+
### Delivered
|
|
60
|
+
- **SDK client factory** (`sdk.ts`): spawns local OpenCode server from monorepo source, or connects to external
|
|
61
|
+
- **SessionManager** (`session-manager.ts`): LRU-bounded map with TTL, reverse lookup (sessionId → chatKey)
|
|
62
|
+
- **TurnManager** (`turn-manager.ts`): per-turn AbortController + timer tracking, clean abort
|
|
63
|
+
- **EventBus** (`event-bus.ts`): single SSE connection, routes events to Telegram chats
|
|
64
|
+
- **Markdown formatter** (`send/format.ts`): Markdown → Telegram HTML conversion
|
|
65
|
+
- **Message chunker** (`send/chunker.ts`): split messages at 4096 char boundary
|
|
66
|
+
- **Message handler**: text → SessionManager → SDK prompt → EventBus → formatted response
|
|
67
|
+
- **`/new` command**: removes mapping, creates fresh session
|
|
68
|
+
- **index.ts wiring**: SDK init → bot + managers → EventBus → graceful shutdown
|
|
69
|
+
|
|
70
|
+
### Tests
|
|
71
|
+
| Type | Count | Status |
|
|
72
|
+
|------|-------|--------|
|
|
73
|
+
| Unit (bun test src/) | 76 | ✅ all pass |
|
|
74
|
+
| E2E Phase 0 (regression) | 2 | ✅ all pass |
|
|
75
|
+
| E2E Phase 1 | 3 | ✅ all pass |
|
|
76
|
+
|
|
77
|
+
### Files Created
|
|
78
|
+
```
|
|
79
|
+
src/
|
|
80
|
+
sdk.ts ← SDK client factory (spawn or connect)
|
|
81
|
+
session-manager.ts ← LRU Map<chatKey, SessionEntry>
|
|
82
|
+
session-manager.test.ts ← 13 tests
|
|
83
|
+
turn-manager.ts ← Per-turn lifecycle (AbortController)
|
|
84
|
+
turn-manager.test.ts ← 12 tests
|
|
85
|
+
event-bus.ts ← Single SSE connection + dispatcher
|
|
86
|
+
event-bus.test.ts ← 5 tests
|
|
87
|
+
send/
|
|
88
|
+
format.ts ← Markdown → Telegram HTML
|
|
89
|
+
format.test.ts ← 22 tests
|
|
90
|
+
chunker.ts ← Split messages at 4096 chars
|
|
91
|
+
chunker.test.ts ← 8 tests
|
|
92
|
+
e2e/
|
|
93
|
+
phase-1.test.ts ← 3 E2E tests
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Files Modified
|
|
97
|
+
```
|
|
98
|
+
src/
|
|
99
|
+
bot.ts ← Added message handler, /new command, BotDeps
|
|
100
|
+
bot.test.ts ← Expanded to 8 tests (handleMessage, handleNew)
|
|
101
|
+
index.ts ← Full wiring: SDK + managers + EventBus + shutdown
|
|
102
|
+
e2e/
|
|
103
|
+
runner.ts ← Spawns server + bot separately, for-await stdout
|
|
104
|
+
helpers.ts ← sendAndWait uses message ID ordering
|
|
105
|
+
phase-0.test.ts ← Increased beforeAll timeout for server startup
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### How to Run
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# From the telegram package directory:
|
|
112
|
+
cd packages/telegram
|
|
113
|
+
|
|
114
|
+
# Set OPENCODE_DIRECTORY to the project you want the AI to work on:
|
|
115
|
+
env $(grep -v '^#' .env | xargs) OPENCODE_DIRECTORY=/home/pedro/dev bun run src/index.ts
|
|
116
|
+
|
|
117
|
+
# Or connect to an existing OpenCode server:
|
|
118
|
+
env $(grep -v '^#' .env | xargs) OPENCODE_URL=http://127.0.0.1:4096 bun run src/index.ts
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**How it works:**
|
|
122
|
+
- Without `OPENCODE_URL`: spawns a local OpenCode server from monorepo source (`packages/opencode`)
|
|
123
|
+
- The server CWD stays at `packages/opencode` (for module resolution)
|
|
124
|
+
- `OPENCODE_DIRECTORY` is sent as `x-opencode-directory` header in every SDK request
|
|
125
|
+
- The OpenCode server uses that header to know which project to operate on
|
|
126
|
+
|
|
127
|
+
### Lessons Learned
|
|
128
|
+
- `Bun.spawn` stdout with `getReader()` + `Promise.race` timeout breaks stream reading — use `for await` instead
|
|
129
|
+
- `process.execPath` resolves bun correctly; ENOENT from spawn usually means CWD doesn't exist
|
|
130
|
+
- E2E runner must spawn OpenCode server separately (not nested inside bot) to avoid subprocess hang
|
|
131
|
+
- `sendAndWait` must use message ID ordering (not timestamps) to avoid picking up stale responses
|
|
132
|
+
- `createOpencode({ port: 0 })` from SDK needs the `opencode` binary — dev mode uses bun source directly
|
|
133
|
+
- Server CWD must be `packages/opencode` (module resolution); project dir via `x-opencode-directory` header
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Phase 2 — Interactive Controls ✅
|
|
138
|
+
|
|
139
|
+
**Status:** Complete
|
|
140
|
+
**Date:** 2026-02-07
|
|
141
|
+
|
|
142
|
+
### Delivered
|
|
143
|
+
- **Permission handling** (`handlers/permissions.ts`): Inline keyboard (Allow / Always / Deny) on `permission.asked` SSE events. Callback routes to `sdk.permission.reply()`.
|
|
144
|
+
- **Question handling** (`handlers/questions.ts`): Inline keyboard with option buttons + Skip on `question.asked` SSE events. Callback routes to `sdk.question.reply()` or `sdk.question.reject()`.
|
|
145
|
+
- **PendingRequests** (`pending-requests.ts`): Bounded Map with TTL for request state tracking — enables index→label resolution for questions and double-click protection for both.
|
|
146
|
+
- **`/cancel` command** (`handlers/cancel.ts`): Aborts active turn via `sdk.session.abort()`, cleans up TurnManager.
|
|
147
|
+
- **Typing indicator** (`handlers/typing.ts`): Sends "typing" chat action every 4s during active turns, auto-stops on turn end via AbortSignal.
|
|
148
|
+
- **Callback query routing** in `bot.ts`: Routes `perm:` and `q:` prefixed callbacks, always calls `answerCallbackQuery()` first, edits original message to show decision.
|
|
149
|
+
- **EventBus wiring** in `index.ts`: Handles `permission.asked` and `question.asked` events, stores in PendingRequests, sends formatted messages with keyboards.
|
|
150
|
+
|
|
151
|
+
### Tests
|
|
152
|
+
| Type | Count | Status |
|
|
153
|
+
|------|-------|--------|
|
|
154
|
+
| Unit (bun test src/) | 108 | ✅ all pass |
|
|
155
|
+
| E2E Phase 0 (regression) | 2 | ✅ all pass |
|
|
156
|
+
| E2E Phase 1 (regression) | 3 | ✅ all pass |
|
|
157
|
+
| E2E Phase 2 | 3 | ✅ all pass |
|
|
158
|
+
|
|
159
|
+
### Files Created
|
|
160
|
+
```
|
|
161
|
+
src/
|
|
162
|
+
pending-requests.ts ← Bounded Map<requestID, PendingEntry> with TTL
|
|
163
|
+
pending-requests.test.ts ← 7 tests
|
|
164
|
+
handlers/
|
|
165
|
+
permissions.ts ← formatPermissionMessage(), parsePermissionCallback()
|
|
166
|
+
permissions.test.ts ← 8 tests
|
|
167
|
+
questions.ts ← formatQuestionMessage(), parseQuestionCallback(), resolveQuestionAnswer()
|
|
168
|
+
questions.test.ts ← 8 tests
|
|
169
|
+
cancel.ts ← handleCancel()
|
|
170
|
+
cancel.test.ts ← 5 tests
|
|
171
|
+
typing.ts ← startTypingLoop()
|
|
172
|
+
typing.test.ts ← 4 tests
|
|
173
|
+
e2e/
|
|
174
|
+
phase-2.test.ts ← 3 E2E tests (regression, /cancel, AI response)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Files Modified
|
|
178
|
+
```
|
|
179
|
+
src/
|
|
180
|
+
bot.ts ← Added /cancel, callback_query handler, BotDeps.pendingRequests,
|
|
181
|
+
handleMessage returns { turn }, typing loop start
|
|
182
|
+
bot.test.ts ← Updated tests for new return type
|
|
183
|
+
index.ts ← PendingRequests instance, permission.asked + question.asked
|
|
184
|
+
event handlers, periodic cleanup
|
|
185
|
+
e2e/
|
|
186
|
+
helpers.ts ← Polling interval 500ms → 1500ms (flood wait mitigation)
|
|
187
|
+
phase-0.test.ts ← Unknown command test adapted (bot now responds via AI)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Key Design Decisions
|
|
191
|
+
- **No sessionID needed for permission/question SDK calls** — only requestID. Simplifies PendingRequests significantly.
|
|
192
|
+
- **Callback data fits 64-byte limit**: `perm:once:per_xxxx` (~40 bytes), `q:que_xxxx:0` (~35 bytes).
|
|
193
|
+
- **Single-select MVP for questions**: `multiple: true` and `custom: true` questions deferred.
|
|
194
|
+
- **E2E permission/cancel-during-generation tests deferred**: Timing-dependent on AI behavior and Grammy's sequential update processing.
|
|
195
|
+
|
|
196
|
+
### Lessons Learned
|
|
197
|
+
- Grammy processes updates sequentially — `/cancel` can't interrupt a handler blocked on `sdk.session.prompt()`
|
|
198
|
+
- `answerCallbackQuery()` MUST be called immediately in callback handlers (prevents Telegram loading spinner)
|
|
199
|
+
- E2E polling at 500ms triggers Telegram flood waits — 1500ms is safe
|
|
200
|
+
- Permission/question button E2E tests are inherently flaky (depend on AI triggering specific tool calls)
|
|
201
|
+
- Bot now processes unknown commands via AI (Phase 1 `message:text` handler catches them), so Phase 0 regression test needed updating
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Phase 3 — Streaming + UX ✅
|
|
206
|
+
|
|
207
|
+
**Status:** Complete
|
|
208
|
+
**Date:** 2026-02-08
|
|
209
|
+
|
|
210
|
+
### Delivered
|
|
211
|
+
- **DraftStream** (`send/draft-stream.ts`): Sends initial message on first text part, then edits it as text streams in. Throttled at 400ms, HTML with plain-text fallback, auto-stop via AbortSignal.
|
|
212
|
+
- **Tool progress** (`send/tool-progress.ts`): Appends `⚙ Running tool: title` suffix to draft during tool execution. Cleared on next text update.
|
|
213
|
+
- **Final response** (`finalizeResponse` in `index.ts`): On `session.idle`, stops draft and either edits to final HTML (single chunk) or deletes draft and sends chunked messages (>4096 chars).
|
|
214
|
+
- **Fire-and-forget prompt** (`bot.ts`): `sdk.session.prompt()` no longer blocks the Grammy handler — the DraftStream is created before the prompt fires, so SSE events can update the draft immediately.
|
|
215
|
+
- **E2E project directory** (`runner.ts`): Bot now uses `/home/pedro/dev/` as OPENCODE_DIRECTORY (configurable via env), pointing to the workspace with Opus configured.
|
|
216
|
+
|
|
217
|
+
### Tests
|
|
218
|
+
| Type | Count | Status |
|
|
219
|
+
|------|-------|--------|
|
|
220
|
+
| Unit (bun test src/) | 137 | ✅ all pass |
|
|
221
|
+
| E2E Phase 0 (regression) | 2 | ✅ all pass |
|
|
222
|
+
| E2E Phase 1 (regression) | 3 | ✅ all pass |
|
|
223
|
+
| E2E Phase 2 (regression) | 3 | ✅ all pass |
|
|
224
|
+
| E2E Phase 3 | 4 | ✅ all pass |
|
|
225
|
+
|
|
226
|
+
### Files Created
|
|
227
|
+
```
|
|
228
|
+
src/
|
|
229
|
+
send/
|
|
230
|
+
draft-stream.ts ← DraftStream class (throttled message editing)
|
|
231
|
+
draft-stream.test.ts ← 18 tests
|
|
232
|
+
tool-progress.ts ← formatToolStatus() pure function
|
|
233
|
+
tool-progress.test.ts ← 8 tests
|
|
234
|
+
e2e/
|
|
235
|
+
phase-3.test.ts ← 4 E2E tests (streaming, tool progress, 2 regression)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Files Modified
|
|
239
|
+
```
|
|
240
|
+
src/
|
|
241
|
+
turn-manager.ts ← Added toolSuffix + draft fields to ActiveTurn
|
|
242
|
+
turn-manager.test.ts ← 3 new tests for new fields
|
|
243
|
+
bot.ts ← DraftStream created before prompt, fire-and-forget prompt,
|
|
244
|
+
draftDeps parameter for testability
|
|
245
|
+
bot.test.ts ← Updated for fire-and-forget prompt (microtick waits)
|
|
246
|
+
index.ts ← DraftStream updates on text/tool events, finalizeResponse
|
|
247
|
+
on session.idle, tool progress integration
|
|
248
|
+
e2e/
|
|
249
|
+
runner.ts ← OPENCODE_DIRECTORY defaults to project root (/home/pedro/dev/)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Key Design Decisions
|
|
253
|
+
- **Fire-and-forget prompt**: `sdk.session.prompt()` was blocking Grammy's sequential handler — with Opus max, this could take minutes. Now it's fire-and-forget with `.catch()`, and the DraftStream is ready before any SSE events arrive.
|
|
254
|
+
- **DraftStream dependency injection**: `DraftStreamDeps` abstracts `bot.api.sendMessage/editMessageText` for testability without Grammy mocks.
|
|
255
|
+
- **Tool progress as suffix**: Tool status is appended to the draft text (not sent as separate messages), keeping the chat clean. Cleared when next text part arrives.
|
|
256
|
+
- **Finalization logic**: Single-chunk → edit draft; multi-chunk → delete draft + send chunked; no draft → send normally.
|
|
257
|
+
|
|
258
|
+
### Lessons Learned
|
|
259
|
+
- `sdk.session.prompt()` blocks until the server responds — with slow models this blocks Grammy's entire update processing. Fire-and-forget is essential.
|
|
260
|
+
- DraftStream must be created BEFORE the prompt call, not after — SSE events arrive immediately and need a target.
|
|
261
|
+
- E2E tests with different model configs (Opus system prompt in Portuguese) may respond differently — regression tests should assert "bot responded" not specific words.
|
|
262
|
+
- Chaining E2E suites with `&&` can cause server port conflicts — run each suite isolated.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Phase 4 — Session Management + Hardening ✅
|
|
267
|
+
|
|
268
|
+
**Status:** Complete
|
|
269
|
+
**Date:** 2026-02-08
|
|
270
|
+
**Plan:** See `docs/plans/phase-4.md`
|
|
271
|
+
|
|
272
|
+
### Delivered
|
|
273
|
+
- **Allowlist middleware (N1)** — `TELEGRAM_ALLOWED_USERS` env var restricts bot access to specific Telegram user IDs. Grammy middleware at top of stack silently ignores non-allowed users. Empty list = accept all.
|
|
274
|
+
- **Streaming interruption fix (N2)** — `sdk.session.abort()` called before starting new turn when existing turn is active. Prevents stale SSE events from corrupting new turns. Generation counter as safety net.
|
|
275
|
+
- **Session restore on restart (N3)** — On startup, `sessionManager.restore(sdk)` pre-populates SessionManager with sessions matching "Telegram {chatId}" title pattern.
|
|
276
|
+
- **Telegram command menu (N4)** — `bot.api.setMyCommands()` registers 9 commands in Telegram's "/" autocomplete menu.
|
|
277
|
+
- **`/list`** — Shows sessions with inline keyboard, click `sess:` callback to switch.
|
|
278
|
+
- **`/rename <title>`** — Renames current session via `sdk.session.update()`.
|
|
279
|
+
- **`/delete`** — Deletes current session via `sdk.session.delete()` + removes SessionManager mapping.
|
|
280
|
+
- **`/info`** — Shows session info (title, directory, created, updated).
|
|
281
|
+
- **`/history`** — Shows last 10 messages (role: truncated text).
|
|
282
|
+
- **`/summarize`** — Returns guidance message (avoids requiring model selection).
|
|
283
|
+
- **`handleSessionCallback`** — Switches session via prefix matching on `session.list()`.
|
|
284
|
+
|
|
285
|
+
### Tests
|
|
286
|
+
| Type | Count | Status |
|
|
287
|
+
|------|-------|--------|
|
|
288
|
+
| Unit (bun test src/) | 184 | ✅ all pass |
|
|
289
|
+
| E2E Phase 0 (regression) | 2 | ✅ all pass |
|
|
290
|
+
| E2E Phase 1 (regression) | 3 | ✅ all pass |
|
|
291
|
+
| E2E Phase 2 (regression) | 3 | ✅ all pass |
|
|
292
|
+
| E2E Phase 3 (regression) | 4 | ✅ all pass |
|
|
293
|
+
| E2E Phase 4 | 5 | ✅ all pass |
|
|
294
|
+
|
|
295
|
+
### Files Created
|
|
296
|
+
```
|
|
297
|
+
src/
|
|
298
|
+
handlers/
|
|
299
|
+
allowlist.ts ← Grammy middleware factory
|
|
300
|
+
allowlist.test.ts ← 6 tests
|
|
301
|
+
sessions.ts ← Session command handlers + formatting
|
|
302
|
+
sessions.test.ts ← 26 tests
|
|
303
|
+
e2e/
|
|
304
|
+
phase-4.test.ts ← 5 E2E tests
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Files Modified
|
|
308
|
+
```
|
|
309
|
+
src/
|
|
310
|
+
config.ts ← Added allowedUsers: number[] field
|
|
311
|
+
config.test.ts ← 3 new tests for allowedUsers parsing
|
|
312
|
+
turn-manager.ts ← Added generation counter to ActiveTurn
|
|
313
|
+
turn-manager.test.ts ← 3 new tests for generation field
|
|
314
|
+
session-manager.ts ← Added restore() method
|
|
315
|
+
session-manager.test.ts ← 5 new tests for restore()
|
|
316
|
+
bot.ts ← Allowlist middleware, 6 new commands, sess: callback,
|
|
317
|
+
sdk.session.abort() before new turn, handleSessionCallback
|
|
318
|
+
bot.test.ts ← 4 new tests (abort behavior, session callback)
|
|
319
|
+
index.ts ← Session restore on startup, setMyCommands registration
|
|
320
|
+
e2e/
|
|
321
|
+
phase-3.test.ts ← Fixed flaky streaming assertion
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Lessons Learned
|
|
325
|
+
- `sdk.session.abort()` is fire-and-forget — errors logged but don't block new turn
|
|
326
|
+
- Phase 3 streaming E2E was flaky: finalizeResponse can shorten text (strips tool suffix), so assert "same message ID + has content" instead of "text grew"
|
|
327
|
+
- `sdk.session.summarize()` requires providerID + modelID — simpler to guide users to ask the AI directly
|
package/docs/mapping.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# OpenCode Telegram Bot - Feature Mapping Report
|
|
2
|
+
|
|
3
|
+
Comparative analysis between the OpenCode SDK (as consumed by the `app` web UI) and the OpenClaw Telegram integration, to plan a comprehensive Telegram bot for OpenCode.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. OpenCode SDK - Complete API Surface
|
|
8
|
+
|
|
9
|
+
The SDK (`@opencode-ai/sdk`) exposes the `OpencodeClient` class with these namespaces:
|
|
10
|
+
|
|
11
|
+
| Namespace | Key Methods | Used by App |
|
|
12
|
+
|-----------|------------|-------------|
|
|
13
|
+
| `global` | `get`, `update`, `health`, `event` (SSE stream), `dispose` | Yes - bootstrap, config, SSE |
|
|
14
|
+
| `auth` | `remove`, `set` | Yes - provider auth |
|
|
15
|
+
| `project` | `list`, `current`, `update` | Yes - multi-project |
|
|
16
|
+
| `pty` | `list`, `create`, `remove`, `get`, `update`, `connect` | No |
|
|
17
|
+
| `config` | `get`, `update`, `providers` | Yes - settings |
|
|
18
|
+
| `tool` | `ids`, `list` | No |
|
|
19
|
+
| `worktree` | `list`, `remove`, `create`, `reset` | Yes - git worktrees |
|
|
20
|
+
| `experimental` | `resource.list` | No |
|
|
21
|
+
| `session` | `list`, `create`, `status`, `delete`, `get`, `update`, `children`, `todo`, `init`, `fork`, `abort`, `share`, `unshare`, `diff`, `summarize`, `messages`, `prompt`, `message`, `promptAsync`, `command`, `shell`, `revert`, `unrevert` | Yes - all core |
|
|
22
|
+
| `part` | `delete`, `update` | No |
|
|
23
|
+
| `permission` | `respond`, `reply`, `list` | Yes - permission dialogs |
|
|
24
|
+
| `question` | `list`, `reply`, `reject` | Yes - question dialogs |
|
|
25
|
+
| `provider` | `list`, `auth` | Yes - provider mgmt |
|
|
26
|
+
| `find` | `text`, `files`, `symbols` | No |
|
|
27
|
+
| `file` | `list`, `read`, `status` | Yes - file tree |
|
|
28
|
+
| `mcp` | `remove`, `start`, `callback`, `authenticate`, `status`, `add`, `connect`, `disconnect` | No |
|
|
29
|
+
| `tui` | `next`, `response`, `appendPrompt`, `openHelp/Sessions/Themes/Models`, `submitPrompt`, `clearPrompt`, `executeCommand`, `showToast`, `publish`, `selectSession` | No (TUI-specific) |
|
|
30
|
+
| `instance` | `dispose` | No |
|
|
31
|
+
| `path` | `get` | Yes - directory paths |
|
|
32
|
+
| `vcs` | `get` | Yes - git status |
|
|
33
|
+
| `command` | `list` | Yes - slash commands |
|
|
34
|
+
| `app` | `log`, `agents`, `skills` | Yes - agent list |
|
|
35
|
+
| `lsp` | `status` | Yes - LSP indicators |
|
|
36
|
+
| `formatter` | `status` | No |
|
|
37
|
+
| `event` | `subscribe` (per-instance SSE) | Yes - realtime updates |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 2. OpenCode App (Web UI) - SDK Usage Breakdown
|
|
42
|
+
|
|
43
|
+
### 2.1 Session Lifecycle
|
|
44
|
+
- `session.create()` - new conversation
|
|
45
|
+
- `session.list()` - sidebar session list
|
|
46
|
+
- `session.get()` - load specific session
|
|
47
|
+
- `session.update()` - rename, archive
|
|
48
|
+
- `session.delete()` - delete session
|
|
49
|
+
- `session.fork()` - fork from message
|
|
50
|
+
- `session.share()` / `unshare()` - sharing URLs
|
|
51
|
+
- `session.abort()` - cancel running response
|
|
52
|
+
- `session.diff()` - view file changes
|
|
53
|
+
- `session.todo()` - task list
|
|
54
|
+
- `session.children()` - sub-sessions
|
|
55
|
+
|
|
56
|
+
### 2.2 Messaging
|
|
57
|
+
- `session.prompt()` - send user message (text + files + images + agent + model + variant)
|
|
58
|
+
- `session.shell()` - execute shell command
|
|
59
|
+
- `session.command()` - execute slash command (with agent, model, variant, parts)
|
|
60
|
+
- `session.messages()` - load message history (paginated)
|
|
61
|
+
- `session.message()` - get single message
|
|
62
|
+
- `session.summarize()` - summarize session
|
|
63
|
+
- `session.revert()` / `unrevert()` - undo/redo changes
|
|
64
|
+
|
|
65
|
+
### 2.3 Events (SSE)
|
|
66
|
+
- `global.event()` - global SSE stream, events by directory
|
|
67
|
+
- Event types handled:
|
|
68
|
+
- `message.updated` - new/updated messages
|
|
69
|
+
- `message.part.updated` - tool calls, text streaming
|
|
70
|
+
- `session.updated` - session metadata changes
|
|
71
|
+
- `session.status` - busy/idle state
|
|
72
|
+
- `session.idle` - turn complete (triggers notifications)
|
|
73
|
+
- `session.error` - error events
|
|
74
|
+
- `permission.asked` - permission request
|
|
75
|
+
- `lsp.updated` - LSP status change
|
|
76
|
+
|
|
77
|
+
### 2.4 Permissions
|
|
78
|
+
- `permission.list()` - pending permissions for directory
|
|
79
|
+
- `permission.respond()` - once/always/reject
|
|
80
|
+
|
|
81
|
+
### 2.5 Questions
|
|
82
|
+
- `question.list()` - pending questions
|
|
83
|
+
- `question.reply()` - answer question
|
|
84
|
+
- `question.reject()` - reject question
|
|
85
|
+
|
|
86
|
+
### 2.6 Config & Providers
|
|
87
|
+
- `global.get()` → config, providers, provider_auth, path, project list
|
|
88
|
+
- `global.update()` → update global config
|
|
89
|
+
- `config.get()` / `config.update()` - per-project config
|
|
90
|
+
- `provider.list()` / `provider.auth()` - provider management
|
|
91
|
+
- `auth.set()` / `auth.remove()` - API key management
|
|
92
|
+
|
|
93
|
+
### 2.7 Files & VCS
|
|
94
|
+
- `file.list()` - directory file tree
|
|
95
|
+
- `file.read()` - read file content
|
|
96
|
+
- `file.status()` - git status
|
|
97
|
+
- `vcs.get()` - VCS info
|
|
98
|
+
- `worktree.create()` / `list()` / `remove()` / `reset()` - git worktree management
|
|
99
|
+
|
|
100
|
+
### 2.8 Other
|
|
101
|
+
- `app.agents()` - list available agents
|
|
102
|
+
- `app.skills()` - list skills
|
|
103
|
+
- `command.list()` - list slash commands
|
|
104
|
+
- `lsp.status()` - LSP server status
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 3. OpenClaw Telegram - Feature Breakdown
|
|
109
|
+
|
|
110
|
+
~6,500 LOC (excluding tests). Built on Grammy (Telegram Bot Framework).
|
|
111
|
+
|
|
112
|
+
### 3.1 Core Architecture
|
|
113
|
+
- **bot.ts** (494 LOC) - Bot creation, Grammy setup, sequentialization, throttling
|
|
114
|
+
- **bot-handlers.ts** (928 LOC) - Message/callback routing, media groups, text fragment assembly, debouncing
|
|
115
|
+
- **bot-message.ts** (93 LOC) - Message processor factory
|
|
116
|
+
- **bot-message-context.ts** (700 LOC) - Rich context building (sender info, group config, history, media, permissions)
|
|
117
|
+
- **bot-message-dispatch.ts** (357 LOC) - Dispatches to AI agent, handles reply delivery
|
|
118
|
+
|
|
119
|
+
### 3.2 Message Handling
|
|
120
|
+
- Text messages (DM + group)
|
|
121
|
+
- Media: photos, videos, documents, audio, voice, stickers
|
|
122
|
+
- Media groups (multiple photos/videos in one message)
|
|
123
|
+
- Text fragment assembly (long messages split across multiple Telegram messages)
|
|
124
|
+
- Inbound debouncing (batches rapid messages)
|
|
125
|
+
- Forward/reply context extraction
|
|
126
|
+
- Sticker image analysis via vision model
|
|
127
|
+
- Inline button callbacks (callback_query)
|
|
128
|
+
|
|
129
|
+
### 3.3 Sending / Response Delivery
|
|
130
|
+
- **send.ts** (754 LOC) - Full send pipeline:
|
|
131
|
+
- Markdown → Telegram HTML conversion
|
|
132
|
+
- Message chunking (4096 char limit)
|
|
133
|
+
- Media attachments (photo, video, audio, document, voice, GIF)
|
|
134
|
+
- Caption splitting for media
|
|
135
|
+
- Inline keyboard buttons
|
|
136
|
+
- Reply threading (reply_to_message_id)
|
|
137
|
+
- Forum topic support (message_thread_id)
|
|
138
|
+
- Silent messages (disable_notification)
|
|
139
|
+
- Voice messages (via ElevenLabs TTS)
|
|
140
|
+
- Reactions (emoji reactions on messages)
|
|
141
|
+
- Proxy support (SOCKS5/HTTP)
|
|
142
|
+
- Retry with exponential backoff
|
|
143
|
+
|
|
144
|
+
### 3.4 Draft Streaming
|
|
145
|
+
- **draft-stream.ts** (139 LOC) - Live streaming of AI response as editable message draft
|
|
146
|
+
- Throttled updates (300ms)
|
|
147
|
+
- Max 4096 chars per draft
|
|
148
|
+
- Falls back gracefully on failure
|
|
149
|
+
|
|
150
|
+
### 3.5 Native Commands (/command)
|
|
151
|
+
- **bot-native-commands.ts** (699 LOC) - Telegram /commands:
|
|
152
|
+
- `/status` - bot status
|
|
153
|
+
- `/models` - model selection with inline keyboard pagination
|
|
154
|
+
- `/help` - command list
|
|
155
|
+
- `/commands` - list available commands
|
|
156
|
+
- Custom commands from config
|
|
157
|
+
- Skill-based commands
|
|
158
|
+
- Plugin commands
|
|
159
|
+
- Per-group/topic command gating
|
|
160
|
+
|
|
161
|
+
### 3.6 Model Selection UI
|
|
162
|
+
- **model-buttons.ts** (217 LOC) - Inline keyboard for model selection:
|
|
163
|
+
- Provider grouping
|
|
164
|
+
- Paginated model list
|
|
165
|
+
- Per-session model override storage
|
|
166
|
+
|
|
167
|
+
### 3.7 Group Chat Support
|
|
168
|
+
- Group policy (allow/deny/mentionOnly)
|
|
169
|
+
- Per-group configuration
|
|
170
|
+
- Forum/topic support (supergroups with topics)
|
|
171
|
+
- Topic-specific skill filters and system prompts
|
|
172
|
+
- Mention detection (@botname)
|
|
173
|
+
- Sender allowlists
|
|
174
|
+
- Group migration handling (when group ID changes)
|
|
175
|
+
- Group history tracking
|
|
176
|
+
|
|
177
|
+
### 3.8 Multi-Account
|
|
178
|
+
- **accounts.ts** (139 LOC) - Multiple Telegram bot accounts
|
|
179
|
+
- Account binding per DM chat
|
|
180
|
+
- Account-specific config
|
|
181
|
+
|
|
182
|
+
### 3.9 Security & Access Control
|
|
183
|
+
- **bot-access.ts** (94 LOC) - Allowlist checking
|
|
184
|
+
- **audit.ts** (162 LOC) - Audit logging for messages
|
|
185
|
+
- Sender verification (phone number, username, Telegram ID)
|
|
186
|
+
- Per-group allowlists
|
|
187
|
+
|
|
188
|
+
### 3.10 Infrastructure
|
|
189
|
+
- **webhook.ts** (127 LOC) - Webhook mode (alternative to polling)
|
|
190
|
+
- **monitor.ts** (215 LOC) - Health monitoring, connection status
|
|
191
|
+
- **network-errors.ts** (150 LOC) - Recoverable error detection
|
|
192
|
+
- **proxy.ts** - SOCKS5/HTTP proxy support
|
|
193
|
+
- **token.ts** (102 LOC) - Token validation
|
|
194
|
+
- **format.ts** (98 LOC) - Markdown → Telegram HTML
|
|
195
|
+
- **download.ts** - Media file downloads
|
|
196
|
+
- **sent-message-cache.ts** - Deduplication of sent messages
|
|
197
|
+
- **update-offset-store.ts** - Persistent update offset tracking
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 4. Feature Mapping: OpenClaw Telegram → OpenCode SDK
|
|
202
|
+
|
|
203
|
+
| OpenClaw Telegram Feature | OpenCode SDK Equivalent | Implementation Notes |
|
|
204
|
+
|--------------------------|------------------------|---------------------|
|
|
205
|
+
| **Session/Thread Management** | `session.create/get/list/delete` | Map Telegram chat/thread → OpenCode session |
|
|
206
|
+
| **Send Message to AI** | `session.prompt()` | Main interaction point |
|
|
207
|
+
| **Shell Commands** | `session.shell()` | Could support `!command` syntax |
|
|
208
|
+
| **Slash Commands** | `session.command()` | Map Telegram /commands → OpenCode commands |
|
|
209
|
+
| **Abort Response** | `session.abort()` | /cancel command or inline button |
|
|
210
|
+
| **Stream Response** | `global.event()` SSE | Listen for `message.part.updated` events |
|
|
211
|
+
| **Tool Call Updates** | `message.part.updated` events | Show tool progress in chat |
|
|
212
|
+
| **Permission Requests** | `permission.list/respond` | Inline buttons: Allow/Deny/Always |
|
|
213
|
+
| **Question Dialogs** | `question.list/reply/reject` | Inline buttons or reply-based |
|
|
214
|
+
| **Model Selection** | `app.agents()` + config | Inline keyboard like OpenClaw |
|
|
215
|
+
| **Agent Selection** | `app.agents()` | /agent command with inline keyboard |
|
|
216
|
+
| **Session Rename** | `session.update()` | /rename command |
|
|
217
|
+
| **Session Fork** | `session.fork()` | /fork command |
|
|
218
|
+
| **Session Share** | `session.share()` | /share → returns URL |
|
|
219
|
+
| **View Diff** | `session.diff()` | /diff → formatted file changes |
|
|
220
|
+
| **View Todo** | `session.todo()` | /todo → task list |
|
|
221
|
+
| **Revert Changes** | `session.revert/unrevert` | /undo /redo commands |
|
|
222
|
+
| **File Attachment** | `session.prompt()` with FileParts | Photo/document → file part |
|
|
223
|
+
| **Media Download** | Direct from Telegram API | Download → base64 → file part |
|
|
224
|
+
| **Voice Messages** | Download → transcription/attach | Could use whisper or send as audio |
|
|
225
|
+
| **Message History** | `session.messages()` | /history or context loading |
|
|
226
|
+
| **Multi-Project** | `project.list()` + directory header | /project command to switch |
|
|
227
|
+
| **Config** | `config.get/update` | /config command |
|
|
228
|
+
| **Provider Auth** | `auth.set/remove` | /auth command |
|
|
229
|
+
| **Notifications** | `session.idle/error` events | Proactive messages on completion |
|
|
230
|
+
| **Draft Streaming** | `message.part.updated` + edit_message | Edit message as response streams in |
|
|
231
|
+
| **Markdown Formatting** | n/a (SDK returns markdown) | Convert markdown → Telegram HTML |
|
|
232
|
+
| **Message Chunking** | n/a | Split >4096 char messages |
|
|
233
|
+
| **Inline Buttons** | n/a | Permissions, questions, model selection |
|
|
234
|
+
| **Reactions** | n/a | Ack reaction on message receipt |
|
|
235
|
+
| **Group Support** | Works with directory header | Group chat → shared project session |
|
|
236
|
+
| **Forum Topics** | Works with directory header | Topic → separate session |
|
|
237
|
+
| **Error Handling** | `session.error` events | Show errors in chat |
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 5. Proposed Architecture
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
opencode-telegram/
|
|
245
|
+
src/
|
|
246
|
+
index.ts # Entry point, bot startup
|
|
247
|
+
bot.ts # Grammy bot creation, middleware
|
|
248
|
+
session-map.ts # Telegram chat/thread → OpenCode session mapping
|
|
249
|
+
handlers/
|
|
250
|
+
message.ts # Text + media message handling
|
|
251
|
+
command.ts # /start, /new, /model, /agent, /cancel, etc.
|
|
252
|
+
callback.ts # Inline button callbacks
|
|
253
|
+
media.ts # Photo, document, voice, sticker handling
|
|
254
|
+
events/
|
|
255
|
+
listener.ts # SSE event listener (global.event)
|
|
256
|
+
dispatcher.ts # Route events → Telegram responses
|
|
257
|
+
send/
|
|
258
|
+
format.ts # Markdown → Telegram HTML
|
|
259
|
+
chunker.ts # Message splitting (4096 limit)
|
|
260
|
+
media.ts # Send photos, files, voice
|
|
261
|
+
draft-stream.ts # Live response streaming via edit_message
|
|
262
|
+
ui/
|
|
263
|
+
permissions.ts # Inline keyboard for permissions
|
|
264
|
+
questions.ts # Inline keyboard for questions
|
|
265
|
+
models.ts # Model selection inline keyboard
|
|
266
|
+
agents.ts # Agent selection inline keyboard
|
|
267
|
+
config.ts # Bot configuration
|
|
268
|
+
types.ts # Shared types
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 5.1 Priority Features (MVP)
|
|
272
|
+
|
|
273
|
+
1. **Session Management** - chat → session mapping, /new, /list
|
|
274
|
+
2. **Prompting** - text messages → `session.prompt()`
|
|
275
|
+
3. **SSE Events** - stream responses back to Telegram
|
|
276
|
+
4. **Response Delivery** - markdown formatting, chunking, streaming edits
|
|
277
|
+
5. **Permission Handling** - inline buttons for allow/deny
|
|
278
|
+
6. **Question Handling** - inline buttons for question replies
|
|
279
|
+
7. **Abort** - /cancel to stop generation
|
|
280
|
+
8. **Model/Agent Selection** - /model, /agent commands with inline keyboards
|
|
281
|
+
9. **File Attachments** - photos/documents → file parts
|
|
282
|
+
10. **Error Handling** - show errors, retry logic
|
|
283
|
+
|
|
284
|
+
### 5.2 Advanced Features (Post-MVP)
|
|
285
|
+
|
|
286
|
+
1. **Draft Streaming** - edit message as response arrives
|
|
287
|
+
2. **Group Chat Support** - mention detection, allowlists
|
|
288
|
+
3. **Forum Topics** - topic → session mapping
|
|
289
|
+
4. **Voice Messages** - download + attach as audio
|
|
290
|
+
5. **Slash Commands** - /commit, /review, etc. → `session.command()`
|
|
291
|
+
6. **Shell Mode** - `!ls` → `session.shell()`
|
|
292
|
+
7. **Session Sharing** - /share → public URL
|
|
293
|
+
8. **Diff View** - /diff → formatted file changes
|
|
294
|
+
9. **Multi-Project** - /project to switch directories
|
|
295
|
+
10. **Notifications** - proactive messages on turn completion
|
|
296
|
+
11. **Reactions** - ack reaction on message receipt, reactions on completion
|
|
297
|
+
|
|
298
|
+
### 5.3 Key Differences from OpenClaw
|
|
299
|
+
|
|
300
|
+
| Aspect | OpenClaw Telegram | OpenCode Telegram (Proposed) |
|
|
301
|
+
|--------|------------------|------------------------------|
|
|
302
|
+
| Backend | Direct AI agent integration | SDK HTTP client (like web UI) |
|
|
303
|
+
| Session Store | Custom session store | OpenCode manages sessions |
|
|
304
|
+
| Model Catalog | Internal model catalog | `app.agents()` + provider list |
|
|
305
|
+
| Commands | Custom command registry | `command.list()` from OpenCode |
|
|
306
|
+
| Streaming | Token-by-token streaming | SSE `message.part.updated` events |
|
|
307
|
+
| Config | OpenClaw config system | `config.get/update` from SDK |
|
|
308
|
+
| Permissions | Not applicable | Full permission system via SDK |
|
|
309
|
+
| Questions | Not applicable | Full question system via SDK |
|
|
310
|
+
| Multi-Project | Not applicable | Directory header in SDK client |
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## 6. SDK Methods NOT Used by Web App (Available for Telegram)
|
|
315
|
+
|
|
316
|
+
These SDK features are available but the web app doesn't use them:
|
|
317
|
+
|
|
318
|
+
- `pty.*` - Terminal sessions (could power interactive shell in Telegram)
|
|
319
|
+
- `tool.ids/list` - Enumerate available tools
|
|
320
|
+
- `find.text/files/symbols` - Code search
|
|
321
|
+
- `mcp.*` - MCP server management
|
|
322
|
+
- `session.promptAsync()` - Non-blocking prompt (useful for Telegram's async nature)
|
|
323
|
+
- `part.delete/update` - Edit/delete message parts
|
|
324
|
+
- `instance.dispose` - Cleanup
|
|
325
|
+
|
|
326
|
+
Of particular interest: **`session.promptAsync()`** could be ideal for Telegram since it doesn't block, allowing the bot to handle other messages while waiting for AI responses.
|