208 lines
6.3 KiB
TypeScript
208 lines
6.3 KiB
TypeScript
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<SessionMeta[]> {
|
|
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<SessionMeta[]> {
|
|
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 [];
|
|
}
|
|
}
|