Files
DashDescomplicar/api/services/sessions/hermes-indexer.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

152 lines
4.3 KiB
TypeScript

import Database from 'better-sqlite3'
import { homedir } from 'os'
import { join } from 'path'
import type { SessionMeta } from '../../types/session.js'
import { openSessionsDb } from './db.js'
const HERMES_DB = join(homedir(), '.hermes', 'state.db')
const DEFAULT_DB_PATH = join(homedir(), '.claude-work', 'sessions.db')
export interface HermesSessionRow {
id: string
model: string | null
started_at: number | null
ended_at: number | null
end_reason: string | null
message_count: number
tool_call_count: number
input_tokens: number | null
output_tokens: number | null
estimated_cost_usd: number | null
title: string | null
api_call_count: number | null
}
export interface HermesMessageRow {
session_id: string
role: string
content: string | null
tool_name: string | null
timestamp: number | null
}
/**
* Lê sessões do state.db do Hermes Agent
*/
export function readHermesSessions(): HermesSessionRow[] {
const db = new Database(HERMES_DB, { readonly: true })
try {
return db.prepare(`
SELECT id, model, started_at, ended_at, end_reason,
message_count, tool_call_count,
input_tokens, output_tokens, estimated_cost_usd,
title, api_call_count
FROM sessions
ORDER BY started_at DESC
`).all() as HermesSessionRow[]
} finally {
db.close()
}
}
/**
* Lê mensagens de uma sessão Hermes
*/
export function readHermesMessages(sessionId: string): HermesMessageRow[] {
const db = new Database(HERMES_DB, { readonly: true })
try {
return db.prepare(`
SELECT session_id, role, content, tool_name, timestamp
FROM messages
WHERE session_id = ?
ORDER BY timestamp ASC
`).all(sessionId) as HermesMessageRow[]
} finally {
db.close()
}
}
function toHermesMeta(row: HermesSessionRow, messages: HermesMessageRow[]): SessionMeta {
const userMsgs = messages.filter(m => m.role === 'user')
const assistantMsgs = messages.filter(m => m.role === 'assistant')
const toolMsgs = messages.filter(m => m.role === 'tool')
const firstUser = userMsgs[0]
const toolsUsed = [...new Set(messages.filter(m => m.tool_name).map(m => m.tool_name!))]
return {
session_id: row.id,
source: 'hermes',
project_path: null,
project_slug: null,
jsonl_path: null,
model: row.model ?? null,
started_at: row.started_at ? new Date(row.started_at * 1000).toISOString() : new Date(0).toISOString(),
ended_at: row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null,
duration_sec: row.started_at && row.ended_at ? Math.round(row.ended_at - row.started_at) : null,
event_count: messages.length,
user_messages: userMsgs.length,
assistant_msgs: assistantMsgs.length,
tool_calls: toolMsgs.length,
input_tokens: row.input_tokens ?? null,
output_tokens: row.output_tokens ?? null,
estimated_cost: row.estimated_cost_usd ?? null,
first_prompt: firstUser?.content?.slice(0, 500) ?? null,
tools_used: toolsUsed,
skills_invoked: [],
outcome: row.end_reason === 'completed' ? 'completed'
: row.end_reason === 'interrupted' || row.end_reason === 'stopped' ? 'interrupted'
: row.end_reason === 'error' ? 'error'
: 'unknown',
permission_mode: null,
file_size: 0,
indexed_at: new Date().toISOString(),
}
}
export interface IndexResult {
indexed: number
failed: number
}
/**
* Indexa todas as sessões Hermes no state.db para o sessions.db unificado
*/
export async function indexHermesSessions(
options: { dbPath?: string } = {},
): Promise<IndexResult> {
let indexed = 0
let failed = 0
const db = openSessionsDb(options.dbPath ?? DEFAULT_DB_PATH)
try {
const sessions = readHermesSessions()
let batch: SessionMeta[] = []
const BATCH = 50
for (const session of sessions) {
try {
const messages = readHermesMessages(session.id)
const meta = toHermesMeta(session, messages)
batch.push(meta)
if (batch.length >= BATCH) {
db.upsertMany(batch)
indexed += batch.length
batch = []
}
} catch (err) {
failed++
console.error(`[hermes-indexer] erro em sessão ${session.id}:`, err)
}
}
if (batch.length > 0) {
db.upsertMany(batch)
indexed += batch.length
}
} finally {
db.close()
}
return { indexed, failed }
}