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, _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 { 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 } }