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>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user