Files
DashDescomplicar/api/routes/sessions.ts
T
ealmeida f733998945 feat(sessions): indexação multi-fonte Hermes + OpenCode com fix TypeScript
- Novos indexadores hermes-indexer.ts e opencode-indexer.ts para unificar
  sessões Claude, Hermes Agent e OpenCode num único sessions.db
- SessionMeta alargado: source (obrigatório), model, input/output_tokens,
  estimated_cost; project_path/jsonl_path agora nullable
- Fix TS: tipos explícitos, guard jsonl_path null, dependências instaladas

Security Audit (Regra #47):
- npm audit executado — 0 vulnerabilities após fix
- vite 7→8.0.16 (breaking upgrade, resolve esbuild CVE GHSA-gv7w-rqvm-qjhr)
- vitest 4.0.18→4.1.9 (resolve esbuild interno CVE GHSA-gv7w-rqvm-qjhr)
- shell-quote override ^1.8.4 via package.json#overrides (CVE GHSA-w7jw-789q-3m8p)
- react-router, joi, qs, form-data, ip-address, js-yaml resolvidos via npm audit fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 19:42:05 +01:00

69 lines
2.3 KiB
TypeScript

/**
* Rota /api/sessions — lista e detalhe de sessões Claude Code.
* Validação Zod em query params; detalhe carrega eventos via parseSessionFile.
* @author Descomplicar® | Projecto Observabilidade (Espelho)
*/
import { Router } from 'express'
import { z } from 'zod'
import type { SessionsDb } from '../services/sessions/db.js'
import { parseSessionFile } from '../services/sessions/parser.js'
const ListQuerySchema = z.object({
days: z.coerce.number().int().min(1).max(3650).optional(),
project: z.string().min(1).max(200).optional(),
tool: z.string().min(1).max(100).optional(),
skill: z.string().min(1).max(200).optional(),
q: z.string().max(500).optional(),
limit: z.coerce.number().int().min(1).max(200).default(50),
offset: z.coerce.number().int().min(0).default(0),
})
const IdParamSchema = z.object({ id: z.string().min(1).max(200) })
export function createSessionsRouter(db: SessionsDb): Router {
const router = Router()
router.get('/', (req, res) => {
const parsed = ListQuerySchema.safeParse(req.query)
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid query', details: parsed.error.format() })
}
const filters = parsed.data
const items = db.listSessions(filters)
const total = db.countSessions(filters)
return res.json({ total, items })
})
router.get('/:id', async (req, res) => {
const parsed = IdParamSchema.safeParse(req.params)
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid id' })
}
const session = db.getSession(parsed.data.id)
if (!session) return res.status(404).json({ error: 'Session not found' })
if (!session.jsonl_path) {
return res.status(422).json({ error: 'Session has no JSONL file (non-Claude source)' })
}
try {
const { events } = await parseSessionFile(session.jsonl_path)
return res.json({ meta: session, events })
} catch (err) {
const e = err as NodeJS.ErrnoException
if (e.code === 'ENOENT') {
return res.status(410).json({
error: 'Session file missing (stale index)',
session_id: parsed.data.id,
})
}
const isProduction = process.env.NODE_ENV === 'production'
return res.status(500).json({
error: 'Failed to parse session',
...(isProduction ? {} : { message: e.message }),
})
}
})
return router
}