--- phase: 38-telegram-bridge plan: 01 type: execute wave: 1 depends_on: [] files_modified: - server/package.json - server/src/services/telegram.ts - server/src/routes/telegram.ts - server/src/app.ts autonomous: true requirements: [TGRAM-01, TGRAM-02, TGRAM-05, TGRAM-06] must_haves: truths: - "A text message sent to the Telegram bot produces an agent reply prefixed with the agent name" - "The bot runs via long polling with no public HTTPS endpoint" - "The telegram.ts service file is under 500 lines" artifacts: - path: "server/src/services/telegram.ts" provides: "grammY bot lifecycle, text message relay, session map, agent prefix" exports: ["telegramService"] - path: "server/src/routes/telegram.ts" provides: "POST /api/telegram/token validation, GET /api/telegram/status" exports: ["telegramRoutes"] key_links: - from: "server/src/services/telegram.ts" to: "server/src/services/chat.ts" via: "chatService(db).createConversation, addMessage" pattern: "chatService.*createConversation|addMessage" - from: "server/src/services/telegram.ts" to: "server/src/services/puter-proxy.ts" via: "puterProxyService(db).chatStream async generator collection" pattern: "puterProxy.*chatStream" - from: "server/src/app.ts" to: "server/src/services/telegram.ts" via: "conditional start on telegramToken presence" pattern: "telegramService|tg\\.start" --- Create the Telegram bridge service and management routes. Install grammY, implement the telegramService factory function with text message relay (user -> agent -> reply with [AgentName] prefix), session mapping, long polling lifecycle, and token validation endpoint. Purpose: This is the core Telegram relay infrastructure. Text messaging must work end-to-end before voice can be layered on (Plan 02). Output: `server/src/services/telegram.ts`, `server/src/routes/telegram.ts`, updated `server/src/app.ts` @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/phases/38-telegram-bridge/38-CONTEXT.md @.planning/phases/38-telegram-bridge/38-RESEARCH.md @.planning/REQUIREMENTS.md From server/src/services/chat.ts: ```typescript export function chatService(db: Db) { return { async createConversation(companyId: string, data: { title?: string; agentId?: string }), async addMessage(conversationId: string, data: { role: string; content: string; agentId?: string; ... }), async listMessages(conversationId: string, opts: { cursor?: string; limit?: number }), // ... } } ``` From server/src/services/puter-proxy.ts: ```typescript export function puterProxyService(db: Db) { return { async* chatStream( companyId: string, agentId: string | null | undefined, messages: unknown[], model: string | undefined, signal: AbortSignal | undefined, ): AsyncGenerator, async resolveToken(companyId: string): Promise, } } ``` From server/src/services/nexus-settings.ts: ```typescript export function nexusSettingsService() { return { async get(): Promise, // includes telegramToken?: string async set(patch: Partial): Promise, } } ``` From server/src/services/agents.ts: ```typescript // agentService(db).list(companyId) returns agents with .name property ``` From server/src/services/companies.ts: ```typescript // companyService(db).list() returns companies array ``` Route mounting pattern in app.ts: ```typescript api.use(someRoutes(db)); // mounts under /api prefix ``` Task 1: Install grammY and create telegramService + telegramRoutes server/package.json, server/src/services/telegram.ts, server/src/routes/telegram.ts - server/src/services/chat.ts (createConversation, addMessage signatures) - server/src/services/puter-proxy.ts (chatStream async generator pattern) - server/src/services/agents.ts (list method, agent.name field) - server/src/services/companies.ts (list method) - server/src/services/nexus-settings.ts (get/set, telegramToken field) - server/src/routes/nexus-settings.ts (route pattern reference) - server/src/routes/authz.ts (assertBoard helper) 1. Install grammY: `cd server && pnpm add grammy` 2. Create `server/src/services/telegram.ts` — factory function `telegramService(db: Db)`: - Import `Bot, InputFile` from "grammy", plus chatService, agentService, companyService, puterProxyService, nexusSettingsService, logger - In-memory `sessionMap: Map` for `${chatId}:${agentId}` -> conversationId - `getOrCreateConversation(chatId, agentId, db)` — looks up Map, creates via chatService if missing - `resolveDefaultAgent(db)` — gets first company via companyService(db).list(), then first agent via agentService(db).list(companyId). Returns `{ companyId, agentId, agentName }`. If no agents, returns null. - `bot.on("message:text", async (ctx) => { ... })` handler: - Resolve agent via `resolveDefaultAgent(db)`. If null, reply "No agents configured" and return. - Get/create conversation via `getOrCreateConversation` - Persist user message via `chatSvc.addMessage(convId, { role: "user", content: ctx.message.text })` - Build messages array from `chatSvc.listMessages` (last 20 messages, map to `{role, content}` format for LLM) - Collect full response from `puterProxyService(db).chatStream(companyId, agentId, messages, undefined, undefined)` by iterating the async generator - Prefix reply with `[AgentName]: ` (TGRAM-02) - Split at 4000 chars if reply exceeds 4096 (Telegram limit) — send multiple messages - Persist assistant message via `chatSvc.addMessage` - Send reply via `ctx.reply(text, { parse_mode: "Markdown" })` - Wrap in try/catch — on error reply "Sorry, something went wrong." - `bot.catch((err) => logger.error({ err }, "Telegram bot error"))` before start - `start(token: string)`: create Bot, call `bot.api.deleteWebhook()` first (Pitfall 4), register handlers, call `bot.start()` (do NOT await — never-resolving promise). Save bot reference. - `stop()`: await `bot?.stop()`, set bot to null - `isRunning()`: returns `bot !== null` - Return `{ start, stop, isRunning }` - CRITICAL: Keep under 500 lines total (TGRAM-06). Target ~250 lines for text-only; voice handlers will add ~150 in Plan 02. 3. Create `server/src/routes/telegram.ts` — `telegramRoutes(db: Db)`: - `POST /telegram/token` — assertBoard, validate token string from body, create temp `new Bot(token)`, call `bot.api.getMe()` to validate (catch -> 400 "Invalid Telegram bot token"). On success save via `nexusSettingsService().set({ telegramToken: token })`. Return `{ ok: true, botUsername: me.username }`. - `GET /telegram/status` — assertBoard, return `{ running: boolean }` from the telegram service instance. To access the service instance, accept it as a second parameter: `telegramRoutes(db, telegramSvc)`. cd server && pnpm exec tsc --noEmit 2>&1 | head -30 - grep -q "grammy" server/package.json - grep -q "export function telegramService" server/src/services/telegram.ts - grep -q "export function telegramRoutes" server/src/routes/telegram.ts - grep -q "bot.on.*message:text" server/src/services/telegram.ts - grep -q "AgentName\|agentName" server/src/services/telegram.ts - grep -q "bot.start()" server/src/services/telegram.ts - grep -q "deleteWebhook" server/src/services/telegram.ts - grep -q "getMe" server/src/routes/telegram.ts - wc -l < server/src/services/telegram.ts | awk '{exit ($1 > 500)}' telegramService factory function exists with text relay handler, agent prefix, long polling lifecycle. telegramRoutes has token validation and status endpoints. grammY installed. File under 500 lines. Task 2: Wire telegram service and routes into app.ts startup server/src/app.ts - server/src/app.ts (full file — understand createApp structure, route mounting, return value) - server/src/services/telegram.ts (the file created in Task 1) - server/src/routes/telegram.ts (the file created in Task 1) 1. In `server/src/app.ts`: - Add imports: `import { telegramService } from "./services/telegram.js"` and `import { telegramRoutes } from "./routes/telegram.js"` - Inside `createApp` function, after all route mounting but before the catch-all 404: - Create telegram service instance: `const tg = telegramService(db);` - Mount routes: `api.use(telegramRoutes(db, tg));` - Read settings and conditionally start: `const settings = await nexusSettingsService().get(); if (settings.telegramToken) { tg.start(settings.telegramToken).catch(err => logger.error({ err }, "Failed to start Telegram bot")); }` - Add `nexusSettingsService` import if not already present - Add `tg` to the return value of createApp (or store as module-level ref) so the token route can restart it after saving a new token 2. In `server/src/routes/telegram.ts`, update the POST /telegram/token handler to also (re)start the telegram service after saving the token. The route receives the service instance as parameter — call `await svc.stop()` then `await svc.start(token)`. cd server && pnpm exec tsc --noEmit 2>&1 | head -30 - grep -q "telegramService" server/src/app.ts - grep -q "telegramRoutes" server/src/app.ts - grep -q "telegramToken" server/src/app.ts - grep -q "tg.start\|tg\.stop\|svc.start\|svc\.stop" server/src/routes/telegram.ts Telegram service starts automatically on app boot when token is configured. Token validation endpoint restarts the bot after saving new token. TypeScript compiles clean. - `cd server && pnpm exec tsc --noEmit` — zero errors - `wc -l server/src/services/telegram.ts` — under 500 lines - `grep -c "bot.on\|bot.start\|bot.catch" server/src/services/telegram.ts` — at least 3 matches - `grep "telegramRoutes\|telegramService" server/src/app.ts` — both present - grammY installed in server/package.json - telegramService factory function with text relay, agent prefix, session map, long polling - telegramRoutes with POST /telegram/token (validates + saves + restarts) and GET /telegram/status - app.ts conditionally starts telegram bot on boot - TypeScript compiles without errors - telegram.ts under 500 lines After completion, create `.planning/phases/38-telegram-bridge/38-01-SUMMARY.md`