import type { Context } from 'hono'; import { createRouter } from '../../create-router'; import { getUserSettings } from 'officerdb'; import { getClaudeSessionsCwd, listClaudePwds, listClaudeSessions, loadClaudeSession, deleteClaudeSession, renameClaudeSession, } from './claude-sessions'; import { listChatModels } from './list-models'; import { logger } from './logger'; import { readSttConfig } from '../server-settings/stt'; import { transcribeAudio } from '../stt/transcribe'; export const chatRouter = createRouter(); // The working directory a request operates on: an explicit ?cwd= (a chosen pwd), else the default // claude_sessions dir. Claude groups sessions by cwd, so this selects which project group we read. const cwdOf = (ctx: Context, email: string): string => ctx.req.query('cwd')?.trim() || getClaudeSessionsCwd(email); // GET /chat/pwds — the default /chat dir plus every directory that already has Claude sessions. chatRouter.get('/pwds', (ctx) => { const email = ctx.get('user').email; return ctx.json({ pwds: listClaudePwds(email), default: getClaudeSessionsCwd(email) }); }); // GET /chat/sessions[?cwd=] — the conversations for a working directory, from Claude's transcripts. chatRouter.get('/sessions', (ctx) => { const email = ctx.get('user').email; return ctx.json({ sessions: listClaudeSessions(email, cwdOf(ctx, email)) }); }); // GET /chat/sessions/:id[?cwd=] — one conversation's full transcript, parsed into display messages. chatRouter.get('/sessions/:id', (ctx) => { const email = ctx.get('user').email; const detail = loadClaudeSession(email, cwdOf(ctx, email), ctx.req.param('id')); if (!detail) return ctx.text('Not found', 404); return ctx.json(detail); }); // DELETE /chat/sessions/:id[?cwd=] — remove a conversation (deletes Claude's transcript file). chatRouter.delete('/sessions/:id', (ctx) => { const email = ctx.get('user').email; const ok = deleteClaudeSession(email, cwdOf(ctx, email), ctx.req.param('id')); if (!ok) return ctx.text('Not found', 404); return ctx.json({ ok: true }); }); // PATCH /chat/sessions/:id/title[?cwd=] — rename by writing a summary entry into Claude's transcript. chatRouter.patch('/sessions/:id/title', async (ctx) => { const email = ctx.get('user').email; const { title } = await ctx.req.json<{ title?: string }>(); if (!title?.trim()) return ctx.text('title is required', 400); const ok = renameClaudeSession(email, cwdOf(ctx, email), ctx.req.param('id'), title.trim()); if (!ok) return ctx.text('Not found', 404); return ctx.json({ ok: true }); }); // GET /chat/models — Claude tiers only (the runner is the `claude` CLI). chatRouter.get('/models', async (ctx: Context) => { try { const models = await listChatModels(); const providerNames: Record = { 'claude-code': 'Claude Code' }; return ctx.json({ models, providerNames, hostHome: process.env.HOME ?? '' }); } catch (err) { logger.error('Failed to list models', { error: String(err) }); return ctx.json({ models: [], providerNames: {} }); } }); // POST /chat/stt — proxy an audio clip to the configured Whisper server. chatRouter.post('/stt', async (ctx: Context) => { const sttConfig = await readSttConfig(); if (!sttConfig?.url) { return ctx.json({ error: 'Whisper not configured — set it up in Settings → Speech to Text' }, 400); } const user = ctx.get('user'); const body = await ctx.req.parseBody(); const file = body['file']; if (!file || !(file instanceof File)) { return ctx.json({ error: 'file is required' }, 400); } const settings = (await getUserSettings(user.id)) as { languages?: { spoken?: string[] } }; const spokenLanguages = settings.languages?.spoken ?? []; try { const result = await transcribeAudio({ file, whisperUrl: sttConfig.url, spokenLanguages }); return ctx.json(result); } catch (err) { logger.error('STT proxy failed', { error: String(err) }); return ctx.json({ error: err instanceof Error ? err.message : 'Failed to reach Whisper server' }, 502); } });