---
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