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:
+57
-15
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user