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>
209 lines
5.8 KiB
TypeScript
209 lines
5.8 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 OPENCODE_DB = join(homedir(), '.local', 'share', 'opencode', 'opencode.db')
|
|
const DEFAULT_DB_PATH = join(homedir(), '.claude-work', 'sessions.db')
|
|
|
|
export interface OCSessionRow {
|
|
id: string
|
|
project_id: string | null
|
|
title: string | null
|
|
summary_additions: number | null
|
|
summary_deletions: number | null
|
|
summary_files: number | null
|
|
time_created: number | null
|
|
time_updated: number | null
|
|
path: string | null
|
|
}
|
|
|
|
export interface OCMessageRow {
|
|
id: string
|
|
session_id: string
|
|
time_created: number | null
|
|
data: string | null
|
|
}
|
|
|
|
export interface OCPartRow {
|
|
id: string
|
|
message_id: string
|
|
session_id: string
|
|
time_created: number | null
|
|
data: string | null
|
|
}
|
|
|
|
export interface OCProjectRow {
|
|
id: string
|
|
name: string | null
|
|
worktree: string | null
|
|
}
|
|
|
|
/**
|
|
* Lê projectos OpenCode
|
|
*/
|
|
export function readOCProjects(): OCProjectRow[] {
|
|
const db = new Database(OPENCODE_DB, { readonly: true })
|
|
try {
|
|
return db.prepare('SELECT id, name, worktree FROM project').all() as OCProjectRow[]
|
|
} finally {
|
|
db.close()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lê sessões OpenCode
|
|
*/
|
|
export function readOCSessions(): OCSessionRow[] {
|
|
const db = new Database(OPENCODE_DB, { readonly: true })
|
|
try {
|
|
return db.prepare(`
|
|
SELECT s.id, s.project_id, s.title, s.summary_additions, s.summary_deletions,
|
|
s.summary_files, s.time_created, s.time_updated, s.path
|
|
FROM session s
|
|
ORDER BY s.time_created DESC
|
|
`).all() as OCSessionRow[]
|
|
} finally {
|
|
db.close()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lê todas as messages + parts de uma sessão OpenCode
|
|
*/
|
|
export function readOCSessionData(sessionId: string): { messages: OCMessageRow[]; parts: OCPartRow[] } {
|
|
const db = new Database(OPENCODE_DB, { readonly: true })
|
|
try {
|
|
const messages = db.prepare(
|
|
'SELECT id, session_id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC'
|
|
).all(sessionId) as OCMessageRow[]
|
|
|
|
const messageIds = messages.map(m => m.id)
|
|
let parts: OCPartRow[] = []
|
|
if (messageIds.length > 0) {
|
|
const placeholders = messageIds.map(() => '?').join(',')
|
|
parts = db.prepare(
|
|
`SELECT id, message_id, session_id, time_created, data FROM part WHERE message_id IN (${placeholders}) ORDER BY time_created ASC`
|
|
).all(...messageIds) as OCPartRow[]
|
|
}
|
|
|
|
return { messages, parts }
|
|
} finally {
|
|
db.close()
|
|
}
|
|
}
|
|
|
|
function parsePartData(data: string | null): { role?: string; content?: string } {
|
|
if (!data) return {}
|
|
try {
|
|
return JSON.parse(data) as { role?: string; content?: string }
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
function toOCMeta(
|
|
session: OCSessionRow,
|
|
projects: Map<string, OCProjectRow>,
|
|
_messages: OCMessageRow[],
|
|
parts: OCPartRow[],
|
|
): SessionMeta {
|
|
const userMsgs = parts.filter(p => parsePartData(p.data).role === 'user')
|
|
const assistantMsgs = parts.filter(p => parsePartData(p.data).role === 'assistant')
|
|
const project = session.project_id ? projects.get(session.project_id) : undefined
|
|
|
|
const firstUser = userMsgs[0]
|
|
const firstUserContent = firstUser ? parsePartData(firstUser.data).content : null
|
|
|
|
let toolsUsed: string[] = []
|
|
for (const p of parts) {
|
|
const d = parsePartData(p.data)
|
|
if (d.content && (d.content.includes('tool_use') || d.content.includes('tool_result'))) {
|
|
try {
|
|
const parsed = JSON.parse(d.content)
|
|
if (parsed.name) toolsUsed.push(parsed.name)
|
|
} catch { /* não é JSON */ }
|
|
}
|
|
}
|
|
toolsUsed = [...new Set(toolsUsed)]
|
|
|
|
const startTs = session.time_created ? session.time_created / 1000 : Date.now() / 1000
|
|
const endTs = session.time_updated ? session.time_updated / 1000 : startTs
|
|
|
|
return {
|
|
session_id: session.id,
|
|
source: 'opencode',
|
|
project_path: project?.worktree ?? session.path ?? null,
|
|
project_slug: project?.name ?? session.path?.split('/').pop() ?? null,
|
|
jsonl_path: null,
|
|
model: null,
|
|
started_at: new Date(startTs * 1000).toISOString(),
|
|
ended_at: new Date(endTs * 1000).toISOString(),
|
|
duration_sec: Math.round(endTs - startTs),
|
|
event_count: parts.length,
|
|
user_messages: userMsgs.length,
|
|
assistant_msgs: assistantMsgs.length,
|
|
tool_calls: 0,
|
|
input_tokens: null,
|
|
output_tokens: null,
|
|
estimated_cost: null,
|
|
first_prompt: firstUserContent?.slice(0, 500) ?? null,
|
|
tools_used: toolsUsed,
|
|
skills_invoked: [],
|
|
outcome: 'completed',
|
|
permission_mode: null,
|
|
file_size: session.summary_additions ?? 0,
|
|
indexed_at: new Date().toISOString(),
|
|
}
|
|
}
|
|
|
|
export interface IndexResult {
|
|
indexed: number
|
|
failed: number
|
|
}
|
|
|
|
/**
|
|
* Indexa todas as sessões OpenCode para o sessions.db unificado
|
|
*/
|
|
export async function indexOCSessions(
|
|
options: { dbPath?: string } = {},
|
|
): Promise<IndexResult> {
|
|
let indexed = 0
|
|
let failed = 0
|
|
const db = openSessionsDb(options.dbPath ?? DEFAULT_DB_PATH)
|
|
|
|
try {
|
|
const sessions = readOCSessions()
|
|
const projects = readOCProjects()
|
|
const projectMap = new Map(projects.map(p => [p.id, p]))
|
|
let batch: SessionMeta[] = []
|
|
const BATCH = 50
|
|
|
|
for (const session of sessions) {
|
|
try {
|
|
const { messages, parts } = readOCSessionData(session.id)
|
|
const meta = toOCMeta(session, projectMap, messages, parts)
|
|
batch.push(meta)
|
|
if (batch.length >= BATCH) {
|
|
db.upsertMany(batch)
|
|
indexed += batch.length
|
|
batch = []
|
|
}
|
|
} catch (err) {
|
|
failed++
|
|
console.error(`[opencode-indexer] erro em sessão ${session.id}:`, err)
|
|
}
|
|
}
|
|
|
|
if (batch.length > 0) {
|
|
db.upsertMany(batch)
|
|
indexed += batch.length
|
|
}
|
|
} finally {
|
|
db.close()
|
|
}
|
|
|
|
return { indexed, failed }
|
|
}
|