@fml-inc/panopticon 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -29
- package/bin/hook-handler +0 -0
- package/bin/mcp-server +0 -0
- package/bin/proxy +0 -0
- package/bin/server +0 -0
- package/dist/api/client.d.ts +2 -2
- package/dist/api/client.js +1 -1
- package/dist/{chunk-3BUJ7URA.js → chunk-3ILOOWUF.js} +66 -2
- package/dist/chunk-3ILOOWUF.js.map +1 -0
- package/dist/{chunk-HQCY722C.js → chunk-3ZT3V7FP.js} +5 -5
- package/dist/{chunk-WLBNFVIG.js → chunk-BKGQJ76N.js} +47 -19
- package/dist/chunk-BKGQJ76N.js.map +1 -0
- package/dist/{chunk-3TZAKV3M.js → chunk-FMAHQRIU.js} +2 -2
- package/dist/{chunk-LWXF7YRG.js → chunk-GPTBERQD.js} +2 -2
- package/dist/{chunk-4SM2H22C.js → chunk-HO443ZQM.js} +1 -1
- package/dist/{chunk-4SM2H22C.js.map → chunk-HO443ZQM.js.map} +1 -1
- package/dist/{chunk-L7G27XWF.js → chunk-HRNZUHTA.js} +3 -3
- package/dist/{chunk-CF4GPWLI.js → chunk-J3HVD4VI.js} +2 -2
- package/dist/{chunk-SEXU2WYG.js → chunk-MEVW27U4.js} +5 -4
- package/dist/chunk-MEVW27U4.js.map +1 -0
- package/dist/{chunk-SUGSQ4YI.js → chunk-N7NCNJZU.js} +4 -4
- package/dist/{chunk-RX2RXHBH.js → chunk-NE7VBLQD.js} +6 -5
- package/dist/{chunk-RX2RXHBH.js.map → chunk-NE7VBLQD.js.map} +1 -1
- package/dist/{chunk-NXH7AONS.js → chunk-OROLSIWZ.js} +8 -6
- package/dist/chunk-OROLSIWZ.js.map +1 -0
- package/dist/{chunk-XLTCUH5A.js → chunk-OW52TNVA.js} +4 -4
- package/dist/{chunk-DZ5HJFB4.js → chunk-SKZHAYNF.js} +53 -2
- package/dist/chunk-SKZHAYNF.js.map +1 -0
- package/dist/{chunk-BVOE7A2Z.js → chunk-V3XR2TAN.js} +8 -6
- package/dist/chunk-V3XR2TAN.js.map +1 -0
- package/dist/{chunk-HRCEIYKU.js → chunk-WXPT6KG7.js} +2 -2
- package/dist/cli.js +6 -6
- package/dist/cli.js.map +1 -1
- package/dist/db.js +1 -1
- package/dist/doctor.js +4 -4
- package/dist/hooks/handler.js +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -15
- package/dist/mcp/server.js +1 -1
- package/dist/otlp/server.js +5 -5
- package/dist/pricing.js +2 -2
- package/dist/proxy/server.js +5 -5
- package/dist/prune.js +2 -2
- package/dist/query.js +2 -2
- package/dist/{reparse-636YZCE3.js → reparse-VHUSGCPN.js} +5 -5
- package/dist/scanner.d.ts +7 -1
- package/dist/scanner.js +1 -1
- package/dist/server.js +13 -13
- package/dist/setup.js +3 -3
- package/dist/sync/index.d.ts +2 -2
- package/dist/sync/index.js +4 -4
- package/dist/{types-D-MYCBol.d.ts → types-DrhrWbWe.d.ts} +1 -0
- package/package.json +21 -13
- package/dist/chunk-3BUJ7URA.js.map +0 -1
- package/dist/chunk-BVOE7A2Z.js.map +0 -1
- package/dist/chunk-DZ5HJFB4.js.map +0 -1
- package/dist/chunk-NXH7AONS.js.map +0 -1
- package/dist/chunk-SEXU2WYG.js.map +0 -1
- package/dist/chunk-WLBNFVIG.js.map +0 -1
- /package/dist/{chunk-HQCY722C.js.map → chunk-3ZT3V7FP.js.map} +0 -0
- /package/dist/{chunk-3TZAKV3M.js.map → chunk-FMAHQRIU.js.map} +0 -0
- /package/dist/{chunk-LWXF7YRG.js.map → chunk-GPTBERQD.js.map} +0 -0
- /package/dist/{chunk-L7G27XWF.js.map → chunk-HRNZUHTA.js.map} +0 -0
- /package/dist/{chunk-CF4GPWLI.js.map → chunk-J3HVD4VI.js.map} +0 -0
- /package/dist/{chunk-SUGSQ4YI.js.map → chunk-N7NCNJZU.js.map} +0 -0
- /package/dist/{chunk-XLTCUH5A.js.map → chunk-OW52TNVA.js.map} +0 -0
- /package/dist/{chunk-HRCEIYKU.js.map → chunk-WXPT6KG7.js.map} +0 -0
- /package/dist/{reparse-636YZCE3.js.map → reparse-VHUSGCPN.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scanner/reparse.ts","../src/scanner/loop.ts","../src/archive/local.ts","../src/archive/index.ts","../src/summary/llm.ts","../src/summary/loop.ts","../src/scanner/store.ts"],"sourcesContent":["/**\n * Atomic DB reparse: builds a fresh database from scratch, copies\n * non-scanner metadata from the old DB, then swaps files atomically.\n *\n * This avoids the cost of row-by-row deletes on large databases and\n * ensures that parser changes (tracked via SCANNER_DATA_VERSION) are\n * applied cleanly to all existing session data.\n *\n * Preserves sync_id values for rows that match on natural keys so\n * that remote sync targets can deduplicate correctly.\n */\nimport fs from \"node:fs\";\nimport { gunzipSync } from \"node:zlib\";\nimport Database from \"better-sqlite3\";\nimport { config } from \"../config.js\";\nimport {\n closeDb,\n getDb,\n runMigrations,\n SCANNER_DATA_VERSION,\n SCHEMA_SQL,\n} from \"../db/schema.js\";\nimport { scanOnce } from \"./loop.js\";\n\n/**\n * Tables whose data is independent of the scanner and must be\n * preserved across reparse. Copied row-by-row from old → new DB.\n */\nconst PRESERVED_TABLES = [\n \"hook_events\",\n \"otel_logs\",\n \"otel_metrics\",\n \"otel_spans\",\n \"watermarks\",\n \"target_session_sync\",\n \"model_pricing\",\n \"user_config_snapshots\",\n \"repo_config_snapshots\",\n];\n\n/**\n * Session columns that come from non-scanner sources (hooks, OTLP)\n * and should be merged back after the scanner rebuilds sessions.\n */\nconst SESSION_MERGE_COLUMNS = [\n \"has_hooks\",\n \"has_otel\",\n \"otel_input_tokens\",\n \"otel_output_tokens\",\n \"otel_cache_read_tokens\",\n \"otel_cache_creation_tokens\",\n \"summary\",\n \"summary_version\",\n \"permission_mode\",\n \"is_automated\",\n \"created_at\",\n];\n\nexport interface ReparseResult {\n success: boolean;\n filesScanned: number;\n newTurns: number;\n error?: string;\n}\n\nfunction removeTempFiles(tempPath: string): void {\n for (const suffix of [\"\", \"-wal\", \"-shm\"]) {\n try {\n fs.unlinkSync(tempPath + suffix);\n } catch {}\n }\n}\n\nfunction removeWAL(dbPath: string): void {\n for (const suffix of [\"-wal\", \"-shm\"]) {\n try {\n fs.unlinkSync(dbPath + suffix);\n } catch {}\n }\n}\n\nfunction initTempDb(tempPath: string): Database.Database {\n const db = new Database(tempPath);\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"busy_timeout = 5000\");\n db.function(\"decompress\", (blob: Buffer | null) =>\n blob ? gunzipSync(blob).toString() : null,\n );\n db.exec(SCHEMA_SQL);\n runMigrations(db);\n return db;\n}\n\n/**\n * Perform an atomic reparse:\n * 1. Close current DB\n * 2. Create a fresh temp DB with current schema\n * 3. Redirect config.dbPath → temp, run full scan into it\n * 4. Copy preserved (non-scanner) data from old DB via ATTACH\n * 5. Merge session metadata from hooks/OTLP\n * 6. Preserve sync_id values for rows matching on natural keys\n * 7. Atomic file swap (rename)\n * 8. Reopen the main DB handle\n */\nexport function reparseAll(\n log: (msg: string) => void = () => {},\n): ReparseResult {\n const origPath = config.dbPath;\n const tempPath = `${origPath}-reparse`;\n\n // Clean up stale temp DB from a prior crash\n removeTempFiles(tempPath);\n\n log(\"Starting atomic reparse...\");\n\n // 1. Create fresh temp DB and verify schema\n let tempDb: Database.Database;\n try {\n tempDb = initTempDb(tempPath);\n tempDb.close();\n } catch (err) {\n removeTempFiles(tempPath);\n return {\n success: false,\n filesScanned: 0,\n newTurns: 0,\n error: `Failed to create temp DB: ${err}`,\n };\n }\n\n // Snapshot old session count for safety check\n let oldSessionCount = 0;\n try {\n const oldDb = new Database(origPath);\n oldSessionCount = (\n oldDb.prepare(\"SELECT COUNT(*) as c FROM sessions\").get() as {\n c: number;\n }\n ).c;\n oldDb.close();\n } catch {}\n\n // 2. Close current DB, redirect to temp, scan\n closeDb();\n const savedDbPath = config.dbPath;\n (config as { dbPath: string }).dbPath = tempPath;\n\n let filesScanned = 0;\n let newTurns = 0;\n try {\n const result = scanOnce();\n filesScanned = result.filesScanned;\n newTurns = result.newTurns;\n } catch (err) {\n (config as { dbPath: string }).dbPath = savedDbPath;\n closeDb();\n getDb(); // reopen original\n removeTempFiles(tempPath);\n return {\n success: false,\n filesScanned: 0,\n newTurns: 0,\n error: `Scan into temp DB failed: ${err}`,\n };\n }\n\n // Check session count in temp DB\n const db = getDb();\n const tempSessionCount = (\n db.prepare(\"SELECT COUNT(*) as c FROM sessions\").get() as { c: number }\n ).c;\n closeDb();\n\n // Restore config path for all subsequent operations\n (config as { dbPath: string }).dbPath = savedDbPath;\n\n // Abort if scan produced nothing but old DB had data\n if (tempSessionCount === 0 && oldSessionCount > 0) {\n log(\n `Reparse aborted: temp DB has 0 sessions but old DB has ${oldSessionCount}`,\n );\n getDb(); // reopen original\n removeTempFiles(tempPath);\n return {\n success: false,\n filesScanned,\n newTurns,\n error: `Aborted: 0 sessions in reparse vs ${oldSessionCount} in old DB`,\n };\n }\n\n // 3. Copy preserved data from old DB into temp DB\n log(\"Copying preserved data from old database...\");\n try {\n tempDb = new Database(tempPath);\n tempDb.pragma(\"journal_mode = WAL\");\n tempDb.function(\"decompress\", (blob: Buffer | null) =>\n blob ? gunzipSync(blob).toString() : null,\n );\n const escapedPath = origPath.replace(/'/g, \"''\");\n tempDb.exec(`ATTACH DATABASE '${escapedPath}' AS old_db`);\n\n const tx = tempDb.transaction(() => {\n for (const table of PRESERVED_TABLES) {\n try {\n tempDb.exec(\n `INSERT OR IGNORE INTO main.${table} SELECT * FROM old_db.${table}`,\n );\n } catch (e) {\n log(` Skipping ${table}: ${e instanceof Error ? e.message : e}`);\n }\n }\n\n // Rebuild hook_events_fts from copied hook_events\n try {\n tempDb.exec(\n \"INSERT INTO main.hook_events_fts(rowid, payload) SELECT id, decompress(payload) FROM main.hook_events\",\n );\n } catch (e) {\n log(` hook_events_fts rebuild: ${e instanceof Error ? e.message : e}`);\n }\n\n // Merge session metadata from hooks/OTLP into scanner-created sessions\n const setClauses = SESSION_MERGE_COLUMNS.map(\n (col) => `${col} = old_db.sessions.${col}`,\n ).join(\", \");\n try {\n tempDb.exec(`\n UPDATE main.sessions SET ${setClauses}\n FROM old_db.sessions\n WHERE main.sessions.session_id = old_db.sessions.session_id\n `);\n } catch (e) {\n log(` Session merge: ${e instanceof Error ? e.message : e}`);\n }\n\n // Copy session_repositories and session_cwds\n try {\n tempDb.exec(\n \"INSERT OR IGNORE INTO main.session_repositories SELECT * FROM old_db.session_repositories\",\n );\n } catch (e) {\n log(` session_repositories: ${e instanceof Error ? e.message : e}`);\n }\n try {\n tempDb.exec(\n \"INSERT OR IGNORE INTO main.session_cwds SELECT * FROM old_db.session_cwds\",\n );\n } catch (e) {\n log(` session_cwds: ${e instanceof Error ? e.message : e}`);\n }\n\n // Preserve sync_id values from old DB for scanner-produced tables\n // by matching on natural keys\n try {\n tempDb.exec(`\n UPDATE main.scanner_turns SET sync_id = old_db.scanner_turns.sync_id\n FROM old_db.scanner_turns\n WHERE main.scanner_turns.session_id = old_db.scanner_turns.session_id\n AND main.scanner_turns.source = old_db.scanner_turns.source\n AND main.scanner_turns.turn_index = old_db.scanner_turns.turn_index\n `);\n } catch (e) {\n log(` scanner_turns sync_id: ${e instanceof Error ? e.message : e}`);\n }\n\n try {\n tempDb.exec(`\n UPDATE main.scanner_events SET sync_id = old_db.scanner_events.sync_id\n FROM old_db.scanner_events\n WHERE main.scanner_events.session_id = old_db.scanner_events.session_id\n AND main.scanner_events.source = old_db.scanner_events.source\n AND main.scanner_events.event_type = old_db.scanner_events.event_type\n AND main.scanner_events.timestamp_ms = old_db.scanner_events.timestamp_ms\n AND COALESCE(main.scanner_events.tool_name, '') = COALESCE(old_db.scanner_events.tool_name, '')\n `);\n } catch (e) {\n log(` scanner_events sync_id: ${e instanceof Error ? e.message : e}`);\n }\n\n // Preserve sync_id for tool_calls by matching via message natural key + tool_use_id\n try {\n tempDb.exec(`\n UPDATE main.tool_calls SET sync_id = old_tc.sync_id\n FROM old_db.tool_calls old_tc\n INNER JOIN old_db.messages old_m ON old_tc.message_id = old_m.id\n INNER JOIN main.messages new_m ON new_m.session_id = old_m.session_id AND new_m.ordinal = old_m.ordinal\n WHERE main.tool_calls.message_id = new_m.id\n AND COALESCE(main.tool_calls.tool_use_id, '') = COALESCE(old_tc.tool_use_id, '')\n AND main.tool_calls.tool_name = old_tc.tool_name\n `);\n } catch (e) {\n log(` tool_calls sync_id: ${e instanceof Error ? e.message : e}`);\n }\n });\n tx();\n\n tempDb.exec(\"DETACH DATABASE old_db\");\n tempDb.pragma(`user_version = ${SCANNER_DATA_VERSION}`);\n tempDb.close();\n } catch (err) {\n log(`Failed to copy preserved data: ${err}`);\n getDb(); // reopen original\n removeTempFiles(tempPath);\n return {\n success: false,\n filesScanned,\n newTurns,\n error: `Copy preserved data failed: ${err}`,\n };\n }\n\n // 4. Atomic file swap\n log(\"Swapping database files...\");\n try {\n removeWAL(origPath);\n fs.renameSync(tempPath, origPath);\n removeWAL(tempPath);\n } catch (err) {\n log(`File swap failed: ${err}`);\n getDb();\n removeTempFiles(tempPath);\n return {\n success: false,\n filesScanned,\n newTurns,\n error: `Atomic swap failed: ${err}`,\n };\n }\n\n // 5. Reopen the main DB handle\n getDb();\n\n log(\n `Reparse complete: ${filesScanned} files, ${newTurns} turns, ${tempSessionCount} sessions`,\n );\n\n return { success: true, filesScanned, newTurns };\n}\n","import fs from \"node:fs\";\nimport type Database from \"better-sqlite3\";\nimport { getDb, markResyncComplete, needsResync } from \"../db/schema.js\";\nimport { updateSessionMessageCounts } from \"../db/store.js\";\n// Import targets so they self-register before we iterate the registry\nimport \"../targets/claude.js\";\nimport \"../targets/codex.js\";\nimport \"../targets/gemini.js\";\nimport { getArchiveBackend } from \"../archive/index.js\";\nimport { log } from \"../log.js\";\nimport { generateSummariesOnce } from \"../summary/index.js\";\nimport { allTargets } from \"../targets/registry.js\";\nimport type { ParseResult } from \"../targets/types.js\";\nimport type { SavedSyncIds } from \"./store.js\";\nimport {\n getMaxOrdinal,\n getTurnCount,\n insertMessages,\n insertScannerEvents,\n insertTurns,\n linkSubagentSessions,\n readArchivedSize,\n readFileWatermark,\n resetFileForReparse,\n restoreSyncIds,\n updateSessionTotals,\n upsertSession,\n writeArchivedSize,\n writeFileWatermark,\n} from \"./store.js\";\nimport type { ScannerHandle, ScannerOptions } from \"./types.js\";\n\nconst DEFAULT_IDLE_MS = 60_000;\nconst DEFAULT_CATCHUP_MS = 5_000;\n\nexport function scanOnce(): {\n filesScanned: number;\n newTurns: number;\n} {\n getDb(); // ensure DB is accessible\n\n let filesScanned = 0;\n let newTurns = 0;\n\n for (const target of allTargets()) {\n if (!target.scanner) continue;\n const source = target.id;\n\n for (const { filePath } of target.scanner.discover()) {\n let offset = readFileWatermark(filePath);\n let result = target.scanner.parseFile(filePath, offset);\n if (!result) continue;\n\n // If incremental parse detected a DAG fork, reset watermark\n // and reparse from byte 0 so fork detection runs on the full file.\n let savedSyncIds: SavedSyncIds | undefined;\n if (result.needsFullReparse && offset > 0) {\n savedSyncIds = resetFileForReparse(filePath, result.meta?.sessionId);\n offset = 0;\n result = target.scanner.parseFile(filePath, 0);\n if (!result) continue;\n log.scanner.info(`Reparsing ${filePath} from start (fork detected)`);\n }\n\n filesScanned++;\n\n // When reading from byte 0 (full file), turn indices start at 0 so\n // INSERT OR IGNORE deduplicates. When incremental (offset > 0),\n // re-index from existing turn count — unless the parser produces\n // absolute indices (e.g. Gemini re-reads the full JSON file).\n if (offset > 0 && result.meta?.sessionId && !result.absoluteIndices) {\n const existingCount = getTurnCount(result.meta.sessionId, source);\n if (existingCount > 0) {\n reindexTurns(result, existingCount);\n }\n // Re-index message ordinals for incremental reads\n if (result.messages.length > 0) {\n const maxOrd = getMaxOrdinal(result.meta.sessionId);\n reindexMessages(result, maxOrd + 1);\n }\n }\n\n if (!result.meta?.sessionId) {\n writeFileWatermark(filePath, result.newByteOffset);\n continue;\n }\n\n // Wrap all per-file DB writes in a single transaction so that\n // a crash can't leave messages inserted without watermark advancement\n // (which would cause tool_call duplication on retry).\n const sessionId = result.meta.sessionId;\n const fileMeta = result.meta;\n const fileResult = result;\n const db = getDb();\n (\n db.transaction(() => {\n upsertSession(fileMeta, filePath, source);\n\n if (fileResult.turns.length > 0) {\n insertTurns(fileResult.turns, source);\n updateSessionTotals(sessionId);\n }\n\n if (fileResult.events.length > 0) {\n insertScannerEvents(fileResult.events, source);\n }\n\n if (\n fileResult.messages.length > 0 ||\n fileResult.orphanedToolResults?.size\n ) {\n insertMessages(fileResult.messages, fileResult.orphanedToolResults);\n updateSessionMessageCounts(sessionId);\n }\n\n writeFileWatermark(filePath, fileResult.newByteOffset);\n }) as Database.Transaction\n )();\n\n newTurns += result.turns.length;\n\n // Process fork results (additional sessions from DAG analysis)\n if (result.forks) {\n for (const fork of result.forks) {\n if (!fork.meta?.sessionId) continue;\n const forkSessionId = fork.meta.sessionId;\n const forkMeta = fork.meta;\n (\n db.transaction(() => {\n upsertSession(forkMeta, filePath, source);\n if (fork.turns.length > 0) {\n insertTurns(fork.turns, source);\n updateSessionTotals(forkSessionId);\n }\n if (fork.events.length > 0) {\n insertScannerEvents(fork.events, source);\n }\n if (fork.messages.length > 0 || fork.orphanedToolResults?.size) {\n insertMessages(fork.messages, fork.orphanedToolResults);\n updateSessionMessageCounts(forkSessionId);\n }\n // No watermark — shared file, one watermark for the whole file\n }) as Database.Transaction\n )();\n newTurns += fork.turns.length;\n }\n }\n\n // Restore sync_ids after all data for this file has been re-inserted\n if (savedSyncIds) {\n restoreSyncIds(savedSyncIds);\n }\n\n // Archive raw file for 100% recall\n try {\n const fileSize = fs.statSync(filePath).size;\n const archivedSize = readArchivedSize(filePath);\n if (fileSize > archivedSize) {\n const rawContent = fs.readFileSync(filePath);\n getArchiveBackend().putSync(\n result.meta.sessionId,\n source,\n rawContent,\n );\n writeArchivedSize(filePath, fileSize);\n }\n } catch (archiveErr) {\n // Archive failure is non-fatal\n log.scanner.warn(\n `Archive error for ${filePath}: ${archiveErr instanceof Error ? archiveErr.message : archiveErr}`,\n );\n }\n }\n }\n\n // Link subagent sessions to parents after all files are processed\n if (filesScanned > 0) {\n const linked = linkSubagentSessions();\n if (linked > 0) {\n log.scanner.info(\n `Linked ${linked} subagent session${linked > 1 ? \"s\" : \"\"}`,\n );\n }\n log.scanner.info(`Scanned ${filesScanned} files, ${newTurns} new turns`);\n }\n\n return { filesScanned, newTurns };\n}\n\nfunction reindexTurns(result: ParseResult, startIndex: number): void {\n for (let i = 0; i < result.turns.length; i++) {\n result.turns[i].turnIndex = startIndex + i;\n }\n}\n\nfunction reindexMessages(result: ParseResult, startOrdinal: number): void {\n for (let i = 0; i < result.messages.length; i++) {\n result.messages[i].ordinal = startOrdinal + i;\n }\n}\n\nexport function createScannerLoop(opts: ScannerOptions): ScannerHandle {\n const idleMs = opts.idleIntervalMs ?? DEFAULT_IDLE_MS;\n const catchUpMs = opts.catchUpIntervalMs ?? DEFAULT_CATCHUP_MS;\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n let stopping = false;\n let reparseChecked = false;\n let ready = false;\n\n function scheduleNext(hadWork: boolean): void {\n if (stopping) return;\n const delay = hadWork ? catchUpMs : idleMs;\n timer = setTimeout(() => tick(), delay);\n if (!opts.keepAlive && timer.unref) {\n timer.unref();\n }\n }\n\n function tick(): void {\n if (stopping) return;\n\n // On first tick, check if data version requires a full reparse\n if (!reparseChecked) {\n reparseChecked = true;\n if (needsResync()) {\n log.scanner.info(\"Data version outdated — running atomic reparse...\");\n import(\"./reparse.js\")\n .then(({ reparseAll }) => {\n try {\n const result = reparseAll((msg) => log.scanner.info(msg));\n if (result.success) {\n markResyncComplete();\n } else {\n log.scanner.error(\n `Reparse failed: ${result.error ?? \"unknown\"}`,\n );\n }\n } catch (err) {\n log.scanner.error(\n `Reparse error: ${err instanceof Error ? err.message : err}`,\n );\n }\n scheduleNext(true);\n })\n .catch((err) => {\n log.scanner.error(\n `Reparse import error: ${err instanceof Error ? err.message : err}`,\n );\n scheduleNext(false);\n });\n return;\n }\n // No reparse needed — stamp version if not already set\n markResyncComplete();\n }\n\n let hadWork = false;\n try {\n const { newTurns } = scanOnce();\n hadWork = newTurns > 0;\n\n if (!ready) {\n ready = true;\n opts.onReady?.();\n }\n\n // Only generate summaries when idle and scanner is ready.\n if (!hadWork && ready) {\n try {\n generateSummariesOnce((msg) => log.scanner.info(msg));\n } catch (err) {\n log.scanner.error(\n `Session summary error: ${err instanceof Error ? err.message : err}`,\n );\n }\n }\n } catch (err) {\n log.scanner.error(\n `Scan error: ${err instanceof Error ? err.message : err}`,\n );\n }\n if (!stopping) {\n scheduleNext(hadWork);\n }\n }\n\n return {\n start() {\n if (timer) return;\n stopping = false;\n log.scanner.info(\"Starting scanner\");\n tick();\n },\n stop() {\n stopping = true;\n if (timer) {\n clearTimeout(timer);\n timer = null;\n log.scanner.info(\"Stopped scanner\");\n }\n },\n };\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { gunzipSync, gzipSync } from \"node:zlib\";\nimport type { ArchiveBackend } from \"./backend.js\";\n\nexport class LocalArchiveBackend implements ArchiveBackend {\n constructor(private baseDir: string) {}\n\n putSync(sessionId: string, source: string, content: Buffer): void {\n const dir = path.join(this.baseDir, sessionId);\n fs.mkdirSync(dir, { recursive: true });\n const filePath = path.join(dir, `${source}.jsonl.gz`);\n const compressed = gzipSync(content);\n fs.writeFileSync(filePath, compressed);\n }\n\n getSync(sessionId: string, source: string): Buffer | null {\n const filePath = path.join(this.baseDir, sessionId, `${source}.jsonl.gz`);\n if (!fs.existsSync(filePath)) return null;\n const compressed = fs.readFileSync(filePath);\n return gunzipSync(compressed);\n }\n\n hasSync(sessionId: string, source: string): boolean {\n const filePath = path.join(this.baseDir, sessionId, `${source}.jsonl.gz`);\n return fs.existsSync(filePath);\n }\n\n list(): Array<{ sessionId: string; source: string; sizeBytes: number }> {\n const results: Array<{\n sessionId: string;\n source: string;\n sizeBytes: number;\n }> = [];\n\n if (!fs.existsSync(this.baseDir)) return results;\n\n for (const sessionId of fs.readdirSync(this.baseDir)) {\n const sessionDir = path.join(this.baseDir, sessionId);\n const stat = fs.statSync(sessionDir);\n if (!stat.isDirectory()) continue;\n\n for (const file of fs.readdirSync(sessionDir)) {\n if (!file.endsWith(\".jsonl.gz\")) continue;\n const source = file.replace(/\\.jsonl\\.gz$/, \"\");\n const fileStat = fs.statSync(path.join(sessionDir, file));\n results.push({\n sessionId,\n source,\n sizeBytes: fileStat.size,\n });\n }\n }\n\n return results;\n }\n\n stats(): { totalFiles: number; totalBytes: number } {\n const entries = this.list();\n return {\n totalFiles: entries.length,\n totalBytes: entries.reduce((sum, e) => sum + e.sizeBytes, 0),\n };\n }\n}\n","export type { ArchiveBackend } from \"./backend.js\";\nexport { LocalArchiveBackend } from \"./local.js\";\n\nimport path from \"node:path\";\nimport { config } from \"../config.js\";\nimport type { ArchiveBackend } from \"./backend.js\";\nimport { LocalArchiveBackend } from \"./local.js\";\n\nlet _backend: ArchiveBackend | null = null;\n\nexport function getArchiveBackend(): ArchiveBackend {\n if (!_backend) {\n _backend = new LocalArchiveBackend(path.join(config.dataDir, \"archive\"));\n }\n return _backend;\n}\n","import { execFileSync, spawnSync } from \"node:child_process\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { log } from \"../log.js\";\n\nconst LLM_TIMEOUT_MS = 180_000;\n\nlet _claudePath: string | null | undefined;\n\n/**\n * Detect whether the `claude` CLI is available on this machine.\n * Result is cached for the lifetime of the process.\n */\nexport function detectAgent(): string | null {\n if (_claudePath !== undefined) return _claudePath;\n try {\n _claudePath = execFileSync(\"which\", [\"claude\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n } catch {\n _claudePath = null;\n }\n return _claudePath;\n}\n\n/**\n * Build a clean env that won't trigger recursive hooks or proxy loops.\n */\nfunction cleanEnv(): Record<string, string> {\n const env: Record<string, string> = {};\n for (const [k, v] of Object.entries(process.env)) {\n if (v === undefined) continue;\n if (k === \"CLAUDECODE\") continue;\n if (k.startsWith(\"CLAUDE_CODE_\")) continue;\n if (k === \"ANTHROPIC_BASE_URL\") continue;\n env[k] = v;\n }\n return env;\n}\n\n/** Get the path to the panopticon MCP server script. */\nfunction getMcpServerPath(): string {\n const dir = path.dirname(fileURLToPath(import.meta.url));\n // In the built dist/, summary code is in a chunk at dist/ level,\n // and mcp/server.js is at dist/mcp/server.js (same level)\n return path.resolve(dir, \"mcp\", \"server.js\");\n}\n\n/**\n * Invoke Claude CLI with a prompt and optional MCP server.\n * Returns the trimmed output text, or null on any failure.\n */\nexport function invokeLlm(\n prompt: string,\n opts: {\n timeoutMs?: number;\n withMcp?: boolean;\n systemPrompt?: string;\n model?: string;\n } = {},\n): string | null {\n const claudePath = detectAgent();\n if (!claudePath) return null;\n\n const timeoutMs = opts.timeoutMs ?? LLM_TIMEOUT_MS;\n\n const args = [\n claudePath,\n \"-p\",\n prompt,\n \"--output-format\",\n \"text\",\n \"--model\",\n opts.model ?? \"haiku\",\n \"--no-session-persistence\",\n \"--permission-mode\",\n \"auto\",\n ];\n\n if (opts.systemPrompt) {\n args.push(\"--append-system-prompt\", opts.systemPrompt);\n }\n\n if (opts.withMcp) {\n const mcpPath = getMcpServerPath();\n args.push(\n \"--strict-mcp-config\",\n \"--mcp-config\",\n JSON.stringify({\n mcpServers: {\n panopticon: {\n command: \"node\",\n args: [mcpPath],\n },\n },\n }),\n \"--allowed-tools\",\n \"mcp__panopticon__timeline\",\n \"mcp__panopticon__get\",\n \"mcp__panopticon__query\",\n \"mcp__panopticon__search\",\n \"mcp__panopticon__status\",\n );\n } else {\n args.push(\"--tools\", \"\");\n }\n\n const result = spawnSync(args[0], args.slice(1), {\n env: cleanEnv(),\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n timeout: timeoutMs,\n maxBuffer: 4 * 1024 * 1024,\n });\n\n const text = result.stdout?.toString().trim();\n const stderr = result.stderr?.toString().trim();\n\n if (stderr) log.llm.warn(`stderr: ${stderr.slice(0, 500)}`);\n if (result.signal) {\n log.llm.error(`killed by signal: ${result.signal}`);\n return null;\n }\n log.llm.info(`exit=${result.status} stdout=${text?.length ?? 0} chars`);\n\n // Accept output even with non-zero exit (hooks may cause exit code 1\n // after successful response)\n return text || null;\n}\n","import { getDb } from \"../db/schema.js\";\nimport { detectAgent, invokeLlm } from \"./llm.js\";\n\n/** Minimum messages before a session is worth summarizing. */\nconst MIN_MESSAGES = 3;\n/** Re-summarize when message count has grown by this much. */\nconst SUMMARY_THRESHOLD = 20;\n/** Max sessions to summarize per idle cycle. */\nconst MAX_PER_CYCLE = 50;\n/** Timeout for agent-based summary (longer than simple LLM call). */\nconst AGENT_TIMEOUT_MS = 120_000;\n\nconst SYSTEM_PROMPT = `You are summarizing a coding session for search and retrieval. You have access to panopticon MCP tools to explore the session data.\n\nInstructions:\n1. Use the \"timeline\" tool to read the session's messages and tool calls\n2. If needed, use \"get\" to read full message content or \"query\" for specific data\n3. Produce a summary optimized for AI consumption and full-text search\n4. Include: what was accomplished, key decisions made, specific file/function/package names, problems encountered and how they were resolved\n5. Use concrete names rather than generic descriptions (e.g. \"added FTS5 index on messages table\" not \"improved search\")\n6. Format as 2-4 concise sentences\n7. Output ONLY the summary text, nothing else`;\n\n/**\n * Generate a summary for a single session.\n * Uses Claude CLI with panopticon MCP if available, falls back to deterministic.\n */\nfunction _summarizeSession(\n sessionId: string,\n log: (msg: string) => void,\n): string | null {\n // Try agent-based summary first\n if (detectAgent()) {\n const prompt = `Summarize session ${sessionId}. Start by calling the timeline tool with sessionId \"${sessionId}\" and limit 50.`;\n const result = invokeLlm(prompt, {\n timeoutMs: AGENT_TIMEOUT_MS,\n withMcp: true,\n systemPrompt: SYSTEM_PROMPT,\n model: \"sonnet\",\n });\n if (result) return result;\n log(`LLM summary failed for ${sessionId}, falling back to deterministic`);\n }\n\n // Deterministic fallback\n return buildDeterministicSummary(sessionId);\n}\n\n/**\n * Build a deterministic summary from messages and tool_calls.\n */\nfunction buildDeterministicSummary(sessionId: string): string | null {\n const db = getDb();\n\n const firstUser = db\n .prepare(\n \"SELECT SUBSTR(content, 1, 200) as content FROM messages WHERE session_id = ? AND role = 'user' AND is_system = 0 ORDER BY ordinal ASC LIMIT 1\",\n )\n .get(sessionId) as { content: string } | undefined;\n\n const counts = db\n .prepare(\n \"SELECT COUNT(*) as msg_count, SUM(CASE WHEN role = 'user' AND is_system = 0 THEN 1 ELSE 0 END) as user_count FROM messages WHERE session_id = ?\",\n )\n .get(sessionId) as { msg_count: number; user_count: number };\n\n const tools = db\n .prepare(\n \"SELECT tool_name, COUNT(*) as cnt FROM tool_calls WHERE session_id = ? GROUP BY tool_name ORDER BY cnt DESC LIMIT 5\",\n )\n .all(sessionId) as Array<{ tool_name: string; cnt: number }>;\n\n const files = db\n .prepare(\n \"SELECT DISTINCT json_extract(input_json, '$.file_path') as fp FROM tool_calls WHERE session_id = ? AND tool_name IN ('Write', 'Edit') AND input_json IS NOT NULL LIMIT 10\",\n )\n .all(sessionId) as Array<{ fp: string | null }>;\n\n if (!firstUser && counts.msg_count === 0) return null;\n\n const parts: string[] = [];\n if (firstUser) parts.push(`Prompt: \"${firstUser.content}\"`);\n parts.push(`${counts.msg_count} messages (${counts.user_count} user)`);\n if (tools.length > 0) {\n parts.push(\n `Tools: ${tools.map((t) => `${t.tool_name}(${t.cnt})`).join(\", \")}`,\n );\n }\n const filePaths = files.map((f) => f.fp).filter(Boolean) as string[];\n if (filePaths.length > 0) {\n parts.push(`Files: ${filePaths.join(\", \")}`);\n }\n\n return parts.join(\". \");\n}\n\n/**\n * Generate summaries for sessions that need them.\n * Called when the scanner is idle.\n */\nexport function generateSummariesOnce(log: (msg: string) => void = () => {}): {\n updated: number;\n} {\n const db = getDb();\n let updated = 0;\n\n // Find sessions needing summary:\n // 1. Never summarized + enough messages\n // 2. Stale by message count (grown by THRESHOLD since last summary)\n // 3. Session ended after last summary\n const sessions = db\n .prepare(\n `\n SELECT s.session_id, s.message_count, s.summary_version, s.ended_at_ms,\n EXISTS(SELECT 1 FROM session_repositories WHERE session_id = s.session_id) as has_repo\n FROM sessions s\n WHERE s.message_count >= ?\n AND (\n s.summary IS NULL\n OR (s.message_count - COALESCE(s.summary_version, 0)) >= ?\n OR (s.ended_at_ms IS NOT NULL AND s.ended_at_ms > COALESCE(\n (SELECT MAX(created_at_ms) FROM session_summary_deltas WHERE session_id = s.session_id),\n 0\n ))\n )\n ORDER BY s.started_at_ms DESC\n LIMIT ?\n `,\n )\n .all(MIN_MESSAGES, SUMMARY_THRESHOLD, MAX_PER_CYCLE) as Array<{\n session_id: string;\n message_count: number;\n summary_version: number | null;\n ended_at_ms: number | null;\n has_repo: number;\n }>;\n\n for (const sess of sessions) {\n try {\n // TODO: re-enable LLM summaries once backfill is complete\n const summary = buildDeterministicSummary(sess.session_id);\n if (!summary) continue;\n\n db.prepare(\n \"UPDATE sessions SET summary = ?, summary_version = ?, sync_dirty = 1, sync_seq = COALESCE(sync_seq, 0) + 1 WHERE session_id = ?\",\n ).run(summary, sess.message_count, sess.session_id);\n\n updated++;\n log(`Summarized ${sess.session_id} (${sess.message_count} messages)`);\n } catch (err) {\n log(\n `Summary error for ${sess.session_id}: ${err instanceof Error ? err.message : err}`,\n );\n }\n }\n\n return { updated };\n}\n","import path from \"node:path\";\nimport { refreshIfStale } from \"../db/pricing.js\";\nimport { getDb } from \"../db/schema.js\";\nimport {\n upsertSessionCwd,\n upsertSessionRepository,\n upsertSession as upsertSessionRow,\n} from \"../db/store.js\";\nimport { resolveRepoFromCwd } from \"../repo.js\";\nimport type {\n ParsedEvent,\n ParsedMessage,\n ParsedSession,\n ParsedToolCall,\n ParsedTurn,\n} from \"../targets/types.js\";\n\n// ── Sync ID preservation across single-file reparses ──────────────────────\n\nexport interface SavedSyncIds {\n turns: Array<{\n sessionId: string;\n source: string;\n turnIndex: number;\n syncId: string;\n }>;\n events: Array<{\n sessionId: string;\n source: string;\n eventType: string;\n timestampMs: number;\n toolName: string;\n syncId: string;\n }>;\n toolCalls: Array<{\n sessionId: string;\n ordinal: number;\n toolUseId: string;\n toolName: string;\n syncId: string;\n }>;\n}\n\n// ── Session upsert (writes to unified sessions table) ───────────────────────\n\nexport function upsertSession(\n meta: ParsedSession,\n filePath: string,\n source: string,\n): void {\n // Derive project from repository or cwd basename\n let project: string | undefined;\n if (meta.cwd) {\n const info = resolveRepoFromCwd(meta.cwd);\n if (info) {\n project = info.repo; // e.g. \"fml-inc/panopticon\"\n } else {\n project = path.basename(meta.cwd);\n }\n }\n\n upsertSessionRow({\n session_id: meta.sessionId,\n target: source,\n started_at_ms: meta.startedAtMs,\n first_prompt: meta.firstPrompt,\n model: meta.model,\n cli_version: meta.cliVersion,\n scanner_file_path: filePath,\n has_scanner: 1,\n project,\n created_at: meta.startedAtMs ?? Date.now(),\n parent_session_id: meta.parentSessionId,\n relationship_type:\n meta.relationshipType ?? (meta.parentSessionId ? \"subagent\" : undefined),\n });\n\n // Record cwd and repo for scanner-only sessions\n if (meta.cwd) {\n upsertSessionCwd(meta.sessionId, meta.cwd, meta.startedAtMs ?? Date.now());\n }\n if (meta.cwd) {\n const info = resolveRepoFromCwd(meta.cwd);\n if (info) {\n upsertSessionRepository(\n meta.sessionId,\n info.repo,\n meta.startedAtMs ?? Date.now(),\n undefined,\n info.branch,\n );\n }\n }\n}\n\n// ── Turn insert ─────────────────────────────────────────────────────────────\n\nconst INSERT_TURN_SQL = `\n INSERT OR IGNORE INTO scanner_turns\n (session_id, source, turn_index, timestamp_ms, model, role,\n content_preview, input_tokens, output_tokens,\n cache_read_tokens, cache_creation_tokens, reasoning_tokens)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n`;\n\nexport function insertTurns(turns: ParsedTurn[], source: string): void {\n if (turns.length === 0) return;\n const db = getDb();\n const stmt = db.prepare(INSERT_TURN_SQL);\n const tx = db.transaction(() => {\n for (const t of turns) {\n stmt.run(\n t.sessionId,\n source,\n t.turnIndex,\n t.timestampMs,\n t.model ?? null,\n t.role,\n t.contentPreview ?? null,\n t.inputTokens,\n t.outputTokens,\n t.cacheReadTokens,\n t.cacheCreationTokens,\n t.reasoningTokens,\n );\n }\n });\n tx();\n}\n\n// ── Scanner events insert ───────────────────────────────────────────────────\n\nconst INSERT_EVENT_SQL = `\n INSERT OR IGNORE INTO scanner_events\n (session_id, source, event_type, timestamp_ms, tool_name, tool_input, tool_output, content, metadata)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n`;\n\nexport function insertScannerEvents(\n events: ParsedEvent[],\n source: string,\n): void {\n if (events.length === 0) return;\n const db = getDb();\n const stmt = db.prepare(INSERT_EVENT_SQL);\n const tx = db.transaction(() => {\n for (const e of events) {\n stmt.run(\n e.sessionId,\n source,\n e.eventType,\n e.timestampMs,\n e.toolName ?? null,\n e.toolInput ?? null,\n e.toolOutput ?? null,\n e.content ?? null,\n e.metadata ? JSON.stringify(e.metadata) : null,\n );\n }\n });\n tx();\n\n // Resolve repos from file paths in tool_call events (greedy attribution)\n const seen = new Set<string>();\n for (const e of events) {\n if (e.eventType !== \"tool_call\" || !e.toolInput) continue;\n try {\n const input = JSON.parse(e.toolInput);\n const fp = input.file_path ?? input.path;\n if (typeof fp !== \"string\" || !path.isAbsolute(fp)) continue;\n const dir = path.dirname(fp);\n if (seen.has(dir)) continue;\n seen.add(dir);\n const info = resolveRepoFromCwd(dir);\n if (info) {\n upsertSessionRepository(\n e.sessionId,\n info.repo,\n e.timestampMs,\n undefined,\n info.branch,\n );\n }\n } catch {\n // malformed tool_input JSON\n }\n }\n}\n\n// ── Session totals update (writes to unified sessions table) ────────────────\n\nconst UPDATE_TOTALS_SQL = `\n UPDATE sessions SET\n total_input_tokens = (SELECT COALESCE(SUM(input_tokens), 0) FROM scanner_turns WHERE session_id = ?),\n total_output_tokens = (SELECT COALESCE(SUM(output_tokens), 0) FROM scanner_turns WHERE session_id = ?),\n total_cache_read_tokens = (SELECT COALESCE(SUM(cache_read_tokens), 0) FROM scanner_turns WHERE session_id = ?),\n total_cache_creation_tokens = (SELECT COALESCE(SUM(cache_creation_tokens), 0) FROM scanner_turns WHERE session_id = ?),\n total_reasoning_tokens = (SELECT COALESCE(SUM(reasoning_tokens), 0) FROM scanner_turns WHERE session_id = ?),\n turn_count = (SELECT COUNT(*) FROM scanner_turns WHERE session_id = ?)\n WHERE session_id = ?\n`;\n\nexport function updateSessionTotals(sessionId: string): void {\n const db = getDb();\n db.prepare(UPDATE_TOTALS_SQL).run(\n sessionId,\n sessionId,\n sessionId,\n sessionId,\n sessionId,\n sessionId,\n sessionId,\n );\n\n // Compute tool_counts from scanner tool_calls table\n const toolRows = db\n .prepare(\n `SELECT tool_name, COUNT(*) as cnt FROM tool_calls WHERE session_id = ? GROUP BY tool_name`,\n )\n .all(sessionId) as Array<{ tool_name: string; cnt: number }>;\n\n // Compute event_type_counts from scanner_events (strip \"progress:\" prefix)\n const eventRows = db\n .prepare(\n `SELECT event_type, COUNT(*) as cnt FROM scanner_events WHERE session_id = ? GROUP BY event_type`,\n )\n .all(sessionId) as Array<{ event_type: string; cnt: number }>;\n\n const toolCounts: Record<string, number> = {};\n for (const r of toolRows) toolCounts[r.tool_name] = r.cnt;\n\n const eventCounts: Record<string, number> = {};\n for (const r of eventRows) {\n const key = r.event_type.startsWith(\"progress:\")\n ? r.event_type.slice(\"progress:\".length)\n : r.event_type;\n eventCounts[key] = (eventCounts[key] ?? 0) + r.cnt;\n }\n\n if (toolRows.length > 0 || eventRows.length > 0) {\n db.prepare(\n `UPDATE sessions\n SET tool_counts = ?,\n event_type_counts = ?,\n sync_seq = COALESCE(sync_seq, 0) + 1\n WHERE session_id = ?`,\n ).run(JSON.stringify(toolCounts), JSON.stringify(eventCounts), sessionId);\n }\n // Scanner produces token data that needs pricing for cost queries\n refreshIfStale().catch(() => {});\n}\n\n// ── File watermarks ─────────────────────────────────────────────────────────\n\nexport function readFileWatermark(filePath: string): number {\n const db = getDb();\n const row = db\n .prepare(\n \"SELECT byte_offset FROM scanner_file_watermarks WHERE file_path = ?\",\n )\n .get(filePath) as { byte_offset: number } | undefined;\n return row?.byte_offset ?? 0;\n}\n\nexport function writeFileWatermark(filePath: string, byteOffset: number): void {\n const db = getDb();\n db.prepare(\n `INSERT INTO scanner_file_watermarks (file_path, byte_offset, last_scanned_ms)\n VALUES (?, ?, ?)\n ON CONFLICT(file_path) DO UPDATE SET byte_offset = excluded.byte_offset, last_scanned_ms = excluded.last_scanned_ms`,\n ).run(filePath, byteOffset, Date.now());\n}\n\n/**\n * Snapshot sync_id values for a session and its fork children so they\n * can be restored after re-insertion (preserving upstream sync identity).\n */\nfunction snapshotSyncIds(sessionId: string): SavedSyncIds {\n const db = getDb();\n const saved: SavedSyncIds = { turns: [], events: [], toolCalls: [] };\n\n const forkRows = db\n .prepare(\n \"SELECT session_id FROM sessions WHERE parent_session_id = ? AND relationship_type = 'fork'\",\n )\n .all(sessionId) as Array<{ session_id: string }>;\n const allIds = [sessionId, ...forkRows.map((r) => r.session_id)];\n\n for (const sid of allIds) {\n const turns = db\n .prepare(\n \"SELECT session_id, source, turn_index, sync_id FROM scanner_turns WHERE session_id = ?\",\n )\n .all(sid) as Array<{\n session_id: string;\n source: string;\n turn_index: number;\n sync_id: string;\n }>;\n for (const t of turns) {\n saved.turns.push({\n sessionId: t.session_id,\n source: t.source,\n turnIndex: t.turn_index,\n syncId: t.sync_id,\n });\n }\n\n const events = db\n .prepare(\n \"SELECT session_id, source, event_type, timestamp_ms, COALESCE(tool_name, '') as tool_name, sync_id FROM scanner_events WHERE session_id = ?\",\n )\n .all(sid) as Array<{\n session_id: string;\n source: string;\n event_type: string;\n timestamp_ms: number;\n tool_name: string;\n sync_id: string;\n }>;\n for (const e of events) {\n saved.events.push({\n sessionId: e.session_id,\n source: e.source,\n eventType: e.event_type,\n timestampMs: e.timestamp_ms,\n toolName: e.tool_name,\n syncId: e.sync_id,\n });\n }\n\n const tcs = db\n .prepare(\n `SELECT tc.session_id, m.ordinal, COALESCE(tc.tool_use_id, '') as tool_use_id,\n tc.tool_name, tc.sync_id\n FROM tool_calls tc\n INNER JOIN messages m ON tc.message_id = m.id\n WHERE tc.session_id = ?`,\n )\n .all(sid) as Array<{\n session_id: string;\n ordinal: number;\n tool_use_id: string;\n tool_name: string;\n sync_id: string;\n }>;\n for (const tc of tcs) {\n saved.toolCalls.push({\n sessionId: tc.session_id,\n ordinal: tc.ordinal,\n toolUseId: tc.tool_use_id,\n toolName: tc.tool_name,\n syncId: tc.sync_id,\n });\n }\n }\n\n return saved;\n}\n\n/**\n * Reset a single file for full reparse: clear its watermark and delete\n * all turns, messages, tool_calls, and events for the session so the\n * full-file parse can re-insert cleanly (including fork detection).\n *\n * Returns previously-assigned sync_id values keyed by natural keys so\n * the caller can restore them after re-insertion.\n */\nexport function resetFileForReparse(\n filePath: string,\n sessionId?: string,\n): SavedSyncIds {\n const db = getDb();\n const saved: SavedSyncIds = sessionId\n ? snapshotSyncIds(sessionId)\n : { turns: [], events: [], toolCalls: [] };\n\n db.prepare(\"DELETE FROM scanner_file_watermarks WHERE file_path = ?\").run(\n filePath,\n );\n if (sessionId) {\n db.prepare(\"DELETE FROM scanner_turns WHERE session_id = ?\").run(sessionId);\n db.prepare(\"DELETE FROM scanner_events WHERE session_id = ?\").run(\n sessionId,\n );\n db.prepare(\"DELETE FROM tool_calls WHERE session_id = ?\").run(sessionId);\n db.prepare(\n \"DELETE FROM messages_fts WHERE rowid IN (SELECT id FROM messages WHERE session_id = ?)\",\n ).run(sessionId);\n db.prepare(\"DELETE FROM messages WHERE session_id = ?\").run(sessionId);\n // Also clean up any previously-detected fork sessions from this file\n const forkSessionFilter =\n \"SELECT session_id FROM sessions WHERE parent_session_id = ? AND relationship_type = 'fork'\";\n db.prepare(\n `DELETE FROM scanner_turns WHERE session_id IN (${forkSessionFilter})`,\n ).run(sessionId);\n db.prepare(\n `DELETE FROM scanner_events WHERE session_id IN (${forkSessionFilter})`,\n ).run(sessionId);\n db.prepare(\n `DELETE FROM tool_calls WHERE session_id IN (${forkSessionFilter})`,\n ).run(sessionId);\n db.prepare(\n `DELETE FROM messages_fts WHERE rowid IN (SELECT id FROM messages WHERE session_id IN (${forkSessionFilter}))`,\n ).run(sessionId);\n db.prepare(\n `DELETE FROM messages WHERE session_id IN (${forkSessionFilter})`,\n ).run(sessionId);\n db.prepare(\n \"DELETE FROM sessions WHERE parent_session_id = ? AND relationship_type = 'fork'\",\n ).run(sessionId);\n }\n\n return saved;\n}\n\n/**\n * Restore previously-saved sync_id values after data has been re-inserted.\n * Matches rows by the same natural keys used in reparseAll().\n */\nexport function restoreSyncIds(saved: SavedSyncIds): void {\n if (!saved.turns.length && !saved.events.length && !saved.toolCalls.length)\n return;\n\n const db = getDb();\n const tx = db.transaction(() => {\n if (saved.turns.length > 0) {\n const stmt = db.prepare(\n \"UPDATE scanner_turns SET sync_id = ? WHERE session_id = ? AND source = ? AND turn_index = ?\",\n );\n for (const t of saved.turns) {\n stmt.run(t.syncId, t.sessionId, t.source, t.turnIndex);\n }\n }\n\n if (saved.events.length > 0) {\n const stmt = db.prepare(\n `UPDATE scanner_events SET sync_id = ?\n WHERE session_id = ? AND source = ? AND event_type = ? AND timestamp_ms = ?\n AND COALESCE(tool_name, '') = ?`,\n );\n for (const e of saved.events) {\n stmt.run(\n e.syncId,\n e.sessionId,\n e.source,\n e.eventType,\n e.timestampMs,\n e.toolName,\n );\n }\n }\n\n if (saved.toolCalls.length > 0) {\n const stmt = db.prepare(\n `UPDATE tool_calls SET sync_id = ?\n WHERE message_id IN (SELECT id FROM messages WHERE session_id = ? AND ordinal = ?)\n AND COALESCE(tool_use_id, '') = ?\n AND tool_name = ?`,\n );\n for (const tc of saved.toolCalls) {\n stmt.run(\n tc.syncId,\n tc.sessionId,\n tc.ordinal,\n tc.toolUseId,\n tc.toolName,\n );\n }\n }\n });\n tx();\n}\n\n// ── Turn count for incremental parsing ──────────────────────────────────────\n\nexport function getTurnCount(sessionId: string, source: string): number {\n const db = getDb();\n const row = db\n .prepare(\n \"SELECT COUNT(*) as count FROM scanner_turns WHERE session_id = ? AND source = ?\",\n )\n .get(sessionId, source) as { count: number };\n return row.count;\n}\n\n// ── Archive watermarks ─────────────────────────────────────────────────────\n\nexport function readArchivedSize(filePath: string): number {\n const db = getDb();\n const row = db\n .prepare(\n \"SELECT archived_size FROM scanner_file_watermarks WHERE file_path = ?\",\n )\n .get(filePath) as { archived_size: number } | undefined;\n return row?.archived_size ?? 0;\n}\n\nexport function writeArchivedSize(filePath: string, size: number): void {\n const db = getDb();\n db.prepare(\n \"UPDATE scanner_file_watermarks SET archived_size = ? WHERE file_path = ?\",\n ).run(size, filePath);\n}\n\n// ── Turn summaries ─────────────────────────────────────────────────────────\n\n// ── Messages & tool calls insert ───────────────────────────────────────────\n\n/** Build a short summary for tool-only assistant messages (no text content). */\nfunction toolUseSummary(toolCalls: ParsedToolCall[]): string {\n return toolCalls\n .map((tc) => {\n let label = \"\";\n if (tc.inputJson) {\n try {\n const input = JSON.parse(tc.inputJson);\n label =\n input.description ??\n input.command ??\n input.pattern ??\n input.file_path ??\n input.query ??\n input.prompt ??\n input.skill ??\n \"\";\n } catch {}\n }\n return label ? `[${tc.toolName}: ${label}]` : `[${tc.toolName}]`;\n })\n .join(\"\\n\");\n}\n\nconst INSERT_MESSAGE_SQL = `\n INSERT OR IGNORE INTO messages\n (session_id, ordinal, role, content, timestamp_ms,\n has_thinking, has_tool_use, content_length, is_system,\n model, token_usage, context_tokens, output_tokens,\n has_context_tokens, has_output_tokens, uuid, parent_uuid)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n`;\n\nconst INSERT_TOOL_CALL_SQL = `\n INSERT INTO tool_calls\n (message_id, session_id, tool_name, category, tool_use_id,\n input_json, skill_name, result_content_length, result_content,\n subagent_session_id, duration_ms)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n`;\n\n/**\n * Insert parsed messages and their tool calls into the database.\n * Tool results from user messages are matched back to tool calls\n * from the preceding assistant message by tool_use_id.\n *\n * Also backfills tool_calls from previous scans whose result_content\n * was NULL because the tool_result arrived in a later batch.\n */\nexport function insertMessages(\n messages: ParsedMessage[],\n orphanedToolResults?: Map<\n string,\n { contentLength: number; contentRaw: string; timestampMs?: number }\n >,\n): void {\n if (messages.length === 0 && !orphanedToolResults?.size) return;\n const db = getDb();\n\n // Collect all tool results across user messages for backfilling\n const toolResultMap = new Map<\n string,\n { contentLength: number; contentRaw: string; timestampMs?: number }\n >();\n // Include orphaned results from filtered-out messages\n if (orphanedToolResults) {\n for (const [id, result] of orphanedToolResults) {\n toolResultMap.set(id, result);\n }\n }\n for (const msg of messages) {\n for (const [id, result] of msg.toolResults) {\n toolResultMap.set(id, result);\n }\n }\n\n const msgStmt = db.prepare(INSERT_MESSAGE_SQL);\n const tcStmt = db.prepare(INSERT_TOOL_CALL_SQL);\n const ftsStmt = db.prepare(\n \"INSERT INTO messages_fts(rowid, content) VALUES (?, ?)\",\n );\n\n const tx = db.transaction(() => {\n for (const msg of messages) {\n // Synthesize content for empty assistant messages with tool calls\n let content = msg.content;\n if (!content && msg.role === \"assistant\" && msg.toolCalls.length > 0) {\n content = toolUseSummary(msg.toolCalls);\n }\n\n const result = msgStmt.run(\n msg.sessionId,\n msg.ordinal,\n msg.role,\n content,\n msg.timestampMs ?? null,\n msg.hasThinking ? 1 : 0,\n msg.hasToolUse ? 1 : 0,\n msg.contentLength,\n msg.isSystem ? 1 : 0,\n msg.model ?? \"\",\n msg.tokenUsage ?? \"\",\n msg.contextTokens ?? 0,\n msg.outputTokens ?? 0,\n msg.hasContextTokens ? 1 : 0,\n msg.hasOutputTokens ? 1 : 0,\n msg.uuid ?? null,\n msg.parentUuid ?? null,\n );\n\n // INSERT OR IGNORE returns 0 changes if the row already exists\n if (result.changes === 0) continue;\n\n const messageId = result.lastInsertRowid;\n ftsStmt.run(messageId, content);\n\n for (const tc of msg.toolCalls) {\n // Look up result from the tool_result blocks\n const toolResult = toolResultMap.get(tc.toolUseId);\n const durationMs =\n tc.timestampMs && toolResult?.timestampMs\n ? toolResult.timestampMs - tc.timestampMs\n : null;\n tcStmt.run(\n messageId,\n msg.sessionId,\n tc.toolName,\n tc.category,\n tc.toolUseId,\n tc.inputJson ?? null,\n tc.skillName ?? null,\n toolResult?.contentLength ?? null,\n toolResult?.contentRaw ?? null,\n tc.subagentSessionId ?? null,\n durationMs != null && durationMs >= 0 ? durationMs : null,\n );\n }\n }\n\n // Backfill tool_calls from previous scans whose results arrived in this batch.\n if (toolResultMap.size > 0) {\n const backfillStmt = db.prepare(\n `UPDATE tool_calls\n SET result_content = ?, result_content_length = ?\n WHERE tool_use_id = ? AND result_content IS NULL`,\n );\n for (const [toolUseId, result] of toolResultMap) {\n backfillStmt.run(result.contentRaw, result.contentLength, toolUseId);\n }\n }\n });\n tx();\n}\n\n/**\n * Link subagent sessions to their parents.\n * Finds sessions whose ID appears in tool_calls.subagent_session_id\n * and sets their parent_session_id and relationship_type accordingly.\n */\nexport function linkSubagentSessions(): number {\n const db = getDb();\n // Only check sessions that don't already have a relationship set,\n // which limits work to newly-discovered sessions.\n const result = db\n .prepare(\n `UPDATE sessions\n SET parent_session_id = (\n SELECT tc.session_id\n FROM tool_calls tc\n WHERE tc.subagent_session_id = sessions.session_id\n LIMIT 1\n ),\n relationship_type = 'subagent',\n sync_seq = COALESCE(sync_seq, 0) + 1\n WHERE (relationship_type = '' OR relationship_type IS NULL)\n AND parent_session_id IS NULL\n AND EXISTS (\n SELECT 1 FROM tool_calls tc\n WHERE tc.subagent_session_id = sessions.session_id\n )`,\n )\n .run();\n return result.changes;\n}\n\n/**\n * Get the highest message ordinal for a session, or -1 if no messages exist.\n */\nexport function getMaxOrdinal(sessionId: string): number {\n const db = getDb();\n const row = db\n .prepare(\n \"SELECT MAX(ordinal) as max_ord FROM messages WHERE session_id = ?\",\n )\n .get(sessionId) as { max_ord: number | null };\n return row.max_ord ?? -1;\n}\n"],"mappings":"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","names":["fs","gunzipSync","fs","path","path","path","log","path","upsertSession","path","upsertSession","fs","reparseAll","fs","gunzipSync","log"]}
|
|
@@ -3,16 +3,16 @@ import {
|
|
|
3
3
|
insertOtelMetrics,
|
|
4
4
|
insertOtelSpans,
|
|
5
5
|
upsertSession
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-V3XR2TAN.js";
|
|
7
7
|
import {
|
|
8
8
|
captureException
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-J3HVD4VI.js";
|
|
10
10
|
import {
|
|
11
11
|
log
|
|
12
12
|
} from "./chunk-7Q3BJMLG.js";
|
|
13
13
|
import {
|
|
14
14
|
refreshIfStale
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-FMAHQRIU.js";
|
|
16
16
|
import {
|
|
17
17
|
allTargets
|
|
18
18
|
} from "./chunk-QVK6VGCV.js";
|
|
@@ -1069,4 +1069,4 @@ export {
|
|
|
1069
1069
|
handleOtlpRequest,
|
|
1070
1070
|
createOtlpServer
|
|
1071
1071
|
};
|
|
1072
|
-
//# sourceMappingURL=chunk-
|
|
1072
|
+
//# sourceMappingURL=chunk-OW52TNVA.js.map
|
|
@@ -2,6 +2,54 @@ import {
|
|
|
2
2
|
config
|
|
3
3
|
} from "./chunk-K7YUPLES.js";
|
|
4
4
|
|
|
5
|
+
// src/db/migrations.ts
|
|
6
|
+
var MIGRATIONS = [
|
|
7
|
+
{
|
|
8
|
+
id: 1,
|
|
9
|
+
name: "add_plugin_hooks_to_user_config",
|
|
10
|
+
sql: "ALTER TABLE user_config_snapshots ADD COLUMN plugin_hooks JSON NOT NULL DEFAULT '[]'"
|
|
11
|
+
}
|
|
12
|
+
];
|
|
13
|
+
function runMigrations(db, migrations = MIGRATIONS) {
|
|
14
|
+
const trackingExists = db.prepare(
|
|
15
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'"
|
|
16
|
+
).get();
|
|
17
|
+
db.exec(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
19
|
+
id INTEGER PRIMARY KEY,
|
|
20
|
+
name TEXT NOT NULL,
|
|
21
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
22
|
+
)
|
|
23
|
+
`);
|
|
24
|
+
if (!trackingExists) {
|
|
25
|
+
const stamp = db.prepare(
|
|
26
|
+
"INSERT INTO schema_migrations (id, name) VALUES (?, ?)"
|
|
27
|
+
);
|
|
28
|
+
for (const m of migrations) {
|
|
29
|
+
stamp.run(m.id, m.name);
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const applied = new Set(
|
|
34
|
+
db.prepare("SELECT id FROM schema_migrations").all().map((r) => r.id)
|
|
35
|
+
);
|
|
36
|
+
for (const migration of migrations) {
|
|
37
|
+
if (applied.has(migration.id)) continue;
|
|
38
|
+
const run = db.transaction(() => {
|
|
39
|
+
if (migration.sql) {
|
|
40
|
+
db.exec(migration.sql);
|
|
41
|
+
} else if (migration.up) {
|
|
42
|
+
migration.up(db);
|
|
43
|
+
}
|
|
44
|
+
db.prepare("INSERT INTO schema_migrations (id, name) VALUES (?, ?)").run(
|
|
45
|
+
migration.id,
|
|
46
|
+
migration.name
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
run();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
5
53
|
// src/db/schema.ts
|
|
6
54
|
import fs from "fs";
|
|
7
55
|
import { gunzipSync } from "zlib";
|
|
@@ -276,7 +324,8 @@ CREATE TABLE IF NOT EXISTS user_config_snapshots (
|
|
|
276
324
|
hooks JSON NOT NULL DEFAULT '[]',
|
|
277
325
|
commands JSON NOT NULL DEFAULT '[]',
|
|
278
326
|
rules JSON NOT NULL DEFAULT '[]',
|
|
279
|
-
skills JSON NOT NULL DEFAULT '[]'
|
|
327
|
+
skills JSON NOT NULL DEFAULT '[]',
|
|
328
|
+
plugin_hooks JSON NOT NULL DEFAULT '[]'
|
|
280
329
|
);
|
|
281
330
|
|
|
282
331
|
CREATE TABLE IF NOT EXISTS repo_config_snapshots (
|
|
@@ -433,6 +482,7 @@ function getDb() {
|
|
|
433
482
|
_db.pragma("busy_timeout = 5000");
|
|
434
483
|
registerCompressionFunctions(_db);
|
|
435
484
|
_db.exec(SCHEMA_SQL);
|
|
485
|
+
runMigrations(_db);
|
|
436
486
|
const currentVersion = _db.pragma("user_version", { simple: true }) ?? 0;
|
|
437
487
|
if (currentVersion < SCANNER_DATA_VERSION) {
|
|
438
488
|
_needsResync = true;
|
|
@@ -457,6 +507,7 @@ function closeDb() {
|
|
|
457
507
|
}
|
|
458
508
|
|
|
459
509
|
export {
|
|
510
|
+
runMigrations,
|
|
460
511
|
SCHEMA_SQL,
|
|
461
512
|
SCANNER_DATA_VERSION,
|
|
462
513
|
getDb,
|
|
@@ -464,4 +515,4 @@ export {
|
|
|
464
515
|
markResyncComplete,
|
|
465
516
|
closeDb
|
|
466
517
|
};
|
|
467
|
-
//# sourceMappingURL=chunk-
|
|
518
|
+
//# sourceMappingURL=chunk-SKZHAYNF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/migrations.ts","../src/db/schema.ts"],"sourcesContent":["/**\n * Schema migration system for panopticon's embedded SQLite database.\n *\n * ## Conventions\n *\n * 1. SCHEMA_SQL in schema.ts is ALWAYS the latest desired schema.\n * It uses CREATE TABLE IF NOT EXISTS, making it idempotent.\n *\n * 2. When adding a column: update the CREATE TABLE in SCHEMA_SQL\n * AND add a migration here with ALTER TABLE ADD COLUMN.\n * Both must exist. SCHEMA_SQL handles fresh DBs; migrations\n * handle existing DBs.\n *\n * 3. When adding a new table: add it to SCHEMA_SQL. No migration\n * needed (CREATE TABLE IF NOT EXISTS handles it).\n *\n * 4. When adding an index: add it to SCHEMA_SQL. No migration\n * needed (CREATE INDEX IF NOT EXISTS handles it).\n *\n * 5. For complex changes (data backfill, column rename via rebuild,\n * virtual table recreation): add an `up` function migration.\n * Update SCHEMA_SQL to reflect the final state.\n *\n * 6. Never reorder or remove migrations. Only append.\n *\n * 7. Migration IDs are sequential integers starting from 1.\n *\n * 8. No down migrations. This is an embedded app — users always\n * upgrade forward. Rolling back means reinstalling.\n */\n\nimport type Database from \"better-sqlite3\";\n\nexport interface Migration {\n id: number;\n name: string;\n /** Simple migrations: single SQL statement. */\n sql?: string;\n /** Complex migrations: function that receives the db handle. */\n up?: (db: Database.Database) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Migration registry — append only, never reorder or remove\n// ---------------------------------------------------------------------------\n\nexport const MIGRATIONS: Migration[] = [\n {\n id: 1,\n name: \"add_plugin_hooks_to_user_config\",\n sql: \"ALTER TABLE user_config_snapshots ADD COLUMN plugin_hooks JSON NOT NULL DEFAULT '[]'\",\n },\n];\n\n// ---------------------------------------------------------------------------\n// Migration runner\n// ---------------------------------------------------------------------------\n\n/**\n * Apply pending schema migrations to the database.\n *\n * On a fresh database (where SCHEMA_SQL just created all tables with all\n * columns), the `schema_migrations` table won't exist yet. In that case\n * we stamp all migrations as applied without executing them — SCHEMA_SQL\n * already reflects the final state.\n *\n * On an existing database, unapplied migrations run sequentially inside\n * transactions.\n */\nexport function runMigrations(\n db: Database.Database,\n migrations: Migration[] = MIGRATIONS,\n): void {\n const trackingExists = db\n .prepare(\n \"SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'\",\n )\n .get();\n\n db.exec(`\n CREATE TABLE IF NOT EXISTS schema_migrations (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n applied_at TEXT NOT NULL DEFAULT (datetime('now'))\n )\n `);\n\n if (!trackingExists) {\n // Fresh database: SCHEMA_SQL already created everything.\n // Stamp all migrations as applied without executing them.\n const stamp = db.prepare(\n \"INSERT INTO schema_migrations (id, name) VALUES (?, ?)\",\n );\n for (const m of migrations) {\n stamp.run(m.id, m.name);\n }\n return;\n }\n\n // Existing database: run unapplied migrations sequentially\n const applied = new Set(\n (\n db.prepare(\"SELECT id FROM schema_migrations\").all() as Array<{\n id: number;\n }>\n ).map((r) => r.id),\n );\n\n for (const migration of migrations) {\n if (applied.has(migration.id)) continue;\n const run = db.transaction(() => {\n if (migration.sql) {\n db.exec(migration.sql);\n } else if (migration.up) {\n migration.up(db);\n }\n db.prepare(\"INSERT INTO schema_migrations (id, name) VALUES (?, ?)\").run(\n migration.id,\n migration.name,\n );\n });\n run();\n }\n}\n","import fs from \"node:fs\";\nimport { gunzipSync } from \"node:zlib\";\nimport Database from \"better-sqlite3\";\nimport { config } from \"../config.js\";\nimport { runMigrations } from \"./migrations.js\";\n\nexport { runMigrations } from \"./migrations.js\";\n\nexport const SCHEMA_SQL = `\n\n-- ── OTLP tables ─────────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS otel_logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp_ns INTEGER NOT NULL,\n observed_timestamp_ns INTEGER,\n severity_number INTEGER,\n severity_text TEXT,\n body TEXT,\n attributes JSON,\n resource_attributes JSON,\n session_id TEXT,\n prompt_id TEXT,\n trace_id TEXT,\n span_id TEXT,\n sync_id TEXT DEFAULT (hex(randomblob(8)))\n);\n\nCREATE TABLE IF NOT EXISTS otel_metrics (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n timestamp_ns INTEGER NOT NULL,\n name TEXT NOT NULL,\n value REAL NOT NULL,\n metric_type TEXT,\n unit TEXT,\n attributes JSON,\n resource_attributes JSON,\n session_id TEXT,\n sync_id TEXT DEFAULT (hex(randomblob(8)))\n);\n\nCREATE TABLE IF NOT EXISTS otel_spans (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n trace_id TEXT NOT NULL,\n span_id TEXT NOT NULL,\n parent_span_id TEXT,\n name TEXT NOT NULL,\n kind INTEGER,\n start_time_ns INTEGER NOT NULL,\n end_time_ns INTEGER NOT NULL,\n status_code INTEGER,\n status_message TEXT,\n attributes JSON,\n resource_attributes JSON,\n session_id TEXT,\n UNIQUE(trace_id, span_id)\n);\n\n-- ── Hook events ─────────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS hook_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n event_type TEXT NOT NULL,\n timestamp_ms INTEGER NOT NULL,\n cwd TEXT,\n repository TEXT,\n tool_name TEXT,\n payload BLOB NOT NULL,\n user_prompt TEXT,\n file_path TEXT,\n command TEXT,\n plan TEXT,\n allowed_prompts TEXT,\n tool_result TEXT,\n target TEXT,\n sync_id TEXT DEFAULT (hex(randomblob(8)))\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS hook_events_fts USING fts5(\n payload,\n content='',\n contentless_delete=1,\n tokenize='trigram'\n);\n\n-- ── Sessions ────────────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS sessions (\n session_id TEXT PRIMARY KEY,\n target TEXT,\n started_at_ms INTEGER,\n ended_at_ms INTEGER,\n cwd TEXT,\n first_prompt TEXT,\n permission_mode TEXT,\n agent_version TEXT,\n model TEXT,\n cli_version TEXT,\n scanner_file_path TEXT,\n total_input_tokens INTEGER DEFAULT 0,\n total_output_tokens INTEGER DEFAULT 0,\n total_cache_read_tokens INTEGER DEFAULT 0,\n total_cache_creation_tokens INTEGER DEFAULT 0,\n total_reasoning_tokens INTEGER DEFAULT 0,\n turn_count INTEGER DEFAULT 0,\n otel_input_tokens INTEGER DEFAULT 0,\n otel_output_tokens INTEGER DEFAULT 0,\n otel_cache_read_tokens INTEGER DEFAULT 0,\n otel_cache_creation_tokens INTEGER DEFAULT 0,\n models TEXT,\n has_hooks INTEGER DEFAULT 0,\n has_otel INTEGER DEFAULT 0,\n has_scanner INTEGER DEFAULT 0,\n summary TEXT,\n summary_version INTEGER DEFAULT 0,\n sync_dirty INTEGER DEFAULT 0,\n sync_seq INTEGER DEFAULT 0,\n tool_counts JSON DEFAULT '{}',\n hook_tool_counts JSON DEFAULT '{}',\n event_type_counts JSON DEFAULT '{}',\n hook_event_type_counts JSON DEFAULT '{}',\n project TEXT,\n machine TEXT NOT NULL DEFAULT 'local',\n message_count INTEGER DEFAULT 0,\n user_message_count INTEGER DEFAULT 0,\n parent_session_id TEXT,\n relationship_type TEXT DEFAULT '',\n is_automated INTEGER DEFAULT 0,\n created_at INTEGER\n);\n\nCREATE TABLE IF NOT EXISTS session_repositories (\n session_id TEXT NOT NULL,\n repository TEXT NOT NULL,\n first_seen_ms INTEGER NOT NULL,\n git_user_name TEXT,\n git_user_email TEXT,\n branch TEXT,\n UNIQUE(session_id, repository)\n);\n\nCREATE TABLE IF NOT EXISTS session_cwds (\n session_id TEXT NOT NULL,\n cwd TEXT NOT NULL,\n first_seen_ms INTEGER NOT NULL,\n UNIQUE(session_id, cwd)\n);\n\n-- ── Messages ────────────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS messages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n timestamp_ms INTEGER,\n has_thinking INTEGER NOT NULL DEFAULT 0,\n has_tool_use INTEGER NOT NULL DEFAULT 0,\n content_length INTEGER NOT NULL DEFAULT 0,\n is_system INTEGER NOT NULL DEFAULT 0,\n model TEXT NOT NULL DEFAULT '',\n token_usage TEXT NOT NULL DEFAULT '',\n context_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n has_context_tokens INTEGER NOT NULL DEFAULT 0,\n has_output_tokens INTEGER NOT NULL DEFAULT 0,\n uuid TEXT,\n parent_uuid TEXT,\n UNIQUE(session_id, ordinal)\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(\n content,\n content='',\n contentless_delete=1,\n tokenize='trigram'\n);\n\n-- ── Tool calls ──────────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS tool_calls (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n message_id INTEGER NOT NULL,\n session_id TEXT NOT NULL,\n tool_name TEXT NOT NULL,\n category TEXT NOT NULL,\n tool_use_id TEXT,\n input_json TEXT,\n skill_name TEXT,\n result_content_length INTEGER,\n result_content TEXT,\n duration_ms INTEGER,\n subagent_session_id TEXT,\n sync_id TEXT DEFAULT (hex(randomblob(8)))\n);\n\n-- ── Scanner tables ──────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS scanner_turns (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n source TEXT NOT NULL,\n turn_index INTEGER NOT NULL,\n timestamp_ms INTEGER NOT NULL,\n model TEXT,\n role TEXT,\n content_preview TEXT,\n input_tokens INTEGER DEFAULT 0,\n output_tokens INTEGER DEFAULT 0,\n cache_read_tokens INTEGER DEFAULT 0,\n cache_creation_tokens INTEGER DEFAULT 0,\n reasoning_tokens INTEGER DEFAULT 0,\n sync_id TEXT DEFAULT (hex(randomblob(8))),\n UNIQUE(session_id, source, turn_index)\n);\n\nCREATE TABLE IF NOT EXISTS scanner_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n source TEXT NOT NULL,\n event_type TEXT NOT NULL,\n timestamp_ms INTEGER NOT NULL,\n tool_name TEXT,\n tool_input TEXT,\n tool_output TEXT,\n content TEXT,\n metadata JSON,\n sync_id TEXT DEFAULT (hex(randomblob(8))),\n UNIQUE(session_id, source, event_type, timestamp_ms, tool_name)\n);\n\nCREATE TABLE IF NOT EXISTS scanner_file_watermarks (\n file_path TEXT PRIMARY KEY,\n byte_offset INTEGER NOT NULL DEFAULT 0,\n last_scanned_ms INTEGER NOT NULL,\n archived_size INTEGER DEFAULT 0\n);\n\n-- ── Session summaries ───────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS session_summary_deltas (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n delta_index INTEGER NOT NULL,\n created_at_ms INTEGER NOT NULL,\n from_turn INTEGER NOT NULL,\n to_turn INTEGER NOT NULL,\n content TEXT NOT NULL,\n method TEXT NOT NULL DEFAULT 'deterministic',\n UNIQUE(session_id, delta_index)\n);\n\n-- ── Model pricing ───────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS model_pricing (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n model_id TEXT NOT NULL,\n input_per_m REAL NOT NULL,\n output_per_m REAL NOT NULL,\n cache_read_per_m REAL NOT NULL DEFAULT 0,\n cache_write_per_m REAL NOT NULL DEFAULT 0,\n updated_ms INTEGER NOT NULL\n);\n\n-- ── Config snapshots ────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS user_config_snapshots (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n device_name TEXT NOT NULL,\n snapshot_at_ms INTEGER NOT NULL,\n content_hash TEXT NOT NULL,\n permissions JSON NOT NULL DEFAULT '{}',\n enabled_plugins JSON NOT NULL DEFAULT '[]',\n hooks JSON NOT NULL DEFAULT '[]',\n commands JSON NOT NULL DEFAULT '[]',\n rules JSON NOT NULL DEFAULT '[]',\n skills JSON NOT NULL DEFAULT '[]',\n plugin_hooks JSON NOT NULL DEFAULT '[]'\n);\n\nCREATE TABLE IF NOT EXISTS repo_config_snapshots (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n repository TEXT NOT NULL,\n cwd TEXT NOT NULL,\n session_id TEXT,\n snapshot_at_ms INTEGER NOT NULL,\n content_hash TEXT NOT NULL,\n hooks JSON NOT NULL DEFAULT '[]',\n mcp_servers JSON NOT NULL DEFAULT '[]',\n commands JSON NOT NULL DEFAULT '[]',\n agents JSON NOT NULL DEFAULT '[]',\n rules JSON NOT NULL DEFAULT '[]',\n local_hooks JSON NOT NULL DEFAULT '[]',\n local_mcp_servers JSON NOT NULL DEFAULT '[]',\n local_permissions JSON NOT NULL DEFAULT '{}',\n local_is_gitignored INTEGER NOT NULL DEFAULT 1,\n instructions JSON NOT NULL DEFAULT '[]'\n);\n\n-- ── Sync watermarks ─────────────────────────────────────────────────────────\n\nCREATE TABLE IF NOT EXISTS watermarks (\n key TEXT PRIMARY KEY,\n value INTEGER NOT NULL\n);\n\n-- ── Per-session sync state ─────────────────────────────────────────────────\n\nDROP TABLE IF EXISTS pending_session_sync;\n\nCREATE TABLE IF NOT EXISTS target_session_sync (\n session_id TEXT NOT NULL,\n target TEXT NOT NULL,\n confirmed INTEGER DEFAULT 0,\n sync_seq INTEGER DEFAULT 0,\n synced_seq INTEGER DEFAULT 0,\n wm_messages INTEGER DEFAULT 0,\n wm_tool_calls INTEGER DEFAULT 0,\n wm_scanner_turns INTEGER DEFAULT 0,\n wm_scanner_events INTEGER DEFAULT 0,\n wm_hook_events INTEGER DEFAULT 0,\n wm_otel_logs INTEGER DEFAULT 0,\n wm_otel_metrics INTEGER DEFAULT 0,\n wm_otel_spans INTEGER DEFAULT 0,\n PRIMARY KEY (session_id, target)\n);\n\n-- ── Indexes ─────────────────────────────────────────────────────────────────\n\n-- otel_logs\nCREATE INDEX IF NOT EXISTS idx_logs_session ON otel_logs(session_id);\nCREATE INDEX IF NOT EXISTS idx_logs_body ON otel_logs(body);\nCREATE INDEX IF NOT EXISTS idx_logs_ts ON otel_logs(timestamp_ns);\nCREATE INDEX IF NOT EXISTS idx_logs_prompt ON otel_logs(prompt_id);\n\n-- otel_metrics\nCREATE INDEX IF NOT EXISTS idx_metrics_session ON otel_metrics(session_id);\nCREATE INDEX IF NOT EXISTS idx_metrics_name ON otel_metrics(name);\nCREATE INDEX IF NOT EXISTS idx_metrics_ts ON otel_metrics(timestamp_ns);\n\n-- otel_spans\nCREATE INDEX IF NOT EXISTS idx_spans_session ON otel_spans(session_id);\nCREATE INDEX IF NOT EXISTS idx_spans_trace ON otel_spans(trace_id);\nCREATE INDEX IF NOT EXISTS idx_spans_start ON otel_spans(start_time_ns);\n\n-- hook_events\nCREATE INDEX IF NOT EXISTS idx_hooks_session ON hook_events(session_id);\nCREATE INDEX IF NOT EXISTS idx_hooks_type ON hook_events(event_type);\nCREATE INDEX IF NOT EXISTS idx_hooks_tool ON hook_events(tool_name);\nCREATE INDEX IF NOT EXISTS idx_hooks_ts ON hook_events(timestamp_ms);\nCREATE INDEX IF NOT EXISTS idx_hooks_file_path ON hook_events(file_path);\nCREATE INDEX IF NOT EXISTS idx_hooks_target ON hook_events(target);\n\n-- sessions\nCREATE INDEX IF NOT EXISTS idx_sessions_target ON sessions(target);\nCREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at_ms);\nCREATE INDEX IF NOT EXISTS idx_sessions_sync_seq ON sessions(sync_seq);\nCREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);\nCREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine);\nCREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id)\n WHERE parent_session_id IS NOT NULL;\n\n-- session_repositories\nCREATE INDEX IF NOT EXISTS idx_session_repos_session ON session_repositories(session_id);\nCREATE INDEX IF NOT EXISTS idx_session_repos_repo ON session_repositories(repository);\n\n-- session_cwds\nCREATE INDEX IF NOT EXISTS idx_session_cwds_session ON session_cwds(session_id);\n\n-- messages\nCREATE INDEX IF NOT EXISTS idx_messages_session_ordinal ON messages(session_id, ordinal);\nCREATE INDEX IF NOT EXISTS idx_messages_session_role ON messages(session_id, role);\n\n-- tool_calls\nCREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_category ON tool_calls(category);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_skill ON tool_calls(skill_name)\n WHERE skill_name IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_tool_calls_subagent ON tool_calls(subagent_session_id)\n WHERE subagent_session_id IS NOT NULL;\n\n-- scanner_turns\nCREATE INDEX IF NOT EXISTS idx_scanner_turns_session ON scanner_turns(session_id);\nCREATE INDEX IF NOT EXISTS idx_scanner_turns_ts ON scanner_turns(timestamp_ms);\n\n-- scanner_events\nCREATE INDEX IF NOT EXISTS idx_scanner_events_session ON scanner_events(session_id);\nCREATE INDEX IF NOT EXISTS idx_scanner_events_type ON scanner_events(event_type);\n\n-- session_summary_deltas\nCREATE INDEX IF NOT EXISTS idx_summary_deltas_session ON session_summary_deltas(session_id);\n\n-- model_pricing\nCREATE INDEX IF NOT EXISTS idx_model_pricing_model ON model_pricing(model_id, updated_ms);\n\n-- user_config_snapshots\nCREATE INDEX IF NOT EXISTS idx_user_config_ts ON user_config_snapshots(snapshot_at_ms);\nCREATE INDEX IF NOT EXISTS idx_user_config_device_hash ON user_config_snapshots(device_name, content_hash);\n\n-- repo_config_snapshots\nCREATE INDEX IF NOT EXISTS idx_repo_config_repo_ts ON repo_config_snapshots(repository, snapshot_at_ms);\nCREATE INDEX IF NOT EXISTS idx_repo_config_session ON repo_config_snapshots(session_id);\nCREATE INDEX IF NOT EXISTS idx_repo_config_repo_hash ON repo_config_snapshots(repository, content_hash);\n\n`;\n\n/**\n * Scanner data version. Increment when parser logic changes in ways that\n * affect stored data (new fields extracted, content formatting changes,\n * fork detection improvements, etc.). On startup, if the DB's user_version\n * is lower than this, a full resync is triggered automatically.\n */\nexport const SCANNER_DATA_VERSION = 1;\n\n// ---------------------------------------------------------------------------\n// Database initialization\n// ---------------------------------------------------------------------------\n\nlet _db: Database.Database | null = null;\nlet _needsResync = false;\n\nfunction registerCompressionFunctions(db: Database.Database): void {\n db.function(\"decompress\", (blob: Buffer | null) =>\n blob ? gunzipSync(blob).toString() : null,\n );\n}\n\nexport function getDb(): Database.Database {\n // If the db file was deleted (e.g. uninstall --purge) while this process\n // still holds a stale connection, drop it so we don't serve old data.\n if (_db && !fs.existsSync(config.dbPath)) {\n try {\n _db.close();\n } catch {}\n _db = null;\n }\n if (_db) return _db;\n\n // Don't auto-create the data directory — callers that need to bootstrap\n // the DB (install, initDb, hook handler) call ensureDataDir() first.\n if (!fs.existsSync(config.dataDir)) {\n throw new Error(\n `Panopticon data directory not found: ${config.dataDir}. Run \"panopticon install\" to set up.`,\n );\n }\n\n _db = new Database(config.dbPath);\n _db.pragma(\"auto_vacuum = INCREMENTAL\");\n _db.pragma(\"journal_mode = WAL\");\n _db.pragma(\"busy_timeout = 5000\");\n\n registerCompressionFunctions(_db);\n _db.exec(SCHEMA_SQL);\n runMigrations(_db);\n\n // Check data version for resync\n const currentVersion = (_db.pragma(\"user_version\", { simple: true }) ??\n 0) as number;\n if (currentVersion < SCANNER_DATA_VERSION) {\n _needsResync = true;\n } else {\n _needsResync = false;\n }\n\n return _db;\n}\n\n/** True when the DB was opened with a stale data version. */\nexport function needsResync(): boolean {\n return _needsResync;\n}\n\n/** Mark resync as complete and stamp the current data version. */\nexport function markResyncComplete(): void {\n _needsResync = false;\n const db = getDb();\n db.pragma(`user_version = ${SCANNER_DATA_VERSION}`);\n}\n\nexport function closeDb(): void {\n if (_db) {\n _db.close();\n _db = null;\n }\n}\n"],"mappings":";;;;;AA8CO,IAAM,aAA0B;AAAA,EACrC;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,KAAK;AAAA,EACP;AACF;AAiBO,SAAS,cACd,IACA,aAA0B,YACpB;AACN,QAAM,iBAAiB,GACpB;AAAA,IACC;AAAA,EACF,EACC,IAAI;AAEP,KAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAMP;AAED,MAAI,CAAC,gBAAgB;AAGnB,UAAM,QAAQ,GAAG;AAAA,MACf;AAAA,IACF;AACA,eAAW,KAAK,YAAY;AAC1B,YAAM,IAAI,EAAE,IAAI,EAAE,IAAI;AAAA,IACxB;AACA;AAAA,EACF;AAGA,QAAM,UAAU,IAAI;AAAA,IAEhB,GAAG,QAAQ,kCAAkC,EAAE,IAAI,EAGnD,IAAI,CAAC,MAAM,EAAE,EAAE;AAAA,EACnB;AAEA,aAAW,aAAa,YAAY;AAClC,QAAI,QAAQ,IAAI,UAAU,EAAE,EAAG;AAC/B,UAAM,MAAM,GAAG,YAAY,MAAM;AAC/B,UAAI,UAAU,KAAK;AACjB,WAAG,KAAK,UAAU,GAAG;AAAA,MACvB,WAAW,UAAU,IAAI;AACvB,kBAAU,GAAG,EAAE;AAAA,MACjB;AACA,SAAG,QAAQ,wDAAwD,EAAE;AAAA,QACnE,UAAU;AAAA,QACV,UAAU;AAAA,MACZ;AAAA,IACF,CAAC;AACD,QAAI;AAAA,EACN;AACF;;;AC3HA,OAAO,QAAQ;AACf,SAAS,kBAAkB;AAC3B,OAAO,cAAc;AAMd,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsZnB,IAAM,uBAAuB;AAMpC,IAAI,MAAgC;AACpC,IAAI,eAAe;AAEnB,SAAS,6BAA6B,IAA6B;AACjE,KAAG;AAAA,IAAS;AAAA,IAAc,CAAC,SACzB,OAAO,WAAW,IAAI,EAAE,SAAS,IAAI;AAAA,EACvC;AACF;AAEO,SAAS,QAA2B;AAGzC,MAAI,OAAO,CAAC,GAAG,WAAW,OAAO,MAAM,GAAG;AACxC,QAAI;AACF,UAAI,MAAM;AAAA,IACZ,QAAQ;AAAA,IAAC;AACT,UAAM;AAAA,EACR;AACA,MAAI,IAAK,QAAO;AAIhB,MAAI,CAAC,GAAG,WAAW,OAAO,OAAO,GAAG;AAClC,UAAM,IAAI;AAAA,MACR,wCAAwC,OAAO,OAAO;AAAA,IACxD;AAAA,EACF;AAEA,QAAM,IAAI,SAAS,OAAO,MAAM;AAChC,MAAI,OAAO,2BAA2B;AACtC,MAAI,OAAO,oBAAoB;AAC/B,MAAI,OAAO,qBAAqB;AAEhC,+BAA6B,GAAG;AAChC,MAAI,KAAK,UAAU;AACnB,gBAAc,GAAG;AAGjB,QAAM,iBAAkB,IAAI,OAAO,gBAAgB,EAAE,QAAQ,KAAK,CAAC,KACjE;AACF,MAAI,iBAAiB,sBAAsB;AACzC,mBAAe;AAAA,EACjB,OAAO;AACL,mBAAe;AAAA,EACjB;AAEA,SAAO;AACT;AAGO,SAAS,cAAuB;AACrC,SAAO;AACT;AAGO,SAAS,qBAA2B;AACzC,iBAAe;AACf,QAAM,KAAK,MAAM;AACjB,KAAG,OAAO,kBAAkB,oBAAoB,EAAE;AACpD;AAEO,SAAS,UAAgB;AAC9B,MAAI,KAAK;AACP,QAAI,MAAM;AACV,UAAM;AAAA,EACR;AACF;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getDb
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SKZHAYNF.js";
|
|
4
4
|
|
|
5
5
|
// src/db/store.ts
|
|
6
6
|
import { createHash } from "crypto";
|
|
@@ -131,7 +131,8 @@ function insertUserConfigSnapshot(snap) {
|
|
|
131
131
|
hooks: snap.hooks,
|
|
132
132
|
commands: snap.commands,
|
|
133
133
|
rules: snap.rules,
|
|
134
|
-
skills: snap.skills
|
|
134
|
+
skills: snap.skills,
|
|
135
|
+
pluginHooks: snap.pluginHooks
|
|
135
136
|
});
|
|
136
137
|
const existing = db.prepare(
|
|
137
138
|
"SELECT content_hash FROM user_config_snapshots WHERE device_name = ? ORDER BY snapshot_at_ms DESC LIMIT 1"
|
|
@@ -139,8 +140,8 @@ function insertUserConfigSnapshot(snap) {
|
|
|
139
140
|
if (existing?.content_hash === hash) return false;
|
|
140
141
|
db.prepare(
|
|
141
142
|
`INSERT INTO user_config_snapshots
|
|
142
|
-
(device_name, snapshot_at_ms, content_hash, permissions, enabled_plugins, hooks, commands, rules, skills)
|
|
143
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
143
|
+
(device_name, snapshot_at_ms, content_hash, permissions, enabled_plugins, hooks, commands, rules, skills, plugin_hooks)
|
|
144
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
144
145
|
).run(
|
|
145
146
|
snap.deviceName,
|
|
146
147
|
Date.now(),
|
|
@@ -150,7 +151,8 @@ function insertUserConfigSnapshot(snap) {
|
|
|
150
151
|
JSON.stringify(snap.hooks),
|
|
151
152
|
JSON.stringify(snap.commands),
|
|
152
153
|
JSON.stringify(snap.rules),
|
|
153
|
-
JSON.stringify(snap.skills)
|
|
154
|
+
JSON.stringify(snap.skills),
|
|
155
|
+
JSON.stringify(snap.pluginHooks)
|
|
154
156
|
);
|
|
155
157
|
return true;
|
|
156
158
|
}
|
|
@@ -409,4 +411,4 @@ export {
|
|
|
409
411
|
incrementEventTypeCount,
|
|
410
412
|
insertHookEvent
|
|
411
413
|
};
|
|
412
|
-
//# sourceMappingURL=chunk-
|
|
414
|
+
//# sourceMappingURL=chunk-V3XR2TAN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/store.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { gzipSync } from \"node:zlib\";\n\nimport { getDb } from \"./schema.js\";\n\nexport interface OtelLogRow {\n timestamp_ns: number;\n observed_timestamp_ns?: number;\n severity_number?: number;\n severity_text?: string;\n body?: string;\n attributes?: Record<string, unknown>;\n resource_attributes?: Record<string, unknown>;\n session_id?: string;\n prompt_id?: string;\n trace_id?: string;\n span_id?: string;\n}\n\nexport interface OtelMetricRow {\n timestamp_ns: number;\n name: string;\n value: number;\n metric_type?: string;\n unit?: string;\n attributes?: Record<string, unknown>;\n resource_attributes?: Record<string, unknown>;\n session_id?: string;\n}\n\nexport interface OtelSpanRow {\n trace_id: string;\n span_id: string;\n parent_span_id?: string;\n name: string;\n kind?: number;\n start_time_ns: number;\n end_time_ns: number;\n status_code?: number;\n status_message?: string;\n attributes?: Record<string, unknown>;\n resource_attributes?: Record<string, unknown>;\n session_id?: string;\n}\n\nexport interface HookEventRow {\n session_id: string;\n event_type: string;\n timestamp_ms: number;\n cwd?: string;\n repository?: string;\n tool_name?: string;\n target?: string;\n user_prompt?: string;\n file_path?: string;\n command?: string;\n plan?: string;\n allowed_prompts?: string;\n payload: unknown;\n}\n\nconst INSERT_LOG_SQL = `\n INSERT INTO otel_logs (timestamp_ns, observed_timestamp_ns, severity_number, severity_text, body, attributes, resource_attributes, session_id, prompt_id, trace_id, span_id)\n VALUES (@timestamp_ns, @observed_timestamp_ns, @severity_number, @severity_text, @body, @attributes, @resource_attributes, @session_id, @prompt_id, @trace_id, @span_id)\n`;\n\nconst INSERT_METRIC_SQL = `\n INSERT INTO otel_metrics (timestamp_ns, name, value, metric_type, unit, attributes, resource_attributes, session_id)\n VALUES (@timestamp_ns, @name, @value, @metric_type, @unit, @attributes, @resource_attributes, @session_id)\n`;\n\nconst INSERT_SPAN_SQL = `\n INSERT OR IGNORE INTO otel_spans (trace_id, span_id, parent_span_id, name, kind, start_time_ns, end_time_ns, status_code, status_message, attributes, resource_attributes, session_id)\n VALUES (@trace_id, @span_id, @parent_span_id, @name, @kind, @start_time_ns, @end_time_ns, @status_code, @status_message, @attributes, @resource_attributes, @session_id)\n`;\n\nconst INSERT_HOOK_SQL = `\n INSERT INTO hook_events (session_id, event_type, timestamp_ms, cwd, repository, tool_name,\n target, user_prompt, file_path, command, tool_result, plan, allowed_prompts, payload)\n VALUES (@session_id, @event_type, @timestamp_ms, @cwd, @repository, @tool_name,\n @target, @user_prompt, @file_path, @command, @tool_result, @plan, @allowed_prompts, @payload)\n`;\n\nexport function insertOtelLogs(rows: OtelLogRow[]): void {\n const db = getDb();\n const stmt = db.prepare(INSERT_LOG_SQL);\n const insertMany = db.transaction((rows: OtelLogRow[]) => {\n for (const row of rows) {\n stmt.run({\n timestamp_ns: row.timestamp_ns,\n observed_timestamp_ns: row.observed_timestamp_ns ?? null,\n severity_number: row.severity_number ?? null,\n severity_text: row.severity_text ?? null,\n body: row.body ?? null,\n attributes: row.attributes ? JSON.stringify(row.attributes) : null,\n resource_attributes: row.resource_attributes\n ? JSON.stringify(row.resource_attributes)\n : null,\n session_id: row.session_id ?? null,\n prompt_id: row.prompt_id ?? null,\n trace_id: row.trace_id ?? null,\n span_id: row.span_id ?? null,\n });\n }\n });\n insertMany(rows);\n}\n\nexport function insertOtelMetrics(rows: OtelMetricRow[]): void {\n const db = getDb();\n const stmt = db.prepare(INSERT_METRIC_SQL);\n\n const insertMany = db.transaction((rows: OtelMetricRow[]) => {\n for (const row of rows) {\n const sessionId = row.session_id ?? null;\n\n stmt.run({\n timestamp_ns: row.timestamp_ns,\n name: row.name,\n value: row.value,\n metric_type: row.metric_type ?? null,\n unit: row.unit ?? null,\n attributes: row.attributes ? JSON.stringify(row.attributes) : null,\n resource_attributes: row.resource_attributes\n ? JSON.stringify(row.resource_attributes)\n : null,\n session_id: sessionId,\n });\n }\n });\n insertMany(rows);\n}\n\nexport function insertOtelSpans(rows: OtelSpanRow[]): void {\n const db = getDb();\n const stmt = db.prepare(INSERT_SPAN_SQL);\n\n const insertMany = db.transaction((rows: OtelSpanRow[]) => {\n for (const row of rows) {\n stmt.run({\n trace_id: row.trace_id,\n span_id: row.span_id,\n parent_span_id: row.parent_span_id ?? null,\n name: row.name,\n kind: row.kind ?? null,\n start_time_ns: row.start_time_ns,\n end_time_ns: row.end_time_ns,\n status_code: row.status_code ?? null,\n status_message: row.status_message ?? null,\n attributes: row.attributes ? JSON.stringify(row.attributes) : null,\n resource_attributes: row.resource_attributes\n ? JSON.stringify(row.resource_attributes)\n : null,\n session_id: row.session_id ?? null,\n });\n }\n });\n insertMany(rows);\n}\n\nexport function upsertSessionRepository(\n sessionId: string,\n repository: string,\n timestampMs: number,\n gitIdentity?: { name: string | null; email: string | null },\n branch?: string | null,\n): void {\n const db = getDb();\n const existing = db\n .prepare(\n `SELECT 1 FROM session_repositories WHERE session_id = ? AND repository = ?`,\n )\n .get(sessionId, repository);\n\n db.prepare(\n `INSERT INTO session_repositories (session_id, repository, first_seen_ms, git_user_name, git_user_email, branch)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(session_id, repository) DO UPDATE SET\n git_user_name = COALESCE(session_repositories.git_user_name, excluded.git_user_name),\n git_user_email = COALESCE(session_repositories.git_user_email, excluded.git_user_email),\n branch = COALESCE(excluded.branch, session_repositories.branch)`,\n ).run(\n sessionId,\n repository,\n timestampMs,\n gitIdentity?.name ?? null,\n gitIdentity?.email ?? null,\n branch ?? null,\n );\n\n // When a NEW repo is associated, bump sync_seq so the session re-syncs\n // with updated repository info (avoids backend repo-filter rejections)\n if (!existing) {\n db.prepare(\n `UPDATE sessions SET sync_seq = COALESCE(sync_seq, 0) + 1 WHERE session_id = ?`,\n ).run(sessionId);\n }\n}\n\nexport function upsertSessionCwd(\n sessionId: string,\n cwd: string,\n timestampMs: number,\n): void {\n const db = getDb();\n db.prepare(\n \"INSERT INTO session_cwds (session_id, cwd, first_seen_ms) VALUES (?, ?, ?) ON CONFLICT DO NOTHING\",\n ).run(sessionId, cwd, timestampMs);\n}\n\n// ---------------------------------------------------------------------------\n// Config snapshots\n// ---------------------------------------------------------------------------\n\nfunction contentHash(obj: Record<string, unknown>): string {\n return createHash(\"sha256\")\n .update(JSON.stringify(obj, Object.keys(obj).sort()))\n .digest(\"hex\");\n}\n\nexport interface UserConfigSnapshot {\n deviceName: string;\n permissions: unknown;\n enabledPlugins: unknown;\n hooks: unknown;\n commands: unknown;\n rules: unknown;\n skills: unknown;\n pluginHooks: unknown;\n}\n\n/**\n * Insert a user config snapshot if the content has changed since the last one\n * for this device. Returns true if a new row was inserted.\n */\nexport function insertUserConfigSnapshot(snap: UserConfigSnapshot): boolean {\n const db = getDb();\n const hash = contentHash({\n permissions: snap.permissions,\n enabledPlugins: snap.enabledPlugins,\n hooks: snap.hooks,\n commands: snap.commands,\n rules: snap.rules,\n skills: snap.skills,\n pluginHooks: snap.pluginHooks,\n });\n\n // Check if latest snapshot for this device has the same hash\n const existing = db\n .prepare(\n \"SELECT content_hash FROM user_config_snapshots WHERE device_name = ? ORDER BY snapshot_at_ms DESC LIMIT 1\",\n )\n .get(snap.deviceName) as { content_hash: string } | undefined;\n\n if (existing?.content_hash === hash) return false;\n\n db.prepare(\n `INSERT INTO user_config_snapshots\n (device_name, snapshot_at_ms, content_hash, permissions, enabled_plugins, hooks, commands, rules, skills, plugin_hooks)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n snap.deviceName,\n Date.now(),\n hash,\n JSON.stringify(snap.permissions),\n JSON.stringify(snap.enabledPlugins),\n JSON.stringify(snap.hooks),\n JSON.stringify(snap.commands),\n JSON.stringify(snap.rules),\n JSON.stringify(snap.skills),\n JSON.stringify(snap.pluginHooks),\n );\n return true;\n}\n\nexport interface RepoConfigSnapshot {\n repository: string;\n cwd: string;\n sessionId?: string;\n // project layer\n hooks: unknown;\n mcpServers: unknown;\n commands: unknown;\n agents: unknown;\n rules: unknown;\n // project local layer\n localHooks: unknown;\n localMcpServers: unknown;\n localPermissions: unknown;\n localIsGitignored: boolean;\n // instructions\n instructions: unknown;\n}\n\n/**\n * Insert a repo config snapshot if the content has changed since the last one\n * for this repository. Returns true if a new row was inserted.\n */\nexport function insertRepoConfigSnapshot(snap: RepoConfigSnapshot): boolean {\n const db = getDb();\n const hash = contentHash({\n hooks: snap.hooks,\n mcpServers: snap.mcpServers,\n commands: snap.commands,\n agents: snap.agents,\n rules: snap.rules,\n localHooks: snap.localHooks,\n localMcpServers: snap.localMcpServers,\n localPermissions: snap.localPermissions,\n localIsGitignored: snap.localIsGitignored,\n instructions: snap.instructions,\n });\n\n const existing = db\n .prepare(\n \"SELECT content_hash FROM repo_config_snapshots WHERE repository = ? ORDER BY snapshot_at_ms DESC LIMIT 1\",\n )\n .get(snap.repository) as { content_hash: string } | undefined;\n\n if (existing?.content_hash === hash) return false;\n\n db.prepare(\n `INSERT INTO repo_config_snapshots\n (repository, cwd, session_id, snapshot_at_ms, content_hash,\n hooks, mcp_servers, commands, agents, rules,\n local_hooks, local_mcp_servers, local_permissions, local_is_gitignored,\n instructions)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n ).run(\n snap.repository,\n snap.cwd,\n snap.sessionId ?? null,\n Date.now(),\n hash,\n JSON.stringify(snap.hooks),\n JSON.stringify(snap.mcpServers),\n JSON.stringify(snap.commands),\n JSON.stringify(snap.agents),\n JSON.stringify(snap.rules),\n JSON.stringify(snap.localHooks),\n JSON.stringify(snap.localMcpServers),\n JSON.stringify(snap.localPermissions),\n snap.localIsGitignored ? 1 : 0,\n JSON.stringify(snap.instructions),\n );\n return true;\n}\n\nexport interface SessionUpsert {\n session_id: string;\n target?: string;\n started_at_ms?: number;\n ended_at_ms?: number;\n first_prompt?: string;\n permission_mode?: string;\n agent_version?: string;\n // Scanner-sourced fields\n model?: string;\n cli_version?: string;\n scanner_file_path?: string;\n total_input_tokens?: number;\n total_output_tokens?: number;\n total_cache_read_tokens?: number;\n total_cache_creation_tokens?: number;\n total_reasoning_tokens?: number;\n turn_count?: number;\n // OTLP-sourced tokens\n otel_input_tokens?: number;\n otel_output_tokens?: number;\n otel_cache_read_tokens?: number;\n otel_cache_creation_tokens?: number;\n // Metadata\n project?: string;\n created_at?: number;\n has_hooks?: number;\n has_otel?: number;\n has_scanner?: number;\n parent_session_id?: string;\n relationship_type?: string;\n is_automated?: number;\n}\n\nexport function upsertSession(row: SessionUpsert): void {\n const db = getDb();\n db.prepare(\n `INSERT INTO sessions (session_id, target, started_at_ms, ended_at_ms, first_prompt,\n permission_mode, agent_version, model, cli_version, scanner_file_path,\n total_input_tokens, total_output_tokens, total_cache_read_tokens,\n total_cache_creation_tokens, total_reasoning_tokens, turn_count,\n otel_input_tokens, otel_output_tokens, otel_cache_read_tokens, otel_cache_creation_tokens,\n models, project, created_at, parent_session_id, relationship_type, is_automated,\n has_hooks, has_otel, has_scanner)\n VALUES (@session_id, @target, @started_at_ms, @ended_at_ms, @first_prompt,\n @permission_mode, @agent_version, @model, @cli_version, @scanner_file_path,\n @total_input_tokens, @total_output_tokens, @total_cache_read_tokens,\n @total_cache_creation_tokens, @total_reasoning_tokens, @turn_count,\n @otel_input_tokens, @otel_output_tokens, @otel_cache_read_tokens, @otel_cache_creation_tokens,\n @model, @project, @created_at, @parent_session_id, @relationship_type, @is_automated,\n @has_hooks, @has_otel, @has_scanner)\n ON CONFLICT(session_id) DO UPDATE SET\n target = COALESCE(excluded.target, sessions.target),\n started_at_ms = COALESCE(excluded.started_at_ms, sessions.started_at_ms),\n ended_at_ms = COALESCE(excluded.ended_at_ms, sessions.ended_at_ms),\n first_prompt = COALESCE(sessions.first_prompt, excluded.first_prompt),\n permission_mode = COALESCE(excluded.permission_mode, sessions.permission_mode),\n agent_version = COALESCE(excluded.agent_version, sessions.agent_version),\n model = COALESCE(excluded.model, sessions.model),\n cli_version = COALESCE(excluded.cli_version, sessions.cli_version),\n scanner_file_path = COALESCE(excluded.scanner_file_path, sessions.scanner_file_path),\n total_input_tokens = COALESCE(excluded.total_input_tokens, sessions.total_input_tokens),\n total_output_tokens = COALESCE(excluded.total_output_tokens, sessions.total_output_tokens),\n total_cache_read_tokens = COALESCE(excluded.total_cache_read_tokens, sessions.total_cache_read_tokens),\n total_cache_creation_tokens = COALESCE(excluded.total_cache_creation_tokens, sessions.total_cache_creation_tokens),\n total_reasoning_tokens = COALESCE(excluded.total_reasoning_tokens, sessions.total_reasoning_tokens),\n turn_count = COALESCE(excluded.turn_count, sessions.turn_count),\n otel_input_tokens = COALESCE(excluded.otel_input_tokens, sessions.otel_input_tokens),\n otel_output_tokens = COALESCE(excluded.otel_output_tokens, sessions.otel_output_tokens),\n otel_cache_read_tokens = COALESCE(excluded.otel_cache_read_tokens, sessions.otel_cache_read_tokens),\n otel_cache_creation_tokens = COALESCE(excluded.otel_cache_creation_tokens, sessions.otel_cache_creation_tokens),\n models = CASE\n WHEN excluded.model IS NULL THEN sessions.models\n WHEN sessions.models IS NULL THEN excluded.model\n WHEN sessions.models LIKE '%' || excluded.model || '%' THEN sessions.models\n ELSE sessions.models || ',' || excluded.model\n END,\n project = COALESCE(sessions.project, excluded.project),\n parent_session_id = COALESCE(excluded.parent_session_id, sessions.parent_session_id),\n relationship_type = COALESCE(excluded.relationship_type, sessions.relationship_type),\n is_automated = COALESCE(excluded.is_automated, sessions.is_automated),\n has_hooks = MAX(COALESCE(sessions.has_hooks, 0), COALESCE(excluded.has_hooks, 0)),\n has_otel = MAX(COALESCE(sessions.has_otel, 0), COALESCE(excluded.has_otel, 0)),\n has_scanner = MAX(COALESCE(sessions.has_scanner, 0), COALESCE(excluded.has_scanner, 0)),\n sync_dirty = 1,\n sync_seq = COALESCE(sessions.sync_seq, 0) + 1`,\n ).run({\n session_id: row.session_id,\n target: row.target ?? null,\n started_at_ms: row.started_at_ms ?? null,\n ended_at_ms: row.ended_at_ms ?? null,\n first_prompt: row.first_prompt ?? null,\n permission_mode: row.permission_mode ?? null,\n agent_version: row.agent_version ?? null,\n model: row.model ?? null,\n cli_version: row.cli_version ?? null,\n scanner_file_path: row.scanner_file_path ?? null,\n total_input_tokens: row.total_input_tokens ?? null,\n total_output_tokens: row.total_output_tokens ?? null,\n total_cache_read_tokens: row.total_cache_read_tokens ?? null,\n total_cache_creation_tokens: row.total_cache_creation_tokens ?? null,\n total_reasoning_tokens: row.total_reasoning_tokens ?? null,\n turn_count: row.turn_count ?? null,\n otel_input_tokens: row.otel_input_tokens ?? null,\n otel_output_tokens: row.otel_output_tokens ?? null,\n otel_cache_read_tokens: row.otel_cache_read_tokens ?? null,\n otel_cache_creation_tokens: row.otel_cache_creation_tokens ?? null,\n project: row.project ?? null,\n created_at: row.created_at ?? null,\n parent_session_id: row.parent_session_id ?? null,\n relationship_type: row.relationship_type ?? null,\n is_automated: row.is_automated ?? null,\n has_hooks: row.has_hooks ?? null,\n has_otel: row.has_otel ?? null,\n has_scanner: row.has_scanner ?? null,\n });\n}\n\n/** Prefixes/substrings that identify automated (non-interactive) sessions. */\nconst AUTOMATED_PREFIXES = [\n \"You are a code reviewer.\",\n \"You are a security code reviewer.\",\n \"You are a design reviewer.\",\n \"You are a code assistant. Your task is to address\",\n \"You are a code review insights analyst.\",\n \"You are reviewing whether an implementation matches\",\n \"You are a plan document reviewer.\",\n \"You are a spec document reviewer.\",\n \"You are summarizing a day of AI agent activity.\",\n \"You are analyzing AI agent sessions.\",\n \"## Analysis Request\",\n \"# Fix Request\",\n];\nconst AUTOMATED_SUBSTRINGS = [\"invoked by roborev to perform this review\"];\n\nfunction isAutomatedPrompt(firstPrompt: string): boolean {\n for (const p of AUTOMATED_PREFIXES) {\n if (firstPrompt.startsWith(p)) return true;\n }\n for (const s of AUTOMATED_SUBSTRINGS) {\n if (firstPrompt.includes(s)) return true;\n }\n return false;\n}\n\n/**\n * Recompute message_count, user_message_count, and is_automated\n * from the messages table. is_automated is set when user_message_count <= 1\n * and first_prompt matches a known automated pattern.\n */\nexport function updateSessionMessageCounts(sessionId: string): void {\n const db = getDb();\n\n // Count non-system user messages\n const counts = db\n .prepare(\n `SELECT\n (SELECT COUNT(*) FROM messages WHERE session_id = ?) as msg_count,\n (SELECT COUNT(*) FROM messages WHERE session_id = ? AND role = 'user' AND is_system = 0) as user_count,\n (SELECT first_prompt FROM sessions WHERE session_id = ?) as first_prompt`,\n )\n .get(sessionId, sessionId, sessionId) as {\n msg_count: number;\n user_count: number;\n first_prompt: string | null;\n };\n\n const isAutomated =\n counts.user_count <= 1 &&\n counts.first_prompt != null &&\n isAutomatedPrompt(counts.first_prompt)\n ? 1\n : 0;\n\n db.prepare(\n `UPDATE sessions SET\n message_count = ?,\n user_message_count = ?,\n is_automated = CASE WHEN ? > 1 THEN 0 ELSE ? END,\n sync_seq = COALESCE(sync_seq, 0) + 1\n WHERE session_id = ?`,\n ).run(\n counts.msg_count,\n counts.user_count,\n counts.user_count,\n isAutomated,\n sessionId,\n );\n}\n\n/**\n * Increment a hook tool count for a session. Uses JSON_SET to atomically\n * update the hook_tool_counts JSON object.\n * Does NOT bump sync_seq — the scanner drives sync_seq via updateSessionTotals.\n * Hook data syncs via Phase 2 (hook_events table), not the session row.\n */\nexport function incrementToolCount(sessionId: string, toolName: string): void {\n const db = getDb();\n db.prepare(\n `UPDATE sessions\n SET hook_tool_counts = JSON_SET(\n COALESCE(hook_tool_counts, '{}'),\n '$.' || @tool,\n COALESCE(JSON_EXTRACT(hook_tool_counts, '$.' || @tool), 0) + 1\n )\n WHERE session_id = @session_id`,\n ).run({ session_id: sessionId, tool: toolName });\n}\n\n/**\n * Increment a hook event type count for a session.\n * Does NOT bump sync_seq — same rationale as incrementToolCount.\n */\nexport function incrementEventTypeCount(\n sessionId: string,\n eventType: string,\n): void {\n const db = getDb();\n db.prepare(\n `UPDATE sessions\n SET hook_event_type_counts = JSON_SET(\n COALESCE(hook_event_type_counts, '{}'),\n '$.' || @event_type,\n COALESCE(JSON_EXTRACT(hook_event_type_counts, '$.' || @event_type), 0) + 1\n )\n WHERE session_id = @session_id`,\n ).run({ session_id: sessionId, event_type: eventType });\n}\n\nfunction extractStr(\n obj: Record<string, unknown> | undefined,\n key: string,\n): string | undefined {\n const v = obj?.[key];\n return typeof v === \"string\" ? v : undefined;\n}\n\nexport function insertHookEvent(row: HookEventRow): void {\n const db = getDb();\n const data = row.payload as Record<string, unknown>;\n const toolInput = data.tool_input as Record<string, unknown> | undefined;\n\n // Extract high-value fields into columns for indexed queries\n const userPrompt =\n extractStr(data, \"prompt\") ?? extractStr(data, \"user_prompt\");\n const filePath = extractStr(toolInput, \"file_path\");\n const command = extractStr(toolInput, \"command\");\n const plan = extractStr(toolInput, \"plan\");\n const toolResultRaw = data.tool_result ?? data.tool_response;\n const toolResult = toolResultRaw\n ? typeof toolResultRaw === \"string\"\n ? toolResultRaw\n : JSON.stringify(toolResultRaw)\n : undefined;\n const allowedPrompts = toolInput?.allowedPrompts\n ? JSON.stringify(toolInput.allowedPrompts)\n : undefined;\n\n const fullJson = JSON.stringify(data);\n\n const insertWithFts = db.transaction(() => {\n db.prepare(INSERT_HOOK_SQL).run({\n session_id: row.session_id,\n event_type: row.event_type,\n timestamp_ms: row.timestamp_ms,\n cwd: row.cwd ?? null,\n repository: row.repository ?? null,\n tool_name: row.tool_name ?? null,\n target: row.target ?? null,\n user_prompt: userPrompt ?? null,\n file_path: filePath ?? null,\n command: command ?? null,\n tool_result: toolResult ?? null,\n plan: plan ?? null,\n allowed_prompts: allowedPrompts ?? null,\n payload: gzipSync(Buffer.from(fullJson)),\n });\n const { id } = db.prepare(\"SELECT last_insert_rowid() as id\").get() as {\n id: number;\n };\n db.prepare(\"INSERT INTO hook_events_fts(rowid, payload) VALUES (?, ?)\").run(\n id,\n fullJson,\n );\n });\n insertWithFts();\n}\n"],"mappings":"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","names":["rows"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getDb
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-SKZHAYNF.js";
|
|
4
4
|
import {
|
|
5
5
|
config
|
|
6
6
|
} from "./chunk-K7YUPLES.js";
|
|
@@ -131,4 +131,4 @@ export {
|
|
|
131
131
|
pruneExecute,
|
|
132
132
|
autoPrune
|
|
133
133
|
};
|
|
134
|
-
//# sourceMappingURL=chunk-
|
|
134
|
+
//# sourceMappingURL=chunk-WXPT6KG7.js.map
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
syncTargetRemove,
|
|
24
24
|
syncWatermarkGet,
|
|
25
25
|
syncWatermarkSet
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-HO443ZQM.js";
|
|
27
27
|
import {
|
|
28
28
|
DAEMON_NAMES,
|
|
29
29
|
LOG_DIR,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from "./chunk-7Q3BJMLG.js";
|
|
33
33
|
import {
|
|
34
34
|
refreshPricing as refreshPricing2
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-FMAHQRIU.js";
|
|
36
36
|
import {
|
|
37
37
|
loadUnifiedConfig
|
|
38
38
|
} from "./chunk-QK5442ZP.js";
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
import {
|
|
46
46
|
closeDb,
|
|
47
47
|
getDb
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-SKZHAYNF.js";
|
|
49
49
|
import {
|
|
50
50
|
config,
|
|
51
51
|
ensureDataDir
|
|
@@ -334,7 +334,7 @@ function removeShellEnv() {
|
|
|
334
334
|
}
|
|
335
335
|
var program = new Command();
|
|
336
336
|
program.name("panopticon").description("Observability for Claude Code").version(
|
|
337
|
-
true ? "0.1.
|
|
337
|
+
true ? "0.1.2+430c018" : "dev"
|
|
338
338
|
);
|
|
339
339
|
program.command("install").alias("setup").description("Build, register plugin, init DB, configure shell").option(
|
|
340
340
|
"--target <target>",
|
|
@@ -462,7 +462,7 @@ program.command("uninstall").description("Remove panopticon hooks, shell config,
|
|
|
462
462
|
}
|
|
463
463
|
});
|
|
464
464
|
program.command("update").description("Update panopticon to the latest version").action(async () => {
|
|
465
|
-
const currentVersion = true ? "0.1.
|
|
465
|
+
const currentVersion = true ? "0.1.2+430c018" : "unknown";
|
|
466
466
|
console.log(`Current: ${currentVersion}`);
|
|
467
467
|
console.log(
|
|
468
468
|
"To update, re-run the install command for your package manager:\n"
|
|
@@ -821,7 +821,7 @@ program.command("status").description("Show server status and database stats").a
|
|
|
821
821
|
console.log(` pending: ${result.totalPending} total`);
|
|
822
822
|
for (const [table, info] of Object.entries(result.tables)) {
|
|
823
823
|
console.log(
|
|
824
|
-
` ${table}: ${info.pending} (${info.
|
|
824
|
+
` ${table}: ${info.pending} (${info.synced} / ${info.total})`
|
|
825
825
|
);
|
|
826
826
|
}
|
|
827
827
|
}
|