import { readdirSync, statSync } from 'fs' import { homedir } from 'os' import { join } from 'path' import { parseSessionFile } from './parser.js' import { openSessionsDb, type SessionsDb } from './db.js' import { indexHermesSessions } from './hermes-indexer.js' import { indexOCSessions } from './opencode-indexer.js' import type { SessionMeta } from '../../types/session.js' export const PROJECTS_ROOT = join(homedir(), '.claude', 'projects') export const DEFAULT_DB_PATH = join(homedir(), '.claude-work', 'sessions.db') /** * Percorre a raiz de projectos Claude (profundidade 2) e devolve todos os .jsonl. * Estrutura: ~/.claude/projects//.jsonl */ export function findAllJsonl(root: string = PROJECTS_ROOT): string[] { const result: string[] = [] let entries: string[] try { entries = readdirSync(root) } catch { return result } for (const entry of entries) { const projectDir = join(root, entry) let st try { st = statSync(projectDir) } catch { continue } if (!st.isDirectory()) continue let files: string[] try { files = readdirSync(projectDir) } catch { continue } for (const f of files) { if (f.endsWith('.jsonl')) result.push(join(projectDir, f)) } } return result } /** * Indexa um único ficheiro (parse + upsert). Uso individual — útil para o watcher (Task 8). */ export async function indexFile(db: SessionsDb, path: string): Promise { const { meta } = await parseSessionFile(path) db.upsertSession(meta) } export interface IndexAllOptions { dbPath?: string sources?: ('claude' | 'hermes' | 'opencode')[] onProgress?: (done: number, total: number) => void } /** * Full scan: indexa Claude Code (JSONL) + Hermes (state.db) + OpenCode (opencode.db). */ export async function indexAll( options: IndexAllOptions = {}, ): Promise<{ indexed: number; failed: number }> { const sources = options.sources ?? ['claude', 'hermes', 'opencode'] let indexed = 0 let failed = 0 const dbPath = options.dbPath ?? DEFAULT_DB_PATH if (sources.includes('claude')) { const db = openSessionsDb(dbPath) const files = findAllJsonl() const BATCH = 50 let batch: SessionMeta[] = [] try { for (let i = 0; i < files.length; i++) { try { const { meta } = await parseSessionFile(files[i]) meta.source = 'claude' meta.model = meta.model ?? null meta.ended_at = meta.ended_at ?? null meta.duration_sec = meta.duration_sec ?? null meta.input_tokens = meta.input_tokens ?? null meta.output_tokens = meta.output_tokens ?? null meta.estimated_cost = meta.estimated_cost ?? null meta.first_prompt = meta.first_prompt ?? null meta.permission_mode = meta.permission_mode ?? null meta.project_path = meta.project_path ?? null meta.project_slug = meta.project_slug ?? null meta.jsonl_path = meta.jsonl_path ?? null batch.push(meta) if (batch.length >= BATCH) { db.upsertMany(batch) indexed += batch.length batch = [] } } catch (err) { failed++ console.error(`[indexer] erro em ${files[i]}:`, err) } if (options.onProgress) { options.onProgress(indexed + failed + batch.length, files.length) } } if (batch.length > 0) { db.upsertMany(batch) indexed += batch.length } } finally { db.close() } } if (sources.includes('hermes')) { try { const result = await indexHermesSessions({ dbPath }) indexed += result.indexed failed += result.failed console.log(`[indexer] Hermes: ${result.indexed} indexadas, ${result.failed} falhas`) } catch (err) { failed++ console.error('[indexer] Erro ao indexar Hermes:', err) } } if (sources.includes('opencode')) { try { const result = await indexOCSessions({ dbPath }) indexed += result.indexed failed += result.failed console.log(`[indexer] OpenCode: ${result.indexed} indexadas, ${result.failed} falhas`) } catch (err) { failed++ console.error('[indexer] Erro ao indexar OpenCode:', err) } } return { indexed, failed } }