f733998945
- 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>
152 lines
4.3 KiB
TypeScript
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 }
|
|
}
|