docs(38): create 3 plans in 2 waves for Telegram bridge

This commit is contained in:
Nexus Dev 2026-04-04 03:09:38 +00:00
parent 4073625cb0
commit 9959d1b77e
3 changed files with 626 additions and 0 deletions

View file

@ -0,0 +1,231 @@
---
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"
---
<objective>
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`
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/phases/38-telegram-bridge/38-CONTEXT.md
@.planning/phases/38-telegram-bridge/38-RESEARCH.md
@.planning/REQUIREMENTS.md
<interfaces>
<!-- Key types and contracts the executor needs -->
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<string>,
async resolveToken(companyId: string): Promise<string>,
}
}
```
From server/src/services/nexus-settings.ts:
```typescript
export function nexusSettingsService() {
return {
async get(): Promise<NexusSettings>, // includes telegramToken?: string
async set(patch: Partial<NexusSettings>): Promise<NexusSettings>,
}
}
```
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
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Install grammY and create telegramService + telegramRoutes</name>
<files>server/package.json, server/src/services/telegram.ts, server/src/routes/telegram.ts</files>
<read_first>
- 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)
</read_first>
<action>
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<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.
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)`.
</action>
<verify>
<automated>cd server && pnpm exec tsc --noEmit 2>&1 | head -30</automated>
</verify>
<acceptance_criteria>
- 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)}'
</acceptance_criteria>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Wire telegram service and routes into app.ts startup</name>
<files>server/src/app.ts</files>
<read_first>
- 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)
</read_first>
<action>
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)`.
</action>
<verify>
<automated>cd server && pnpm exec tsc --noEmit 2>&1 | head -30</automated>
</verify>
<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>
<done>Telegram service starts automatically on app boot when token is configured. Token validation endpoint restarts the bot after saving new token. TypeScript compiles clean.</done>
</task>
</tasks>
<verification>
- `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
</verification>
<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>
<output>
After completion, create `.planning/phases/38-telegram-bridge/38-01-SUMMARY.md`
</output>

View file

@ -0,0 +1,183 @@
---
phase: 38-telegram-bridge
plan: 02
type: execute
wave: 2
depends_on: ["38-01"]
files_modified:
- server/src/services/telegram.ts
autonomous: true
requirements: [TGRAM-03, TGRAM-04]
must_haves:
truths:
- "A voice note sent to the Telegram bot is transcribed and produces an agent text reply"
- "The bot can send back an OGG voice note generated from TTS"
artifacts:
- path: "server/src/services/telegram.ts"
provides: "Voice message handler (OGG download, transcribe, relay) and TTS reply (synthesize, WAV->OGG, sendVoice)"
contains: "message:voice"
key_links:
- from: "server/src/services/telegram.ts"
to: "server/src/services/voice-pipeline.ts"
via: "voicePipelineService().transcribe and synthesize"
pattern: "voicePipelineService.*transcribe|synthesize"
- from: "server/src/services/telegram.ts"
to: "Telegram Bot API file download"
via: "ctx.getFile() + fetch download URL"
pattern: "ctx\\.getFile|api\\.telegram\\.org/file"
---
<objective>
Add voice message handling to the Telegram bridge. Voice notes received are downloaded (OGG), transcribed via VoicePipelineService, and relayed as text. Agent responses can optionally be sent back as OGG voice notes via TTS.
Purpose: Completes the bidirectional voice relay, making the Telegram bridge work for hands-free phone use.
Output: Updated `server/src/services/telegram.ts` with voice handlers
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/phases/38-telegram-bridge/38-CONTEXT.md
@.planning/phases/38-telegram-bridge/38-RESEARCH.md
@.planning/phases/38-telegram-bridge/38-01-SUMMARY.md
@.planning/REQUIREMENTS.md
<interfaces>
<!-- Voice pipeline service from Phase 36 -->
From server/src/services/voice-pipeline.ts:
```typescript
export function voicePipelineService() {
return {
async transcodeToWav16k(inputBuffer: Buffer, inputFormat: string): Promise<Buffer>,
async transcribe(audioBuffer: Buffer, format?: string): Promise<{ text: string; language?: string }>,
async synthesize(text: string, voiceId?: string): Promise<Buffer>, // returns raw PCM s16le
formatForVoice(text: string): string, // strips markdown for natural speech
}
}
```
From grammy (already installed in Plan 01):
```typescript
import { Bot, InputFile } from "grammy";
// ctx.getFile() returns { file_path: string }
// Download URL: https://api.telegram.org/file/bot{TOKEN}/{file_path}
// ctx.replyWithVoice(new InputFile(buffer)) sends OGG voice note
```
ffmpeg transcoding pattern (from voice-pipeline.ts):
```typescript
// spawn(ffmpegBin, ["-f", "s16le", "-ar", "22050", "-ac", "1", "-i", "pipe:0", ...])
// For WAV->OGG: output args = ["-c:a", "libopus", "-ar", "48000", "-f", "ogg", "pipe:1"]
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add voice message handler — OGG download, transcription, text relay</name>
<files>server/src/services/telegram.ts</files>
<read_first>
- server/src/services/telegram.ts (the file from Plan 01 — understand handler registration pattern, existing text handler)
- server/src/services/voice-pipeline.ts (transcribe method signature, transcodeToWav16k)
</read_first>
<action>
In `server/src/services/telegram.ts`, add a voice message handler:
1. Import `voicePipelineService` from `"./voice-pipeline.js"` and `InputFile` from "grammy" (if not already)
2. Add `bot.on("message:voice", async (ctx) => { ... })` handler:
- Send immediate "Transcribing..." reply to prevent Telegram resend (Pitfall 1)
- Fire off async `processVoiceMessage(ctx, db)` — do NOT await in handler body. Catch errors and reply with "Voice transcription failed."
3. Implement `processVoiceMessage(ctx, db)`:
- Download OGG: `const file = await ctx.getFile()` then construct URL `https://api.telegram.org/file/bot${token}/${file.file_path}` — get token from bot instance. Fetch the URL, get `arrayBuffer()`, convert to Buffer.
- Transcribe: `const voiceSvc = voicePipelineService()`, then `const { text } = await voiceSvc.transcribe(oggBuffer, "ogg")`. The transcribe method handles OGG->WAV16k internally via transcodeToWav16k.
- If transcription is empty, reply "Could not transcribe voice message." and return.
- Send transcription confirmation: `await ctx.reply("Heard: " + text.slice(0, 200))` (truncate for readability)
- Then relay as text — reuse the SAME text relay logic from the text handler. Extract the text relay logic into a shared `relayToAgent(ctx, chatId, userText, db)` function that both `message:text` and voice handlers call.
4. Refactor: Extract the core relay logic from the existing `message:text` handler into `relayToAgent(ctx, chatId, userText, db)` so both text and voice handlers can use it. This function:
- Resolves agent, gets/creates conversation, persists user message, collects LLM stream, prefixes with [AgentName], splits long messages, persists assistant message, sends reply.
5. CRITICAL: Check total line count stays under 500 (TGRAM-06). The voice handler + refactor should add ~60-80 lines.
</action>
<verify>
<automated>cd server && pnpm exec tsc --noEmit 2>&1 | head -30</automated>
</verify>
<acceptance_criteria>
- grep -q "message:voice" server/src/services/telegram.ts
- grep -q "voicePipelineService" server/src/services/telegram.ts
- grep -q "transcribe" server/src/services/telegram.ts
- grep -q "ctx.getFile" server/src/services/telegram.ts
- grep -q "Transcribing" server/src/services/telegram.ts
- grep -q "relayToAgent\|relayText" server/src/services/telegram.ts
- wc -l < server/src/services/telegram.ts | awk '{exit ($1 > 500)}'
</acceptance_criteria>
<done>Voice notes sent to the bot are downloaded, transcribed via voicePipelineService, and relayed to the agent as text. Transcription confirmation shown to user. Shared relay function prevents code duplication.</done>
</task>
<task type="auto">
<name>Task 2: Add TTS voice reply — synthesize agent response to OGG voice note</name>
<files>server/src/services/telegram.ts</files>
<read_first>
- server/src/services/telegram.ts (current state after Task 1)
- server/src/services/voice-pipeline.ts (synthesize method — returns raw PCM s16le buffer)
</read_first>
<action>
In `server/src/services/telegram.ts`, add voice reply capability:
1. Add a `transcodeToOggOpus(rawPcmBuffer: Buffer): Promise<Buffer>` helper function:
- Use `spawn` from `node:child_process` with `ffmpegPath` from `ffmpeg-static`
- Input: raw PCM s16le (Piper output). Use `-f s16le -ar 22050 -ac 1 -i pipe:0`
- Output: OGG Opus for Telegram. Use `-c:a libopus -ar 48000 -f ogg pipe:1`
- Collect stdout chunks into Buffer. Reject on non-zero exit code.
- Note on sample rate: Piper `en_US-lessac-medium` outputs 22050Hz. If the model metadata is available at `voicePipelineService`, read it. Otherwise hardcode 22050 with a comment noting it must match the Piper model.
2. Modify the `relayToAgent` function (or add a post-relay hook):
- After sending the text reply, check if the user's last message was a voice note (pass a `voiceMode` flag or check context)
- If voice mode: call `voiceSvc.formatForVoice(fullResponse)` to strip markdown, then `voiceSvc.synthesize(voiceText)` to get raw PCM, then `transcodeToOggOpus(pcmBuffer)` to get OGG, then `ctx.replyWithVoice(new InputFile(oggBuffer, "response.ogg"))`.
- Wrap TTS reply in try/catch — if synthesis fails (Piper not installed), log warning and skip voice reply silently. Text reply already sent, so user still gets the response.
3. Alternative approach (simpler): Always send text reply first. Then if the original message was voice, attempt a voice reply as a bonus. This way failure of TTS never blocks the text response.
4. Keep total telegram.ts under 500 lines. The TTS reply adds ~40-50 lines.
</action>
<verify>
<automated>cd server && pnpm exec tsc --noEmit 2>&1 | head -30</automated>
</verify>
<acceptance_criteria>
- grep -q "transcodeToOggOpus\|ogg.*opus\|libopus" server/src/services/telegram.ts
- grep -q "synthesize" server/src/services/telegram.ts
- grep -q "replyWithVoice\|reply_with_voice" server/src/services/telegram.ts
- grep -q "InputFile" server/src/services/telegram.ts
- grep -q "formatForVoice" server/src/services/telegram.ts
- wc -l < server/src/services/telegram.ts | awk '{exit ($1 > 500)}'
</acceptance_criteria>
<done>Agent responses to voice messages include both a text reply and an OGG voice note. TTS failure degrades gracefully to text-only. telegram.ts remains under 500 lines.</done>
</task>
</tasks>
<verification>
- `cd server && pnpm exec tsc --noEmit` — zero errors
- `wc -l server/src/services/telegram.ts` — under 500 lines
- `grep -c "message:voice\|transcribe\|synthesize\|replyWithVoice\|InputFile" server/src/services/telegram.ts` — at least 5 matches
</verification>
<success_criteria>
- Voice notes are downloaded from Telegram, transcribed, and relayed to agent as text
- Agent responses generate OGG voice notes sent back via Telegram
- TTS failure degrades gracefully (text reply still works)
- telegram.ts remains under 500 lines total
- TypeScript compiles without errors
</success_criteria>
<output>
After completion, create `.planning/phases/38-telegram-bridge/38-02-SUMMARY.md`
</output>

View file

@ -0,0 +1,212 @@
---
phase: 38-telegram-bridge
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- ui/src/components/onboarding/TelegramStep.tsx
- ui/src/components/NexusOnboardingWizard.tsx
autonomous: true
requirements: [ONBRD-03]
must_haves:
truths:
- "The onboarding wizard includes a BotFather setup step that walks the user through creating a bot token"
- "The token is validated with a live API call before saving"
- "The step can be skipped without blocking onboarding completion"
artifacts:
- path: "ui/src/components/onboarding/TelegramStep.tsx"
provides: "BotFather instructions, token input, validation, skip button"
exports: ["TelegramStep"]
- path: "ui/src/components/NexusOnboardingWizard.tsx"
provides: "Updated step flow with TelegramStep inserted"
contains: "TelegramStep"
key_links:
- from: "ui/src/components/onboarding/TelegramStep.tsx"
to: "POST /api/telegram/token"
via: "fetch call for token validation"
pattern: "fetch.*telegram/token"
- from: "ui/src/components/NexusOnboardingWizard.tsx"
to: "ui/src/components/onboarding/TelegramStep.tsx"
via: "import and render as step 5"
pattern: "TelegramStep"
---
<objective>
Add a BotFather guided setup step to the onboarding wizard. Users are walked through creating a Telegram bot, entering the token, and validating it — all without touching config files.
Purpose: Makes Telegram bridge setup accessible to non-technical users during first-run onboarding.
Output: `TelegramStep.tsx` component, updated `NexusOnboardingWizard.tsx`
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/phases/38-telegram-bridge/38-CONTEXT.md
@.planning/phases/38-telegram-bridge/38-RESEARCH.md
@.planning/REQUIREMENTS.md
<interfaces>
<!-- Existing onboarding step pattern -->
From ui/src/components/NexusOnboardingWizard.tsx:
```typescript
// Current 6-step flow:
// Step 1: Hardware detection (HardwareSummaryStep)
// Step 2: Mode selection
// Step 3: Provider selection (ProviderSelectionStep)
// Step 4: Voice (VoiceStep)
// Step 5: Root directory
// Step 6: Summary (OnboardingSummaryStep)
//
// Step indicator shows "Step N of 5" (summary is step 6 but not counted)
// Each step has Next/Back/Skip buttons using <Button> from @/components/ui/button
```
From ui/src/components/onboarding/VoiceStep.tsx (pattern reference):
```typescript
// VoiceStep receives props for onNext, onBack, onSkip
// Uses same Button/Input/cn imports as other steps
```
Token validation endpoint (from Plan 01):
```typescript
// POST /api/telegram/token { token: string }
// Returns: { ok: true, botUsername: string } on success
// Returns: 400 { error: string } on invalid token
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create TelegramStep onboarding component</name>
<files>ui/src/components/onboarding/TelegramStep.tsx</files>
<read_first>
- ui/src/components/onboarding/VoiceStep.tsx (pattern reference for step component structure, props, styling)
- ui/src/components/onboarding/HardwareSummaryStep.tsx (alternative pattern reference)
- ui/src/components/ui/button.tsx (Button component API)
- ui/src/components/ui/input.tsx (Input component API)
</read_first>
<action>
Create `ui/src/components/onboarding/TelegramStep.tsx`:
1. Props interface: `{ onNext: () => void; onBack: () => void; }` — match the pattern from VoiceStep or HardwareSummaryStep.
2. State:
- `token: string` — the bot token input
- `validating: boolean` — loading state during validation
- `botUsername: string | null` — set after successful validation
- `error: string | null` — validation error message
3. `handleValidate` function:
- POST to `/api/telegram/token` with `{ token }` body
- On success: set `botUsername` from response, clear error
- On failure: set error from response or generic "Invalid token"
- Always clear `validating` in finally block
4. Render structure (follow existing step styling patterns):
- Title: "Telegram Bridge" or "Connect Telegram" with a brief subtitle
- BotFather instructions section — numbered steps:
1. "Open Telegram and search for @BotFather"
2. "Send /newbot and follow the prompts to create a bot"
3. "Copy the bot token (looks like 123456:ABC-DEF...)"
4. "Paste the token below"
- Token input field: `<Input type="text" placeholder="Paste bot token here" value={token} onChange={...} />`
- Validate button: `<Button onClick={handleValidate} disabled={!token.trim() || validating}>Validate Token</Button>`
- Success state: when `botUsername` is set, show green checkmark/text: "Connected to @{botUsername}" and enable Next button
- Error state: show red error text below input
- Navigation buttons: Back (always), Skip (always, calls onNext without saving), Next/Continue (enabled only when botUsername is set, calls onNext)
5. Style with Tailwind classes matching existing onboarding steps — use `cn()` from `../lib/utils` for conditional classes. Look at VoiceStep for the exact layout pattern (padding, spacing, button alignment).
</action>
<verify>
<automated>cd /opt/nexus/.claude/worktrees/agent-a61d32dc && pnpm --filter @paperclipai/ui exec tsc --noEmit 2>&1 | head -30</automated>
</verify>
<acceptance_criteria>
- grep -q "export.*TelegramStep\|export function TelegramStep" ui/src/components/onboarding/TelegramStep.tsx
- grep -q "telegram/token" ui/src/components/onboarding/TelegramStep.tsx
- grep -q "botUsername\|bot_username" ui/src/components/onboarding/TelegramStep.tsx
- grep -q "BotFather\|@BotFather" ui/src/components/onboarding/TelegramStep.tsx
- grep -q "onNext\|onBack" ui/src/components/onboarding/TelegramStep.tsx
- grep -q "Skip" ui/src/components/onboarding/TelegramStep.tsx
</acceptance_criteria>
<done>TelegramStep component exists with BotFather instructions, token input, live validation via POST /api/telegram/token, success/error states, and Skip/Back/Next navigation.</done>
</task>
<task type="auto">
<name>Task 2: Insert TelegramStep into NexusOnboardingWizard as step 5</name>
<files>ui/src/components/NexusOnboardingWizard.tsx</files>
<read_first>
- ui/src/components/NexusOnboardingWizard.tsx (full file — understand step flow, step state, step indicator, navigation callbacks)
- ui/src/components/onboarding/TelegramStep.tsx (the component from Task 1)
</read_first>
<action>
Modify `ui/src/components/NexusOnboardingWizard.tsx`:
1. Add import: `import { TelegramStep } from "./onboarding/TelegramStep";`
2. Update step comment to reflect 7-step flow:
`// Step 1: hardware detection, 2: mode selection, 3: provider selection, 4: voice, 5: telegram, 6: root directory, 7: summary`
3. Update step indicator text:
- Change `"Step ${step} of 5"` to `"Step ${step} of 6"` (telegram is now step 5, summary at 7 doesn't count)
- Change `step === 6 ? "Summary"` to `step === 7 ? "Summary"`
4. Insert Telegram step block between step 4 (Voice) and what was step 5 (Root directory):
```tsx
{step === 5 && (
<TelegramStep
onNext={() => setStep(6)}
onBack={() => setStep(4)}
/>
)}
```
5. Shift existing step numbers:
- What was `step === 5` (root directory) becomes `step === 6`
- What was `step === 6` (summary) becomes `step === 7`
- Update ALL `setStep(5)` calls that navigate TO root directory to `setStep(6)`
- Update ALL `setStep(6)` calls that navigate TO summary to `setStep(7)`
- Update the back button in root directory step from `setStep(4)` to `setStep(5)`
- Update the back button in summary step (if any) accordingly
6. IMPORTANT: Be thorough — search for EVERY occurrence of `setStep(5)` and `setStep(6)` in the file and update them. Missing one will cause navigation bugs.
7. Update the error message that references "step 5" for root directory — change to "step 6".
</action>
<verify>
<automated>cd /opt/nexus/.claude/worktrees/agent-a61d32dc && pnpm --filter @paperclipai/ui exec tsc --noEmit 2>&1 | head -30</automated>
</verify>
<acceptance_criteria>
- grep -q "TelegramStep" ui/src/components/NexusOnboardingWizard.tsx
- grep -q "step === 5" ui/src/components/NexusOnboardingWizard.tsx
- grep -q "Step.*of 6" ui/src/components/NexusOnboardingWizard.tsx
- grep -q "step === 7.*Summary\|step === 7" ui/src/components/NexusOnboardingWizard.tsx
</acceptance_criteria>
<done>NexusOnboardingWizard has 7 steps. TelegramStep is step 5. Root directory shifted to step 6, summary to step 7. All navigation callbacks updated. TypeScript compiles clean.</done>
</task>
</tasks>
<verification>
- `cd /opt/nexus/.claude/worktrees/agent-a61d32dc && pnpm --filter @paperclipai/ui exec tsc --noEmit` — zero errors
- `grep -c "TelegramStep" ui/src/components/NexusOnboardingWizard.tsx` — at least 2 (import + render)
- `grep "step === [1-7]" ui/src/components/NexusOnboardingWizard.tsx` — steps 1 through 7 all present
</verification>
<success_criteria>
- TelegramStep component renders BotFather instructions and validates token via API
- Step is skippable without blocking onboarding
- Wizard flow is now 7 steps with telegram at position 5
- TypeScript compiles without errors
</success_criteria>
<output>
After completion, create `.planning/phases/38-telegram-bridge/38-03-SUMMARY.md`
</output>