#!/usr/bin/env node /** * qmem-sqlite — CLI per l'indice locale di pi-qmem (SQLite + FTS5). * * Uso: * node scripts/qmem-sqlite.mjs status * node scripts/qmem-sqlite.mjs import [--db FILE] * node scripts/qmem-sqlite.mjs find "query" [--kind K] [--project P] [--scope S] * [--top N] [--all] [--private] [--exact] [--json] * node scripts/qmem-sqlite.mjs enrich [--all] [--limit N] [--pace MS] * node scripts/qmem-sqlite.mjs pull [--limit N] * * Il DB di default è ~/.local/share/pi-qmem/qmem.sqlite (override: --db, * env QMEM_SQLITE, oppure `localDbPath` in ~/.config/pi-qmem/config.json). */ // import dinamico: permette di sopprimere il warning MODULE_TYPELESS_PACKAGE_JSON // (il package non dichiara "type":"module" per non cambiare la semantica del // manifest pi) e di degradare con grazia se node:sqlite non è disponibile. const originalEmitWarning = process.emitWarning; process.emitWarning = (warning, ...rest) => { const code = rest[0]?.code ?? (typeof rest[0] === "string" ? rest[0] : undefined) ?? warning?.code; if (code === "MODULE_TYPELESS_PACKAGE_JSON") return; return originalEmitWarning.call(process, warning, ...rest); }; const localDb = await import("../extensions/local-db.ts"); const { DEFAULT_DB_FILE, enrichFromGateway, importFromSessions, localDbPath, localDbReport, localSearch, pullFromGatewayExport, sessionRoots } = localDb; const { loadConfig } = await import("../extensions/shared.ts"); process.emitWarning = originalEmitWarning; const argv = process.argv.slice(2); const cmd = argv[0] ?? "status"; function opt(name, fallback) { const i = argv.indexOf(`--${name}`); if (i === -1) return fallback; const v = argv[i + 1]; return v && !v.startsWith("--") ? v : true; } const has = (name) => argv.includes(`--${name}`); const dbFile = typeof opt("db", null) === "string" ? opt("db", null) : localDbPath(loadConfig()); const json = has("json"); function out(obj) { if (json) console.log(JSON.stringify(obj, null, 2)); else console.log(obj); } if (cmd === "status") { const r = await localDbReport({ dbFile }); if (json) { out(r); } else { console.log(`DB locale : ${r.path}${r.exists ? "" : " (assente — esegui: import)"}`); if (r.exists) { console.log(` dimensione : ${r.sizeKb} KB`); console.log(` record : ${r.total} (${r.withText} con testo, ${r.active} attivi, ${r.superseded} superseduti, ${r.private} privati)`); console.log(` con project_id: ${r.withProject}/${r.total}`); console.log(` ultimo import : ${r.lastImport ?? "-"} enrich: ${r.lastEnrich ?? "-"} export: ${r.lastExport ?? "-"}`); console.log(` top progetti : ${r.topProjects.map((p) => `${p.project_id ?? "(null)"}=${p.n}`).join(", ") || "-"}`); if (r.duplicates.length) { console.log(` possibili duplicati (testo identico): ${r.duplicates.length} gruppi — es. ${r.duplicates[0].ids.map((i) => i.slice(0, 8)).join(", ")}`); } } } } else if (cmd === "import") { const roots = sessionRoots(); const stats = await importFromSessions({ dbFile }); const r = await localDbReport({ dbFile }); if (json) out({ stats, report: r }); else { console.log(`Import dalle sessioni (${roots.join(", ")})`); console.log(` file letti : ${stats.files} (${stats.lines} righe)`); console.log(` eventi : store=${stats.store} correct=${stats.correct} get=${stats.get} search_hit=${stats.searchHits} non_interpretati=${stats.unparsed}`); console.log(` record unici : ${stats.records} (scritti/aggiornati: ${stats.written})`); console.log(` DB : ${r.path} — ${r.total} record, ${r.sizeKb} KB, ${r.withText} con testo`); if (r.duplicates.length) console.log(` duplicati : ${r.duplicates.length} gruppi con testo identico`); } } else if (cmd === "find") { const query = argv[1] && !argv[1].startsWith("--") ? argv[1] : ""; if (!query) { console.error('Uso: find "query" [--kind K] [--project P] [--top N] [--all] [--exact] [--json]'); process.exit(2); } const hits = await localSearch( { query, kind: typeof opt("kind", null) === "string" ? opt("kind", null) : undefined, project_id: typeof opt("project", null) === "string" ? opt("project", null) : undefined, scope: typeof opt("scope", null) === "string" ? opt("scope", null) : undefined, level: typeof opt("level", null) === "string" ? opt("level", null) : undefined, topic: typeof opt("topic", null) === "string" ? opt("topic", null) : undefined, include_superseded: has("all"), include_private: has("private"), exact: has("exact"), top_k: Number(opt("top", 5)) || 5, }, { dbFile }, ); if (json) { out(hits); } else { if (!hits.length) console.log(`Nessun risultato locale per "${query}" (indice: ${dbFile}).`); hits.forEach((h, i) => { console.log(`${i + 1}. [${h.kind ?? "?"}/${h.scope ?? "?"}${h.project_id ? ` project=${h.project_id}` : ""} rank=${Number(h.rank).toFixed(2)}${h.superseded_by ? " superseduto" : ""}] ${h.snippet}`); console.log(` (id: ${h.memory_id}, creato: ${h.created_at ?? "?"}, agente: ${h.agent_id ?? "?"}, fonti: ${h.sources ?? "?"})`); }); } } else if (cmd === "enrich") { const cfg = loadConfig(); const stats = await enrichFromGateway(cfg, { dbFile, onlyIncomplete: !has("all"), limit: Number(opt("limit", 1000)) || 1000, paceMs: Number(opt("pace", 600)), }); if (json) out(stats); else { console.log(`Arricchimento dal gateway (${cfg.url})`); console.log(` richiesti: ${stats.requested} ok: ${stats.ok} falliti: ${stats.failed} aggiornati: ${stats.updated}`); if (stats.errors.length) console.log(` errori : ${stats.errors.join(" | ")}`); if (stats.failed && !stats.ok) console.log(" (gateway non raggiungibile: riprova quando torna online)"); } } else if (cmd === "pull") { const cfg = loadConfig(); const res = await pullFromGatewayExport(cfg, { dbFile, limit: Number(opt("limit", 500)) || 500 }); if (json) out(res); else { console.log(`Pull export dal gateway (${cfg.url})`); console.log(` supportato: ${res.supported} pagine: ${res.pages} record: ${res.fetched}${res.message ? ` — ${res.message}` : ""}`); } } else { console.error(`Comando sconosciuto: ${cmd}\nComandi: status | import | find | enrich | pull`); process.exit(2); } void DEFAULT_DB_FILE;