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:
2026-06-15 19:41:32 +01:00
parent 9f3d14dc51
commit f733998945
18 changed files with 1475 additions and 678 deletions
+57 -15
View File
@@ -5,6 +5,7 @@ import type { SessionMeta } from '../../types/session.js'
export interface ListFilters {
days?: number
source?: string
project?: string
tool?: string
skill?: string
@@ -73,9 +74,11 @@ export interface SessionsDb {
const SCHEMA = `
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
project_path TEXT NOT NULL,
project_slug TEXT NOT NULL,
jsonl_path TEXT NOT NULL UNIQUE,
source TEXT NOT NULL DEFAULT 'claude',
project_path TEXT,
project_slug TEXT,
jsonl_path TEXT,
model TEXT,
started_at TEXT NOT NULL,
ended_at TEXT,
duration_sec INTEGER,
@@ -83,16 +86,20 @@ CREATE TABLE IF NOT EXISTS sessions (
user_messages INTEGER NOT NULL,
assistant_msgs INTEGER NOT NULL,
tool_calls INTEGER NOT NULL,
input_tokens INTEGER,
output_tokens INTEGER,
estimated_cost REAL,
first_prompt TEXT,
tools_used TEXT NOT NULL,
skills_invoked TEXT NOT NULL,
tools_used TEXT NOT NULL DEFAULT '[]',
skills_invoked TEXT NOT NULL DEFAULT '[]',
outcome TEXT NOT NULL,
permission_mode TEXT,
file_size INTEGER NOT NULL,
file_size INTEGER NOT NULL DEFAULT 0,
indexed_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_started ON sessions(started_at DESC);
CREATE INDEX IF NOT EXISTS idx_project ON sessions(project_slug, started_at DESC);
CREATE INDEX IF NOT EXISTS idx_source ON sessions(source);
CREATE TABLE IF NOT EXISTS patterns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -134,9 +141,11 @@ CREATE INDEX IF NOT EXISTS idx_wc_task ON worklog_comments(task_ref);
function rowToMeta(row: Record<string, unknown>): SessionMeta {
return {
session_id: row.session_id as string,
project_path: row.project_path as string,
project_slug: row.project_slug as string,
jsonl_path: row.jsonl_path as string,
source: (row.source as SessionMeta['source']) ?? 'claude',
project_path: (row.project_path as string | null) ?? null,
project_slug: (row.project_slug as string | null) ?? null,
jsonl_path: (row.jsonl_path as string | null) ?? null,
model: (row.model as string | null) ?? null,
started_at: row.started_at as string,
ended_at: (row.ended_at as string | null) ?? null,
duration_sec: (row.duration_sec as number | null) ?? null,
@@ -144,9 +153,12 @@ function rowToMeta(row: Record<string, unknown>): SessionMeta {
user_messages: row.user_messages as number,
assistant_msgs: row.assistant_msgs as number,
tool_calls: row.tool_calls as number,
input_tokens: (row.input_tokens as number | null) ?? null,
output_tokens: (row.output_tokens as number | null) ?? null,
estimated_cost: (row.estimated_cost as number | null) ?? null,
first_prompt: (row.first_prompt as string | null) ?? null,
tools_used: JSON.parse(row.tools_used as string),
skills_invoked: JSON.parse(row.skills_invoked as string),
tools_used: row.tools_used ? JSON.parse(row.tools_used as string) : [],
skills_invoked: row.skills_invoked ? JSON.parse(row.skills_invoked as string) : [],
outcome: row.outcome as SessionMeta['outcome'],
permission_mode: (row.permission_mode as string | null) ?? null,
file_size: row.file_size as number,
@@ -162,6 +174,10 @@ function buildWhere(f: ListFilters): { sql: string; params: Record<string, unkno
parts.push('started_at >= @cutoff')
params.cutoff = cutoff
}
if (f.source) {
parts.push('source = @source')
params.source = f.source
}
if (f.project) {
parts.push('project_slug = @project')
params.project = f.project
@@ -191,17 +207,40 @@ export function openSessionsDb(dbPath: string): SessionsDb {
db.pragma('synchronous = NORMAL')
db.exec(SCHEMA)
// Migração: adicionar colunas novas se não existirem
const existingCols = db.prepare("PRAGMA table_info(sessions)").all() as { name: string }[]
const colNames = new Set(existingCols.map(c => c.name))
const migrations: [string, string][] = [
['source', "TEXT NOT NULL DEFAULT 'claude'"],
['model', 'TEXT'],
['input_tokens', 'INTEGER'],
['output_tokens', 'INTEGER'],
['estimated_cost', 'REAL'],
]
for (const [col, type] of migrations) {
if (!colNames.has(col)) {
db.exec(`ALTER TABLE sessions ADD COLUMN ${col} ${type}`)
}
}
if (!colNames.has('source')) {
db.exec('CREATE INDEX IF NOT EXISTS idx_source ON sessions(source)')
}
const upsertStmt = db.prepare(`
INSERT INTO sessions (session_id, project_path, project_slug, jsonl_path, started_at, ended_at,
duration_sec, event_count, user_messages, assistant_msgs, tool_calls, first_prompt,
INSERT INTO sessions (session_id, source, project_path, project_slug, jsonl_path, model,
started_at, ended_at, duration_sec, event_count, user_messages, assistant_msgs, tool_calls,
input_tokens, output_tokens, estimated_cost, first_prompt,
tools_used, skills_invoked, outcome, permission_mode, file_size, indexed_at)
VALUES (@session_id, @project_path, @project_slug, @jsonl_path, @started_at, @ended_at,
@duration_sec, @event_count, @user_messages, @assistant_msgs, @tool_calls, @first_prompt,
VALUES (@session_id, @source, @project_path, @project_slug, @jsonl_path, @model,
@started_at, @ended_at, @duration_sec, @event_count, @user_messages, @assistant_msgs, @tool_calls,
@input_tokens, @output_tokens, @estimated_cost, @first_prompt,
@tools_used, @skills_invoked, @outcome, @permission_mode, @file_size, @indexed_at)
ON CONFLICT(session_id) DO UPDATE SET
source = excluded.source,
project_path = excluded.project_path,
project_slug = excluded.project_slug,
jsonl_path = excluded.jsonl_path,
model = excluded.model,
started_at = excluded.started_at,
ended_at = excluded.ended_at,
duration_sec = excluded.duration_sec,
@@ -209,6 +248,9 @@ export function openSessionsDb(dbPath: string): SessionsDb {
user_messages = excluded.user_messages,
assistant_msgs = excluded.assistant_msgs,
tool_calls = excluded.tool_calls,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
estimated_cost = excluded.estimated_cost,
first_prompt = excluded.first_prompt,
tools_used = excluded.tools_used,
skills_invoked = excluded.skills_invoked,