11 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 38-telegram-bridge | 01 | execute | 1 |
|
true |
|
|
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.mdFrom 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`
-
Create
server/src/services/telegram.ts— factory functiontelegramService(db: Db):- Import
Bot, InputFilefrom "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 missingresolveDefaultAgent(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."
- Resolve agent via
bot.catch((err) => logger.error({ err }, "Telegram bot error"))before startstart(token: string): create Bot, callbot.api.deleteWebhook()first (Pitfall 4), register handlers, callbot.start()(do NOT await — never-resolving promise). Save bot reference.stop(): awaitbot?.stop(), set bot to nullisRunning(): returnsbot !== 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.
- Import
-
Create
server/src/routes/telegram.ts—telegramRoutes(db: Db):POST /telegram/token— assertBoard, validate token string from body, create tempnew Bot(token), callbot.api.getMe()to validate (catch -> 400 "Invalid Telegram bot token"). On success save vianexusSettingsService().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).
- 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 — callawait svc.stop()thenawait 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.
<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>