nexus/.planning/phases/38-telegram-bridge/38-01-PLAN.md

11 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
38-telegram-bridge 01 execute 1
server/package.json
server/src/services/telegram.ts
server/src/routes/telegram.ts
server/src/app.ts
true
TGRAM-01
TGRAM-02
TGRAM-05
TGRAM-06
truths artifacts key_links
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
path provides exports
server/src/services/telegram.ts grammY bot lifecycle, text message relay, session map, agent prefix
telegramService
path provides exports
server/src/routes/telegram.ts POST /api/telegram/token validation, GET /api/telegram/status
telegramRoutes
from to via pattern
server/src/services/telegram.ts server/src/services/chat.ts chatService(db).createConversation, addMessage chatService.*createConversation|addMessage
from to via pattern
server/src/services/telegram.ts server/src/services/puter-proxy.ts puterProxyService(db).chatStream async generator collection puterProxy.*chatStream
from to via pattern
server/src/app.ts server/src/services/telegram.ts conditional start on telegramToken presence 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

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

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:

export function puterProxyService(db: Db) {
  return {
    async* chatStream(
      companyId: string,
      agentId: string | null | undefined,
      messages: unknown[],
      model: string | undefined,
      signal: AbortSignal | undefined,
    ): AsyncGenerator<string>,
    async resolveToken(companyId: string): Promise<string>,
  }
}

From server/src/services/nexus-settings.ts:

export function nexusSettingsService() {
  return {
    async get(): Promise<NexusSettings>,  // includes telegramToken?: string
    async set(patch: Partial<NexusSettings>): Promise<NexusSettings>,
  }
}

From server/src/services/agents.ts:

// agentService(db).list(companyId) returns agents with .name property

From server/src/services/companies.ts:

// companyService(db).list() returns companies array

Route mounting pattern in app.ts:

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`
  1. 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<string, string> 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.
  2. Create server/src/routes/telegram.tstelegramRoutes(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
  1. 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 <acceptance_criteria>
    • 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 </acceptance_criteria> 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

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/38-telegram-bridge/38-01-SUMMARY.md`