- Import telegramService, telegramRoutes, nexusSettingsService
- Mount /telegram routes under /api prefix
- Conditionally start Telegram bot on boot if telegramToken is configured
- Token route restarts bot after saving new token
- Install grammy v2 for long polling Telegram bot
- telegramService: text relay handler, agent prefix, session map, deleteWebhook lifecycle
- telegramRoutes: POST /telegram/token (getMe validation), GET /telegram/status
- telegram.ts under 500 lines (187 lines)
- Add Cross-Origin-Opener-Policy: same-origin middleware before all routes
- Add Cross-Origin-Embedder-Policy: require-corp middleware before all routes
- Required for @ricky0123/vad-react (VAD uses SharedArrayBuffer internally)
- server/src/routes/chat.ts: destructure voiceMode from req.body in stream endpoint
- server/src/routes/chat.ts: inject dual-output system prompt when voiceMode=full_voice (VPIPE-06)
- server/src/routes/chat.ts: persist voiceMode to messageType column (voice_full/voice_input)
- server/src/routes/chat-files.ts: remove old inline /transcribe endpoint (lines 297-386)
- server/src/app.ts: import and mount voiceRoutes() after nexusSettingsRoutes()
- Export VOICE_MODES constant and VoiceMode type from nexus-settings
- Export nexusSettingsSchema for testing
- Add voiceMode field with default 'text' to nexusSettingsSchema
- Add telegramToken optional field to nexusSettingsSchema
- Add piperBinaryPath and whisperBinaryPath optional fields
- Update fallback in get() to use nexusSettingsSchema.parse({}) for consistent defaults
- Add 5 passing tests for nexus-settings schema in 36-voice-schema.test.ts
- Add VOICE_MODES constant and VoiceMode type to shared validators/chat.ts
- Extend createMessageSchema with optional voiceMode enum field
- Add voiceMode optional field to ChatMessage interface in types/chat.ts
- Add 36-voice-schema.test.ts with 6 passing tests for voiceMode validation
- Install ffmpeg-static and @types/ffmpeg-static
- Create voice-pipeline.ts with voicePipelineService factory function
- transcodeToWav16k: pipes audio through ffmpeg at 16kHz mono WAV
- transcribe: whisper-cpp cascade with --language auto, falls back to openai-whisper
- synthesize: piper TTS with sentence chunking and 8s timeout via Promise.race
- formatForVoice: extracts SPOKEN marker or strips markdown as fallback
- Unit tests with mocked child_process (12 tests all passing)
- Export VOICE_MODES constant and VoiceMode type from nexus-settings
- Export nexusSettingsSchema for testing
- Add voiceMode field with default 'text' to nexusSettingsSchema
- Add telegramToken optional field to nexusSettingsSchema
- Add piperBinaryPath and whisperBinaryPath optional fields
- Update fallback in get() to use nexusSettingsSchema.parse({}) for consistent defaults
- Add 5 passing tests for nexus-settings schema in 36-voice-schema.test.ts
- Add VOICE_MODES constant and VoiceMode type to shared validators/chat.ts
- Extend createMessageSchema with optional voiceMode enum field
- Add voiceMode optional field to ChatMessage interface in types/chat.ts
- Add 36-voice-schema.test.ts with 6 passing tests for voiceMode validation
- Add chatFileRoutes(db, storageService) after assistantHandoffRoutes (inside boardMutationGuard)
- Add nexusSettingsRoutes() after chatFileRoutes
- Extend nexusSettingsSchema with voiceEnabled: z.boolean().default(false)
- Update default return values in nexusSettingsService.get() to include voiceEnabled: false
- Add voiceEnabled?: boolean to NexusSettings client interface in hardware.ts
Replace streamEcho with Puter proxy AI call, inject memory facts as
system message, append memory after each turn. Assistant-to-PM handoff
creates new conversation with context summary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add import for puterProxyRoutes from routes/puter-proxy.js
- Mount api.use(puterProxyRoutes(db)) after costRoutes inside api Router
- Route is protected by boardMutationGuard as required
- Test 1-2: PKCE generation (verifier/challenge format, auth URL params)
- Test 3: token exchange posts correct body to Google token endpoint
- Test 4-5: storeTokens create and rotate paths
- Test 6: authorize returns {url, stateId} with no companyId in pendingPkce
- Test 7: callback exchanges code and redirects with google_oauth=success
- Test 8: callback with invalid state returns 400
- Test 9: full authorize->callback->claim flow stores tokens by companyId
- Test 10: claim with missing stateId returns 404
- Test 11: api-keys/store upserts via secretService
- puterProxyService with storeToken (create/rotate idempotent), resolveToken, chatStream
- chatStream relays to Puter OpenAI-compat endpoint with SSE streaming
- Cost recording with provider=puter, billingType=subscription_included, costCents=0
- Cost recording skipped when agentId is null/undefined (no FK violation)
- puterProxyRoutes with POST /puter-proxy/token and POST /puter-proxy/chat
- Board auth (assertBoard + assertCompanyAccess) on all routes
- All 10 TDD tests passing
- POST /oauth/google/authorize: returns {url, stateId}, stores PKCE verifier only (no companyId)
- GET /oauth/google/callback: exchanges code, parks tokens in pendingTokens by stateId
- POST /oauth/google/claim: moves tokens from pendingTokens to secretService with real companyId
- POST /api-keys/store: upserts provider API keys (openai/anthropic/groq) via secretService
- Cleanup of entries older than 10 minutes on each request
- Mounted in app.ts via api.use(googleOAuthRoutes(db))
- generatePkce() using crypto.randomBytes base64url verifier and SHA256 challenge
- generateAuthUrl() builds Google OAuth URL with PKCE params for Gemini scopes
- exchangeCode() POSTs to Google token endpoint with code_verifier
- storeTokens() upserts google_gemini_oauth_token via secretService
- resolveTokens() retrieves and parses stored tokens by companyId
- Add hardwareRoutes with unauthenticated GET /system/providers
- Add hardwareRoutes with GET /system/providers/recommendation
- Add nexusSettingsRoutes with board-auth GET/PATCH /nexus/settings
- Mount hardwareRoutes on app before boardMutationGuard (unauthenticated)
- Mount nexusSettingsRoutes on api router (board-auth gated)
- Add hardwareService with Apple Silicon / GPU / cpu_only tier detection
- Add 3s Promise.race timeout for si.graphics() with cpu_only fallback
- Add nexusSettingsService with Zod validation and file-backed persistence
- Extend ollama-model-catalog.json with tier arrays on every variant
- Add qwen3:8b family to catalog
- Update getRecommendedModel to accept optional hardwareTier parameter
- All 13 unit tests pass (TDD green)
28-02: ollamaApi client, model dropdown in config, skill badge
28-03: stateJson merge after heartbeat, HermesRuntimeCard in AgentOverview
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Push notifications: call sendPushToAll after streaming completes
2. Mobile offline: add useOfflineQueue + banners to MobileChatView
3. New conversation streaming: call startStream in Path 1 handleSend
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add push_subscriptions pgTable with endpoint, p256dh, auth, userId, companyId, deviceLabel
- Add 0055_create_push_subscriptions.sql migration with CREATE TABLE and endpoint index
- Export pushSubscriptions from schema/index.ts
- Create pushService with initVapid, getVapidPublicKey, saveSubscription, removeSubscription, sendPushToAll
- sendPushToAll auto-deletes stale subscriptions on 410/404 response
- Create pushRoutes: GET /vapid-public-key, POST /subscribe, DELETE /subscribe
- Mount /api/push routes and call initVapid() in app.ts with graceful skip
- Install web-push and @types/web-push
Adds gitFileService with commitFile/getLog, wires git commits into
upload flow, adds GET /files/:fileId/history endpoint, and exports
ChatFileHistoryEntry type.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Import placeholderService and resolveDefaultStorageDir in chat-files routes
- Track agent_generated project-scoped uploads in PLACEHOLDERS.md manifest
- Add POST /files/:fileId/replace endpoint for placeholder replacement
- Replace endpoint updates manifest and creates cross-reference chain
- Mark FILE-08 and FILE-11 Complete in REQUIREMENTS.md
- Create server/src/services/placeholder-service.ts with addEntry, replaceEntry, listEntries
- Generates PLACEHOLDERS.md with Active Placeholders and Replaced markdown tables
- Add ChatPlaceholderEntry interface to packages/shared/src/types/chat.ts
- Export ChatPlaceholderEntry from packages/shared/src/index.ts
- Add markAsPlaceholder method to chatFileService in chat-files.ts
- Add ChatCodeFilePreview component with hljs syntax highlighting
- Fetch file content from contentPath with credentials
- Use DOMParser-based safe rendering (no dangerouslySetInnerHTML)
- Include copy button, language label, and ChatFileCard download below
- Add extToLang extension-to-language mapping
- Register 14 common languages with hljs
- Add highlight.js as direct dependency in ui/package.json
- Add VoiceRecordButton with MediaRecorder API, recording/transcribing/idle states
- Add POST /transcribe endpoint to chat-files.ts using execFileAsync (safe, no shell)
- Tries whisper-cpp first, falls back to openai-whisper Python CLI
- Returns 503 with helpful message if whisper is not installed
- Create chatFileRoutes with upload, list, content, references, attach endpoints
- Wire into app.ts after assetRoutes, export from routes/index.ts
- Real tests replacing todo stubs in chat-file-routes.test.ts
- Implement chatFileService(db) with create, getById, listByConversation, listByMessage, createReference, listReferences, attachToMessage
- Export deriveCategory() helper mapping MIME types to image/code/document/other
- Add unit tests verifying service methods and category derivation with mocked DB
- Add ChatFile, ChatFileReference, ChatFileUploadResponse, ChatFileListResponse interfaces to types/chat.ts
- Add optional files?: ChatFile[] field to ChatMessage interface
- Add uploadChatFileSchema and createFileReferenceSchema Zod validators to validators/chat.ts
- Re-export all new types and validators from shared/src/index.ts
- Create test stubs for chat-file-service.test.ts and chat-file-routes.test.ts with it.todo() entries
- GET /companies/:companyId/messages/search: FTS search with ZodError 400 guard
- POST /conversations/:id/bookmarks: toggle bookmark with UUID validation
- GET /companies/:companyId/bookmarks: list bookmarks with optional conversationId filter
- POST /conversations/:id/branch: branch conversation from message point
- GET /conversations/:id/branches: list child conversations
- GET /conversations/:id/export: download Markdown or JSON with Content-Disposition header
- searchMessages: tsvector FTS with ts_rank ordering, joins conversations for companyId scoping
- toggleBookmark: transactional insert-or-delete bookmark
- getBookmarks: joins bookmarks+messages+conversations, supports conversationId filter
- branchConversation: copies messages up to branch point into new child conversation
- listBranches: queries child conversations by parentConversationId
- exportConversation: LEFT JOINs agents for name resolution, produces Markdown or JSON with file headers
- POST /conversations/:id/handoff: resolves companyId, inserts handoff
message, creates issue from spec, inserts task_created message
- POST /conversations/:id/status-update: inserts status_update message
- Import issueService and handoffSchema; instantiate issueSvc in chatRoutes
- Both routes use assertBoard for authentication
- Add editMessage, truncateMessagesAfter, streamEcho methods to chatService
- Add POST /conversations/:id/stream SSE endpoint with flushHeaders before loop (PERF-02)
- Add PATCH /conversations/:id/messages/:msgId for message editing
- Add DELETE /conversations/:id/messages/after/:msgId for message truncation
- Import gt from drizzle-orm for createdAt comparison in truncateMessagesAfter
- Guard all res.write() calls with res.writable check (prevents write-after-end)
- Add ilike import and search/agentId conditions in chatService.listConversations
- Extract search and agentId from req.query in GET /conversations route
- Extend chatApi.listConversations opts with search and agentId params
- Update useChatConversations to accept opts.search and include in queryKey
- chatRoutes factory with 7 REST endpoints for conversations and messages
- All routes gated by assertBoard; company-scoped routes also use assertCompanyAccess
- Mounted as api.use(chatRoutes(db)) after activityRoutes in app.ts
- Export createConversationSchema/updateConversationSchema/createMessageSchema
from @paperclipai/shared (were missing from main package index)
- 11 vitest route tests passing
- Add resolveSkillsDirForAgent helper to skill-registry.ts and skill-registry-groups.ts
- Install route accepts agentId in body (not agentSkillsDir)
- Uninstall route accepts agentId as query param; returns 403 for native skills
- Rollback route accepts agentId in body (not agentSkillsDir)
- Group assign/remove routes resolve dir from URL agentId param
- List agent skills route calls syncHermesNativeSkills for hermes_local agents
- skillRegistryRoutes(db) and skillGroupRoutes(db) factory signatures updated
- app.ts passes db to both route factories