@pedrohnas/opencode-telegram 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-41-55-194Z.yml +36 -0
  2. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-42-17-115Z.yml +36 -0
  3. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-43-15-988Z.yml +26 -0
  4. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-43-26-107Z.yml +26 -0
  5. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-03-139Z.yml +29 -0
  6. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-21-579Z.yml +29 -0
  7. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-48-051Z.yml +30 -0
  8. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-46-27-632Z.yml +33 -0
  9. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-46-46-519Z.yml +33 -0
  10. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-28-491Z.yml +349 -0
  11. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-34-834Z.yml +349 -0
  12. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-54-066Z.yml +168 -0
  13. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-48-19-667Z.yml +219 -0
  14. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-49-32-311Z.yml +221 -0
  15. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-49-57-109Z.yml +230 -0
  16. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-50-24-052Z.yml +235 -0
  17. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-50-41-148Z.yml +248 -0
  18. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-51-10-916Z.yml +234 -0
  19. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-51-28-271Z.yml +234 -0
  20. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-52-32-324Z.yml +234 -0
  21. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-52-47-801Z.yml +196 -0
  22. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-56-07-361Z.yml +203 -0
  23. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-56-35-534Z.yml +49 -0
  24. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-57-04-658Z.yml +52 -0
  25. package/docs/AUDIT.md +193 -0
  26. package/docs/PROGRESS.md +188 -0
  27. package/docs/plans/phase-5.md +410 -0
  28. package/docs/plans/phase-6.5.md +426 -0
  29. package/docs/plans/phase-6.md +349 -0
  30. package/e2e/helpers.ts +34 -0
  31. package/e2e/phase-5.test.ts +295 -0
  32. package/e2e/phase-6.5.test.ts +239 -0
  33. package/e2e/phase-6.test.ts +302 -0
  34. package/package.json +6 -3
  35. package/src/api-server.test.ts +309 -0
  36. package/src/api-server.ts +201 -0
  37. package/src/bot.test.ts +354 -0
  38. package/src/bot.ts +200 -2
  39. package/src/config.test.ts +16 -0
  40. package/src/config.ts +4 -0
  41. package/src/event-bus.test.ts +337 -1
  42. package/src/event-bus.ts +83 -3
  43. package/src/handlers/agents.test.ts +122 -0
  44. package/src/handlers/agents.ts +93 -0
  45. package/src/handlers/media.test.ts +264 -0
  46. package/src/handlers/media.ts +168 -0
  47. package/src/handlers/models.test.ts +319 -0
  48. package/src/handlers/models.ts +191 -0
  49. package/src/index.ts +15 -0
  50. package/src/send/draft-stream.test.ts +76 -0
  51. package/src/send/draft-stream.ts +13 -1
  52. package/src/session-manager.test.ts +46 -0
  53. package/src/session-manager.ts +10 -1
@@ -0,0 +1,426 @@
1
+ # Phase 6.5 — Production Hardening + Bot Control API
2
+
3
+ **Goal:** Fix 3 critical production gaps (AUDIT.md), improve /model UX, and add
4
+ a Bot Control API so the AI can manage its own model/agent/session via skills.
5
+
6
+ ## What This Phase Delivers
7
+
8
+ 1. **EventBus auto-reconnect (G1/G5)** — SSE stream break → auto-reconnect
9
+ with exponential backoff. Without this, the bot silently goes deaf.
10
+
11
+ 2. **Grammy `apiThrottler()` middleware (G2)** — Automatic Telegram API 429
12
+ rate limit handling.
13
+
14
+ 3. **Grammy `sequentialize()` middleware (G3)** — Per-chat sequential update
15
+ processing. Prevents race conditions with webhooks/runner.
16
+
17
+ 4. **Smart /model filtering (G12)** — Only connected providers, most recent
18
+ model per family, active model marked with ✓. Reduces 86 providers / ~400
19
+ models → 4 providers / 16 models.
20
+
21
+ 5. **Bot Control API** — Hono HTTP server (localhost:4097) that exposes bot
22
+ state to the AI running in OpenCode. Enables AI-driven model switching,
23
+ agent selection, and session management via skills.
24
+
25
+ 6. **SKILL.md** — Teaches the AI how to use the Bot Control API to change
26
+ models, agents, and sessions autonomously.
27
+
28
+ ---
29
+
30
+ ## Gap Details
31
+
32
+ ### G1/G5 — EventBus auto-reconnect (CRITICAL)
33
+
34
+ **Current behavior:**
35
+ ```ts
36
+ // event-bus.ts — stream ends → NOTHING HAPPENS. Bot goes deaf.
37
+ for await (const event of result.stream) { ... }
38
+ ```
39
+
40
+ **Fix:** Reconnect loop with exponential backoff (2s→30s, 1.8x, ±25% jitter).
41
+
42
+ ### G2 — apiThrottler() middleware
43
+
44
+ **Fix:** `bot.api.config.use(apiThrottler())` — Grammy queues + retries on 429.
45
+
46
+ **Dependency:** `@grammyjs/transformer-throttler`
47
+
48
+ ### G3 — sequentialize() middleware
49
+
50
+ **Fix:** `bot.use(sequentialize((ctx) => String(ctx.chat?.id ?? "")))` — first
51
+ middleware, before allowlist.
52
+
53
+ **Dependency:** `@grammyjs/runner`
54
+
55
+ ### G12 — Smart /model filtering
56
+
57
+ **Fix — 3 filters:**
58
+ 1. Only show providers in `connected[]` (have API keys)
59
+ 2. Group by `family`, keep most recent `release_date` per family
60
+ 3. Mark active model with `✓` (from override or server default)
61
+
62
+ ### Bot Control API
63
+
64
+ **Problem:** The AI runs in the OpenCode server but can't control the Telegram
65
+ bot's state (model override, agent override, session management). These live in
66
+ the bot's in-memory SessionManager — a separate process.
67
+
68
+ **Solution:** Hono HTTP server on `127.0.0.1:TELEGRAM_API_PORT` (default 4097).
69
+ The AI calls it via `curl` from bash. Follows the OpenCode server pattern
70
+ (`new Hono()` → `Bun.serve({ fetch: app.fetch })`).
71
+
72
+ **How the AI identifies itself:**
73
+ 1. AI calls `curl localhost:4096/session` (OpenCode API)
74
+ 2. Finds session with title `"Telegram {chatId}"`
75
+ 3. Uses `sessionId` in Bot Control API calls
76
+ 4. Bot does reverse lookup via `sessionManager.getBySessionId()` → chatKey
77
+
78
+ **Endpoints:**
79
+
80
+ | Method | Path | Body | Returns |
81
+ |--------|------|------|---------|
82
+ | `GET` | `/api/sessions` | — | All active sessions (chatId, sessionId, model, agent) |
83
+ | `GET` | `/api/session/:sessionId` | — | Session info (model, agent, turn status) |
84
+ | `POST` | `/api/session/:sessionId/model` | `{ providerID, modelID }` | Updated session |
85
+ | `POST` | `/api/session/:sessionId/agent` | `{ agent }` | Updated session |
86
+ | `POST` | `/api/session/:sessionId/new` | — | New session created |
87
+ | `GET` | `/api/models` | — | Connected providers + filtered models (JSON) |
88
+ | `GET` | `/api/agents` | — | Available agents (JSON) |
89
+
90
+ **Security:** Binds to `127.0.0.1` only — accessible from same VPS, no auth.
91
+
92
+ ---
93
+
94
+ ## Architecture
95
+
96
+ ### EventBus reconnect loop
97
+
98
+ ```
99
+ start()
100
+ └→ reconnectLoop()
101
+ └→ listen()
102
+ └→ sdk.event.subscribe() → for await (stream)
103
+ ├→ event → onEvent(...)
104
+ └→ stream ends
105
+ └→ if stopped: return
106
+ └→ wait(backoff with jitter)
107
+ └→ reconnectLoop() (retry)
108
+ ```
109
+
110
+ **Backoff:** `delay = min(maxDelay, initialDelay * factor^attempt) * (1 + jitter * random(-1, 1))`
111
+
112
+ ### Grammy middleware stack (updated)
113
+
114
+ ```
115
+ 1. apiThrottler() ← NEW (API transformer)
116
+ 2. sequentialize(chatId) ← NEW (before all handlers)
117
+ 3. allowlistMiddleware
118
+ 4. bot.command("start", ...)
119
+ 5. ... (all other handlers unchanged)
120
+ ```
121
+
122
+ ### Bot Control API (Hono pattern)
123
+
124
+ ```
125
+ index.ts
126
+ ├→ createBot(config, deps) ← Grammy bot (existing)
127
+ ├→ createApiServer(deps, port) ← Hono API (NEW)
128
+ └→ eventBus.start() ← SSE listener (existing)
129
+
130
+ createApiServer(deps)
131
+ └→ new Hono()
132
+ .get("/api/sessions", ...)
133
+ .get("/api/session/:sessionId", ...)
134
+ .post("/api/session/:sessionId/model", ...)
135
+ .post("/api/session/:sessionId/agent", ...)
136
+ .post("/api/session/:sessionId/new", ...)
137
+ .get("/api/models", ...)
138
+ .get("/api/agents", ...)
139
+ └→ Bun.serve({ hostname: "127.0.0.1", port, fetch: app.fetch })
140
+ ```
141
+
142
+ Dependencies shared: `sessionManager`, `turnManager`, `sdk`.
143
+
144
+ ---
145
+
146
+ ## New Files
147
+
148
+ ```
149
+ src/
150
+ api-server.ts ← Hono app + Bun.serve (Bot Control API)
151
+ api-server.test.ts ← Tests for all endpoints
152
+
153
+ deploy/
154
+ skills/
155
+ telegram-control/
156
+ SKILL.md ← AI skill doc (copy to .opencode/skills/ on deploy)
157
+ ```
158
+
159
+ ## Modified Files
160
+
161
+ ```
162
+ src/
163
+ event-bus.ts ← Add reconnect loop with exponential backoff
164
+ event-bus.test.ts ← Tests for reconnect behavior
165
+ bot.ts ← Add sequentialize() + pass connected/default to model handlers
166
+ bot.test.ts ← Test middleware ordering
167
+ config.ts ← Add apiPort config (TELEGRAM_API_PORT)
168
+ config.test.ts ← Test apiPort parsing
169
+ index.ts ← Add apiThrottler() + start API server + shutdown
170
+ handlers/models.ts ← Add filterModels(), update format* signatures
171
+ handlers/models.test.ts ← Tests for filtering, connected, ✓ indicator
172
+ package.json ← Add hono, @grammyjs/transformer-throttler, @grammyjs/runner
173
+ ```
174
+
175
+ ---
176
+
177
+ ## TDD Execution Order
178
+
179
+ ### A1. EventBus reconnect (8-10 tests)
180
+
181
+ **File:** `src/event-bus.ts` + `src/event-bus.test.ts`
182
+
183
+ Changes to EventBus:
184
+ - Add `reconnectLoop()` method that wraps `listen()` with retry logic
185
+ - Add backoff config: `initialDelayMs`, `maxDelayMs`, `backoffFactor`, `jitter`
186
+ - `start()` calls `reconnectLoop()` instead of `listen()` directly
187
+ - `stop()` sets `stopped = true` (already exists) — breaks reconnect loop
188
+ - Log reconnection attempts with attempt number and delay
189
+
190
+ **Tests:**
191
+
192
+ *Reconnect behavior:*
193
+ 1. Reconnects after stream ends (mock stream that yields 2 events then closes)
194
+ 2. Does NOT reconnect after stop() is called
195
+ 3. Backoff delay increases on consecutive failures
196
+ 4. Backoff delay resets to initial after successful connection
197
+ 5. Jitter adds randomness to delay (delay varies between calls)
198
+ 6. Max delay is capped at maxDelayMs
199
+
200
+ *Error handling:*
201
+ 7. Reconnects after subscribe() throws an error
202
+ 8. Reconnects after stream throws mid-iteration
203
+ 9. Continues processing events after reconnecting (events from new stream arrive)
204
+
205
+ *Integration:*
206
+ 10. stop() during reconnect delay cancels the reconnect
207
+
208
+ ### B2. Grammy middleware — sequentialize + throttler (3-4 tests)
209
+
210
+ **File:** `src/bot.ts` + `src/bot.test.ts` + `src/index.ts`
211
+
212
+ Changes:
213
+ - `createBot()`: add `sequentialize()` as first middleware (before allowlist)
214
+ - `index.ts`: add `apiThrottler()` to `bot.api.config`
215
+
216
+ **Tests:**
217
+ 1. Bot has sequentialize middleware (verify middleware count or behavior)
218
+ 2. apiThrottler is configured on bot API (verify transformer count)
219
+ 3. sequentialize key uses chat ID
220
+
221
+ ### C3. Smart /model filtering (8-10 tests)
222
+
223
+ **File:** `src/handlers/models.ts` + `src/handlers/models.test.ts` + `src/bot.ts`
224
+
225
+ New function:
226
+ - `filterModels(models)` — groups by `family`, picks most recent `release_date`
227
+ per family, excludes `status: "deprecated"`
228
+
229
+ Changes to existing functions:
230
+ - `formatProviderList(providers, connected?)` — filter by connected, show count
231
+ - `formatModelList(providerID, providerName, models, activeModelID?)` — ✓ marker
232
+ - `handleModel(params)` — pass `connected` and `default` from SDK response
233
+ - `bot.ts` callback — apply `filterModels`, pass `activeModelID`
234
+
235
+ **Tests:**
236
+
237
+ *filterModels:*
238
+ 1. Groups by family, returns most recent per family
239
+ 2. Excludes deprecated models
240
+ 3. Keeps models without family field (uses id as key)
241
+ 4. Handles single model per family (no-op)
242
+
243
+ *formatProviderList:*
244
+ 5. Filters to only connected providers when connected array provided
245
+ 6. Shows model count in button text
246
+ 7. Shows all providers when connected not provided (backward compat)
247
+
248
+ *formatModelList:*
249
+ 8. Marks active model with ✓ prefix
250
+ 9. No ✓ when activeModelID not provided
251
+
252
+ *handleModel:*
253
+ 10. Passes connected and default from SDK response
254
+
255
+ ### D4. Bot Control API server (10-12 tests)
256
+
257
+ **File:** `src/api-server.ts` + `src/api-server.test.ts` + `src/config.ts`
258
+
259
+ New files:
260
+ - `api-server.ts` — Hono app with all endpoints + `createApiServer()` function
261
+ - `api-server.test.ts` — tests against the Hono app (no real Bun.serve needed,
262
+ test via `app.request()` — Hono's built-in test utility)
263
+
264
+ Config change:
265
+ - `config.ts` — add `apiPort: number` (from `TELEGRAM_API_PORT`, default 4097)
266
+
267
+ Index change:
268
+ - `index.ts` — call `createApiServer()`, stop on shutdown
269
+
270
+ **Hono app design (following OpenCode pattern):**
271
+ ```ts
272
+ import { Hono } from "hono"
273
+
274
+ export type ApiDeps = {
275
+ sessionManager: SessionManager
276
+ turnManager: TurnManager
277
+ sdk: OpencodeClient
278
+ }
279
+
280
+ export function createApiApp(deps: ApiDeps) {
281
+ return new Hono()
282
+ .get("/api/sessions", (c) => { /* list active sessions */ })
283
+ .get("/api/session/:sessionId", (c) => { /* session info */ })
284
+ .post("/api/session/:sessionId/model", (c) => { /* set model */ })
285
+ .post("/api/session/:sessionId/agent", (c) => { /* set agent */ })
286
+ .post("/api/session/:sessionId/new", (c) => { /* new session */ })
287
+ .get("/api/models", (c) => { /* connected + filtered */ })
288
+ .get("/api/agents", (c) => { /* available agents */ })
289
+ }
290
+
291
+ export function createApiServer(deps: ApiDeps, port: number) {
292
+ const app = createApiApp(deps)
293
+ return Bun.serve({
294
+ hostname: "127.0.0.1",
295
+ port,
296
+ fetch: app.fetch,
297
+ })
298
+ }
299
+ ```
300
+
301
+ **Tests (using `app.request()`):**
302
+
303
+ *Session endpoints:*
304
+ 1. GET /api/sessions returns all active sessions with chatId, model, agent
305
+ 2. GET /api/sessions returns empty array when no sessions
306
+ 3. GET /api/session/:sessionId returns session info
307
+ 4. GET /api/session/:sessionId returns 404 for unknown session
308
+
309
+ *Model/Agent override:*
310
+ 5. POST /api/session/:sessionId/model sets modelOverride in SessionManager
311
+ 6. POST /api/session/:sessionId/model returns 404 for unknown session
312
+ 7. POST /api/session/:sessionId/agent sets agentOverride in SessionManager
313
+ 8. POST /api/session/:sessionId/agent returns 404 for unknown session
314
+
315
+ *Session management:*
316
+ 9. POST /api/session/:sessionId/new creates new session for the chat
317
+
318
+ *Provider/agent listing:*
319
+ 10. GET /api/models returns connected providers with filtered models
320
+ 11. GET /api/agents returns available agents
321
+ 12. GET /api/models uses filterModels (reuses C3 logic)
322
+
323
+ ### E5. SKILL.md — AI model/agent control skill
324
+
325
+ **File:** `deploy/skills/telegram-control/SKILL.md`
326
+
327
+ No tests — documentation only. Teaches the AI:
328
+ 1. How to discover its own sessionId (curl OpenCode session list, match title)
329
+ 2. Bot Control API base URL and endpoints
330
+ 3. Workflow examples: "switch to Opus", "use a different agent", "start new chat"
331
+ 4. Available models/agents listing
332
+
333
+ Deployment: copy to `$OPENCODE_DIRECTORY/.opencode/skills/telegram-control/SKILL.md`
334
+ on the VPS. Document in VPS.md.
335
+
336
+ ### F6. E2E tests — phase-6.5 (6-8 tests)
337
+
338
+ **File:** `e2e/phase-6.5.test.ts`
339
+
340
+ New E2E tests validating the real Telegram + OpenCode + Bot API integration.
341
+ The E2E runner already has the bot process running (with API server on :4097).
342
+
343
+ **/model filtering (via Telegram userbot):**
344
+ 1. /model shows provider buttons (not 86 — verify count is small, e.g. < 10)
345
+ 2. Clicking a provider shows models (verify count is small per provider)
346
+
347
+ **Bot Control API (via curl from E2E test):**
348
+ 3. GET /api/sessions returns at least 1 session after sending a message
349
+ 4. POST /api/session/:id/model changes the model (GET confirms new value)
350
+ 5. POST /api/session/:id/agent changes the agent (GET confirms new value)
351
+ 6. POST /api/session/:id/new creates a new session (old sessionId differs)
352
+ 7. GET /api/models returns providers with models (non-empty)
353
+ 8. GET /api/agents returns agents (non-empty)
354
+
355
+ **How E2E tests call the Bot API:**
356
+ The E2E test process runs on the same machine as the bot. It can call
357
+ `fetch("http://127.0.0.1:4097/api/sessions")` directly — no Telegram
358
+ intermediary needed. This tests the real Hono server with real SessionManager.
359
+
360
+ **Flow for test 4 (model change):**
361
+ ```
362
+ 1. Userbot sends "hello" → bot creates session
363
+ 2. E2E calls GET /api/sessions → find sessionId
364
+ 3. E2E calls POST /api/session/:id/model { providerID, modelID }
365
+ 4. E2E calls GET /api/session/:id → verify modelOverride changed
366
+ 5. Userbot sends "ping" → bot responds (model override is used)
367
+ ```
368
+
369
+ ### G7. Full E2E regression
370
+
371
+ Run `bun test ./e2e/` — all previous 32 tests + new 6-8 tests must pass.
372
+
373
+ ---
374
+
375
+ ## Dependencies to Add
376
+
377
+ ```json
378
+ {
379
+ "dependencies": {
380
+ "hono": "^4.x",
381
+ "@grammyjs/transformer-throttler": "^1.x",
382
+ "@grammyjs/runner": "^2.x"
383
+ }
384
+ }
385
+ ```
386
+
387
+ ## Acceptance Criteria
388
+
389
+ - [ ] EventBus reconnects automatically after SSE stream breaks
390
+ - [ ] Reconnection uses exponential backoff (2s→30s)
391
+ - [ ] Bot continues working after OpenCode server restart
392
+ - [ ] Grammy apiThrottler handles 429 rate limits
393
+ - [ ] Grammy sequentialize prevents per-chat race conditions
394
+ - [ ] /model shows only connected providers (not all 86)
395
+ - [ ] /model shows only most recent model per family
396
+ - [ ] /model marks active model with ✓
397
+ - [ ] Bot Control API starts on configured port (default 4097)
398
+ - [ ] AI can list sessions via GET /api/sessions
399
+ - [ ] AI can change model via POST /api/session/:id/model
400
+ - [ ] AI can change agent via POST /api/session/:id/agent
401
+ - [ ] AI can create new session via POST /api/session/:id/new
402
+ - [ ] SKILL.md documents full workflow for AI
403
+ - [ ] E2E: /model shows filtered providers (small count, not 86)
404
+ - [ ] E2E: Bot Control API sessions, model, agent, new all work end-to-end
405
+ - [ ] `bun test src/` passes (all unit tests)
406
+ - [ ] `bun test ./e2e/` passes (all E2E tests, previous 32 + new ~7)
407
+
408
+ ## Estimated Scope
409
+
410
+ - ~250-300 LOC new src (api-server.ts ~120, model filtering ~50, reconnect ~80)
411
+ - ~350-400 LOC new tests (unit + E2E)
412
+ - ~80 LOC SKILL.md
413
+ - 4 new files (api-server.ts, api-server.test.ts, e2e/phase-6.5.test.ts, SKILL.md)
414
+ - 3 new npm dependencies (hono, @grammyjs/transformer-throttler, @grammyjs/runner)
415
+
416
+ ### Test Count Estimate
417
+
418
+ | File | New Tests |
419
+ |------|-----------|
420
+ | event-bus.test.ts | ~10 |
421
+ | bot.test.ts | ~3 |
422
+ | models.test.ts | ~10 |
423
+ | api-server.test.ts | ~12 |
424
+ | **Unit total** | **~35 new → ~287 total** |
425
+ | e2e/phase-6.5.test.ts | ~7 |
426
+ | **E2E total** | **~7 new → ~39 total** |