import { Hono } from 'hono'; import { mkdir, readdir, rename, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { getClaudeDir, getSessionDir, getArchivedSessionDir, getPiMonoDir, getPiMonoSessionDir } from '@@/data-path'; import type { HonoVariables } from '@@/create-router'; export const sessionsRouter = new Hono<{ Variables: HonoVariables }>(); // --- List all sessions (merged from both providers) --- sessionsRouter.get('/sessions', async (ctx) => { const { email } = ctx.get('user'); const [claudeSessions, piMonoSessions] = await Promise.all([fetchClaudeSessions(email), fetchPiMonoSessions(email)]); const merged = [...claudeSessions, ...piMonoSessions].sort((a, b) => b.createdAt - a.createdAt); return ctx.json(merged); }); // --- Messages --- sessionsRouter.get('/sessions/:provider/:id/messages', async (ctx) => { const { email } = ctx.get('user'); const provider = ctx.req.param('provider'); const id = ctx.req.param('id'); if (provider === 'claude') { const file = Bun.file(join(getSessionDir(email, id), 'messages.json')); if (!(await file.exists())) return ctx.json([]); try { return ctx.json(await file.json()); } catch { return ctx.json([]); } } if (provider === 'pi-mono') { const file = Bun.file(join(getPiMonoSessionDir(email, id), 'messages.json')); if (!(await file.exists())) return ctx.json([]); try { return ctx.json(await file.json()); } catch { return ctx.json([]); } } return ctx.json({ error: 'invalid provider' }, 400); }); sessionsRouter.put('/sessions/:provider/:id/messages', async (ctx) => { const { email } = ctx.get('user'); const provider = ctx.req.param('provider'); const id = ctx.req.param('id'); if (provider !== 'claude' && provider !== 'pi-mono') return ctx.json({ error: 'invalid provider' }, 400); const messages = ctx.get('body'); const dir = provider === 'pi-mono' ? getPiMonoSessionDir(email, id) : getSessionDir(email, id); await Bun.write(join(dir, 'messages.json'), JSON.stringify(messages)); return ctx.json({ ok: true }); }); // --- Rename --- sessionsRouter.put('/sessions/:provider/:id', async (ctx) => { const { email } = ctx.get('user'); const provider = ctx.req.param('provider'); const id = ctx.req.param('id'); const body = ctx.get('body') as { title?: string }; if (!body?.title || typeof body.title !== 'string') return ctx.json({ error: 'title required' }, 400); if (provider === 'claude') { const dir = getSessionDir(email, id); const metaFile = Bun.file(join(dir, 'meta.json')); if (!(await metaFile.exists())) return ctx.json({ error: 'session not found' }, 404); let meta: Record; try { meta = await metaFile.json(); } catch { return ctx.json({ error: 'corrupted session' }, 500); } meta.title = body.title.slice(0, 200); await Bun.write(join(dir, 'meta.json'), JSON.stringify(meta)); return ctx.json({ ok: true }); } if (provider === 'pi-mono') { const dir = getPiMonoSessionDir(email, id); const metaFile = Bun.file(join(dir, 'meta.json')); if (!(await metaFile.exists())) return ctx.json({ error: 'session not found' }, 404); let meta: Record; try { meta = await metaFile.json(); } catch { return ctx.json({ error: 'corrupted session' }, 500); } meta.title = body.title.slice(0, 200); await Bun.write(join(dir, 'meta.json'), JSON.stringify(meta)); return ctx.json({ ok: true }); } return ctx.json({ error: 'invalid provider' }, 400); }); // --- Delete --- sessionsRouter.delete('/sessions/:provider/:id', async (ctx) => { const { email } = ctx.get('user'); const provider = ctx.req.param('provider'); const id = ctx.req.param('id'); if (provider === 'claude') { const dir = getSessionDir(email, id); try { await rm(dir, { recursive: true }); } catch { // dir may not exist } return ctx.json({ ok: true }); } if (provider === 'pi-mono') { const dir = getPiMonoSessionDir(email, id); try { await rm(dir, { recursive: true }); } catch { // dir may not exist } return ctx.json({ ok: true }); } return ctx.json({ error: 'invalid provider' }, 400); }); // --- Archive (Claude only) --- sessionsRouter.post('/sessions/:provider/:id/archive', async (ctx) => { const { email } = ctx.get('user'); const provider = ctx.req.param('provider'); const id = ctx.req.param('id'); if (provider === 'pi-mono') return ctx.json({ error: 'pi-mono sessions cannot be archived' }, 400); if (provider !== 'claude') return ctx.json({ error: 'invalid provider' }, 400); const src = getSessionDir(email, id); const dest = getArchivedSessionDir(email, id); await mkdir(join(getClaudeDir(email), 'archived'), { recursive: true }); await rename(src, dest); return ctx.json({ ok: true }); }); // --- Helpers --- type SessionMeta = { id: string; title: string; createdAt: number; provider: 'claude' | 'pi-mono'; model?: string | null; }; async function fetchClaudeSessions(email: string): Promise { const dir = getClaudeDir(email); try { const entries = await readdir(dir); const sessions = await Promise.all( entries .filter((name) => name !== 'archived') .map(async (id) => { try { const metaFile = Bun.file(join(dir, id, 'meta.json')); if (!(await metaFile.exists())) return null; const meta = await metaFile.json(); return { ...meta, provider: 'claude' as const }; } catch { return null; } }), ); return sessions.filter((s): s is SessionMeta => s !== null); } catch { return []; } } async function fetchPiMonoSessions(email: string): Promise { const dir = getPiMonoDir(email); try { const entries = await readdir(dir); const sessions = await Promise.all( entries.map(async (id) => { try { const metaFile = Bun.file(join(dir, id, 'meta.json')); if (!(await metaFile.exists())) return null; const meta = await metaFile.json(); return { ...meta, provider: 'pi-mono' as const }; } catch { return null; } }), ); return sessions.filter((s): s is SessionMeta => s !== null); } catch { return []; } }