Pure rename, no behavior change. Moves the misnamed "pi" chat harness into the
chat namespace:
- api/pi/{websocket,session-manager,types,logger,list-models} → api/chat/
- merge api/pi/rest.ts into api/chat/chat.ts (/pi/models → /chat/models,
/pi/stt → /chat/stt); drop the piRestRouter mount
- PiEvent → ChatEvent, piWebsocket → chatWebsocket, listPiModels → listChatModels
- WS route /api/pi/chat/ws → /api/chat/ws, provider tag 'pi' → 'chat'
- frontend: useChat/useAudioRecording URLs, usePiModels→useModels /
useVisiblePiModels→useVisibleModels / useEnabledPiModels→useEnabledModels,
'PI_MODELS' query key → 'CHAT_MODELS', attachments provider 'pi-mono' → 'chat'
The /pi-mono provider/harness settings router is renamed separately (next commit).
Note: the WS route change requires the mobile app to point at /api/chat/ws.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
4.0 KiB
TypeScript
98 lines
4.0 KiB
TypeScript
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<string, string> = { '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);
|
|
}
|
|
});
|