@fml-inc/fml 0.0.30 → 0.1.1
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/.claude-plugin/plugin.json +1 -1
- package/dist/{chunk-TXQRKU2Y.js → chunk-CUBBYHH7.js} +92 -53
- package/dist/chunk-CUBBYHH7.js.map +1 -0
- package/dist/{chunk-RJOFOTNH.js → chunk-I5BHNW37.js} +6 -140
- package/dist/chunk-I5BHNW37.js.map +1 -0
- package/dist/cli.js +134 -1808
- package/dist/cli.js.map +1 -1
- package/dist/mcp/server.js +20 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/{token-store-NE7AM2PH.js → token-store-HOFFHNNN.js} +2 -2
- package/package.json +4 -4
- package/dist/chunk-2CRFCFE4.js +0 -11
- package/dist/chunk-2CRFCFE4.js.map +0 -1
- package/dist/chunk-NLFKYSN7.js +0 -640
- package/dist/chunk-NLFKYSN7.js.map +0 -1
- package/dist/chunk-RJOFOTNH.js.map +0 -1
- package/dist/chunk-TXQRKU2Y.js.map +0 -1
- package/dist/chunk-UALBZA5X.js +0 -123
- package/dist/chunk-UALBZA5X.js.map +0 -1
- package/dist/doctor-YV6KQEGW.js +0 -2019
- package/dist/doctor-YV6KQEGW.js.map +0 -1
- package/dist/esm-GTDP7OMQ-EP6UYLQG.js +0 -14442
- package/dist/esm-GTDP7OMQ-EP6UYLQG.js.map +0 -1
- /package/dist/{token-store-NE7AM2PH.js.map → token-store-HOFFHNNN.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/config.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/db/migrations.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/db/schema.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/sync/reader.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/sync/registry.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/sync/watermark.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/unified-config.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nfunction defaultDataDir(): string {\n switch (process.platform) {\n case \"darwin\":\n return path.join(\n os.homedir(),\n \"Library\",\n \"Application Support\",\n \"panopticon\",\n );\n case \"win32\":\n return path.join(\n process.env.APPDATA ?? path.join(os.homedir(), \"AppData\", \"Roaming\"),\n \"panopticon\",\n );\n default:\n return path.join(os.homedir(), \".local\", \"share\", \"panopticon\");\n }\n}\n\nfunction resolveDataDir(): string {\n if (process.env.PANOPTICON_DATA_DIR) return process.env.PANOPTICON_DATA_DIR;\n return defaultDataDir();\n}\n\nconst DATA_DIR = resolveDataDir();\n\nconst CLAUDE_DIR = path.join(os.homedir(), \".claude\");\nconst MARKETPLACE_DIR = path.join(\n os.homedir(),\n \".local\",\n \"share\",\n \"claude-plugins\",\n);\n\nconst DEFAULT_PORT = 4318;\n\nexport const config = {\n dataDir: DATA_DIR,\n dbPath: path.join(DATA_DIR, \"panopticon.db\"),\n // Unified server port — replaces separate OTLP and proxy ports\n port: parseInt(\n process.env.PANOPTICON_PORT ??\n process.env.PANOPTICON_OTLP_PORT ??\n String(DEFAULT_PORT),\n 10,\n ),\n host: process.env.PANOPTICON_HOST ?? \"127.0.0.1\",\n serverPidFile: path.join(DATA_DIR, \"panopticon.pid\"),\n // Legacy — kept for backward compat during transition\n pidFile: path.join(DATA_DIR, \"otlp-receiver.pid\"),\n otlpPort: parseInt(process.env.PANOPTICON_OTLP_PORT ?? \"4318\", 10),\n otlpHost: process.env.PANOPTICON_OTLP_HOST ?? \"0.0.0.0\",\n marketplaceDir: MARKETPLACE_DIR,\n marketplaceManifest: path.join(\n MARKETPLACE_DIR,\n \".claude-plugin\",\n \"marketplace.json\",\n ),\n pluginCacheDir: path.join(\n CLAUDE_DIR,\n \"plugins\",\n \"cache\",\n \"local-plugins\",\n \"panopticon\",\n ),\n proxyPort: parseInt(process.env.PANOPTICON_PROXY_PORT ?? \"4320\", 10),\n proxyHost: process.env.PANOPTICON_PROXY_HOST ?? \"127.0.0.1\",\n proxyPidFile: path.join(DATA_DIR, \"proxy.pid\"),\n proxyIdleSessionMs: 30 * 60 * 1000,\n} as const;\n\nexport function ensureDataDir(): void {\n fs.mkdirSync(config.dataDir, { recursive: true });\n}\n","/**\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 \"./driver.js\";\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) => 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 id: 2,\n name: \"remove_sessions_sync_watermark\",\n up: (db) => {\n // sync_seq is per-session, not globally monotonic — the old watermark\n // was poisoned by a long-running session. Sessions now sync by\n // comparing against target_session_sync instead of a global cursor.\n const exists = db\n .prepare(\n \"SELECT 1 FROM sqlite_master WHERE type='table' AND name='watermarks'\",\n )\n .get();\n if (exists) {\n db.exec(\"DELETE FROM watermarks WHERE key LIKE 'sessions:%'\");\n }\n },\n },\n {\n id: 3,\n name: \"drop_session_summary_deltas\",\n // #115 (Apr 2) replaced delta summaries with a single sessions.summary\n // column but left the table in the schema and never wrote to it. The\n // prune/MCP/e2e references have been cleaned up; this migration removes\n // the dead table for DBs that were created before the rewrite.\n sql: \"DROP TABLE IF EXISTS session_summary_deltas\",\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,\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 // No tracking table could mean:\n // a) Truly fresh DB — SCHEMA_SQL just created all tables with all columns\n // b) Pre-migration-system DB — tables existed before migrations were added,\n // and CREATE TABLE IF NOT EXISTS didn't add new columns\n //\n // Distinguish by checking for existing data. A fresh DB has no rows yet.\n let hasData = false;\n try {\n hasData = !!db.prepare(\"SELECT 1 FROM sessions LIMIT 1\").get();\n } catch {\n // sessions table doesn't exist — definitely a fresh/test DB\n }\n\n if (!hasData) {\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 // Pre-migration-system DB: fall through to run migrations normally\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 { config } from \"../config.js\";\nimport { Database } from \"./driver.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-- ── 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-- 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 | null = null;\nlet _needsResync = false;\n\nfunction registerCompressionFunctions(db: Database): void {\n db.function(\"decompress\", (blob: unknown) =>\n blob ? gunzipSync(blob as Uint8Array).toString() : null,\n );\n}\n\nexport function getDb(): 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","import { getDb } from \"../db/schema.js\";\nimport type {\n HookEventRecord,\n MessageSyncRecord,\n MetricRow,\n OtelLogRecord,\n OtelSpanRecord,\n RepoConfigSnapshotRecord,\n ScannerEventRecord,\n ScannerTurnRecord,\n SessionSyncRecord,\n ToolCallSyncRecord,\n UserConfigSnapshotRecord,\n} from \"./types.js\";\n\nfunction parseJson(raw: string | null): Record<string, unknown> | null {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw);\n return typeof parsed === \"object\" && parsed !== null ? parsed : null;\n } catch {\n return null;\n }\n}\n\n// ── Hook events ──────────────────────────────────────────────────────────────\n\nconst HOOK_EVENTS_SQL = `\n SELECT h.id, h.session_id, h.sync_id, h.event_type, h.timestamp_ms, h.cwd, h.repository,\n h.tool_name, decompress(h.payload) as payload,\n h.user_prompt, h.file_path, h.command, h.tool_result,\n s.target\n FROM hook_events h\n LEFT JOIN sessions s ON s.session_id = h.session_id\n WHERE h.id > ?\n ORDER BY h.id\n LIMIT ?\n`;\n\nexport function readHookEvents(\n afterId: number,\n limit: number,\n): { rows: HookEventRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(HOOK_EVENTS_SQL).all(afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n event_type: string;\n timestamp_ms: number;\n cwd: string | null;\n repository: string | null;\n tool_name: string | null;\n payload: string | null;\n user_prompt: string | null;\n file_path: string | null;\n command: string | null;\n tool_result: string | null;\n target: string | null;\n }>;\n\n const rows: HookEventRecord[] = rawRows.map((r) => ({\n hookId: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n eventType: r.event_type,\n timestampMs: r.timestamp_ms,\n cwd: r.cwd,\n repository: r.repository,\n toolName: r.tool_name,\n payload: parseJson(r.payload),\n userPrompt: r.user_prompt,\n filePath: r.file_path,\n command: r.command,\n toolResult: r.tool_result,\n target: r.target,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].hookId : afterId;\n return { rows, maxId };\n}\n\n// ── OTLP logs ────────────────────────────────────────────────────────────────\n\n/** Body types that hooks already cover — filtered out when hooks are installed. */\nconst HOOK_COVERED_BODIES = [\n // Claude Code\n \"claude_code.user_prompt\",\n \"claude_code.tool_decision\",\n \"claude_code.tool_result\",\n // Gemini CLI\n \"gemini_cli.user_prompt\",\n \"gemini_cli.tool_call\",\n \"gemini_cli.hook_call\",\n];\n\nconst ALL_LOGS_SQL = `\n SELECT id, sync_id, timestamp_ns, body, attributes, resource_attributes,\n severity_text, session_id, prompt_id, trace_id, span_id\n FROM otel_logs\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\ninterface RawOtelLogRow {\n id: number;\n sync_id: string | null;\n timestamp_ns: number;\n body: string | null;\n attributes: string | null;\n resource_attributes: string | null;\n severity_text: string | null;\n session_id: string | null;\n prompt_id: string | null;\n trace_id: string | null;\n span_id: string | null;\n}\n\nfunction mapOtelRows(rawRows: RawOtelLogRow[]): OtelLogRecord[] {\n return rawRows.map((r) => ({\n id: r.id,\n syncId: r.sync_id,\n timestampNs: r.timestamp_ns,\n body: r.body,\n attributes: parseJson(r.attributes),\n resourceAttributes: parseJson(r.resource_attributes),\n severityText: r.severity_text,\n sessionId: r.session_id,\n promptId: r.prompt_id,\n traceId: r.trace_id,\n spanId: r.span_id,\n }));\n}\n\n/**\n * Read OTLP logs in batches.\n * When hooksInstalled is true, filters out body types that hooks cover\n * (tool_decision, tool_result, user_prompt) to avoid double-counting.\n */\n/**\n * Read OTLP logs in batches.\n * When hooksInstalled is true, filters out body types that hooks cover\n * (tool_decision, tool_result, user_prompt) to avoid double-counting.\n *\n * maxId advances to the highest id scanned (not just the highest returned),\n * so the watermark skips past blocks of filtered rows without stalling.\n */\nexport function readOtelLogs(\n afterId: number,\n limit: number,\n hooksInstalled: boolean,\n): { rows: OtelLogRecord[]; maxId: number } {\n const db = getDb();\n\n if (!hooksInstalled) {\n const rawRows = db\n .prepare(ALL_LOGS_SQL)\n .all(afterId, limit) as RawOtelLogRow[];\n const rows = mapOtelRows(rawRows);\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n }\n\n // When filtering, we need the max id from the unfiltered range so the\n // watermark advances past blocks of hook-covered rows.\n const scanMaxId = (\n db\n .prepare(\n \"SELECT MAX(id) as m FROM (SELECT id FROM otel_logs WHERE id > ? ORDER BY id LIMIT ?)\",\n )\n .get(afterId, limit) as { m: number | null }\n ).m;\n\n if (scanMaxId == null) return { rows: [], maxId: afterId };\n\n // Only return filtered rows within the scanned range (id <= scanMaxId),\n // not beyond it — otherwise we'd skip ahead of the scan window.\n const rawRows = db\n .prepare(\n `SELECT id, sync_id, timestamp_ns, body, attributes, resource_attributes,\n severity_text, session_id, prompt_id, trace_id, span_id\n FROM otel_logs\n WHERE id > ? AND id <= ?\n AND body NOT IN (${HOOK_COVERED_BODIES.map(() => \"?\").join(\", \")})\n ORDER BY id`,\n )\n .all(afterId, scanMaxId, ...HOOK_COVERED_BODIES) as RawOtelLogRow[];\n\n const rows = mapOtelRows(rawRows);\n return { rows, maxId: scanMaxId };\n}\n\n// ── Metrics ──────────────────────────────────────────────────────────────────\n\nconst METRICS_SQL = `\n SELECT id, sync_id, timestamp_ns, name, value, metric_type, unit,\n attributes, resource_attributes, session_id\n FROM otel_metrics\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readMetrics(\n afterId: number,\n limit: number,\n): { rows: MetricRow[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(METRICS_SQL).all(afterId, limit) as Array<{\n id: number;\n sync_id: string | null;\n timestamp_ns: number;\n name: string;\n value: number;\n metric_type: string | null;\n unit: string | null;\n attributes: string | null;\n resource_attributes: string | null;\n session_id: string | null;\n }>;\n\n const rows: MetricRow[] = rawRows.map((r) => ({\n id: r.id,\n syncId: r.sync_id,\n timestampNs: r.timestamp_ns,\n name: r.name,\n value: r.value,\n metricType: r.metric_type,\n unit: r.unit,\n attributes: parseJson(r.attributes),\n resourceAttributes: parseJson(r.resource_attributes),\n sessionId: r.session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Scanner turns ───────────────────────────────────────────────────────────\n\nconst SCANNER_TURNS_SQL = `\n SELECT t.id, t.session_id, t.sync_id, t.source, t.turn_index, t.timestamp_ms,\n t.model, t.role, t.content_preview,\n t.input_tokens, t.output_tokens, t.cache_read_tokens,\n t.cache_creation_tokens, t.reasoning_tokens,\n s.cli_version\n FROM scanner_turns t\n LEFT JOIN sessions s ON s.session_id = t.session_id\n WHERE t.id > ?\n ORDER BY t.id\n LIMIT ?\n`;\n\nexport function readScannerTurns(\n afterId: number,\n limit: number,\n): { rows: ScannerTurnRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(SCANNER_TURNS_SQL).all(afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n source: string;\n turn_index: number;\n timestamp_ms: number;\n model: string | null;\n role: string | null;\n content_preview: string | null;\n input_tokens: number;\n output_tokens: number;\n cache_read_tokens: number;\n cache_creation_tokens: number;\n reasoning_tokens: number;\n cli_version: string | null;\n }>;\n\n const rows: ScannerTurnRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n source: r.source,\n turnIndex: r.turn_index,\n timestampMs: r.timestamp_ms,\n model: r.model,\n role: r.role,\n contentPreview: r.content_preview,\n inputTokens: r.input_tokens,\n outputTokens: r.output_tokens,\n cacheReadTokens: r.cache_read_tokens,\n cacheCreationTokens: r.cache_creation_tokens,\n reasoningTokens: r.reasoning_tokens,\n cliVersion: r.cli_version,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Scanner events ──────────────────────────────────────────────────────────\n\nconst SCANNER_EVENTS_SQL = `\n SELECT id, session_id, sync_id, source, event_type, timestamp_ms,\n tool_name, tool_input, tool_output, content, metadata\n FROM scanner_events\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readScannerEvents(\n afterId: number,\n limit: number,\n): { rows: ScannerEventRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(SCANNER_EVENTS_SQL).all(afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n source: string;\n event_type: string;\n timestamp_ms: number;\n tool_name: string | null;\n tool_input: string | null;\n tool_output: string | null;\n content: string | null;\n metadata: string | null;\n }>;\n\n const rows: ScannerEventRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n source: r.source,\n eventType: r.event_type,\n timestampMs: r.timestamp_ms,\n toolName: r.tool_name,\n toolInput: r.tool_input,\n toolOutput: r.tool_output,\n content: r.content,\n metadata: parseJson(r.metadata),\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── OTLP spans ─────────────────────────────────────────────────────────────\n\nconst SPANS_SQL = `\n SELECT id, trace_id, span_id, parent_span_id, name, kind,\n start_time_ns, end_time_ns, status_code, status_message,\n attributes, resource_attributes, session_id\n FROM otel_spans\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readOtelSpans(\n afterId: number,\n limit: number,\n): { rows: OtelSpanRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(SPANS_SQL).all(afterId, limit) as Array<{\n id: number;\n trace_id: string;\n span_id: string;\n parent_span_id: string | null;\n name: string;\n kind: number | null;\n start_time_ns: number;\n end_time_ns: number;\n status_code: number | null;\n status_message: string | null;\n attributes: string | null;\n resource_attributes: string | null;\n session_id: string | null;\n }>;\n\n const rows: OtelSpanRecord[] = rawRows.map((r) => ({\n id: r.id,\n traceId: r.trace_id,\n spanId: r.span_id,\n parentSpanId: r.parent_span_id,\n name: r.name,\n kind: r.kind,\n startTimeNs: r.start_time_ns,\n endTimeNs: r.end_time_ns,\n statusCode: r.status_code,\n statusMessage: r.status_message,\n attributes: parseJson(r.attributes),\n resourceAttributes: parseJson(r.resource_attributes),\n sessionId: r.session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── JSON parse helpers for config snapshots ─────────────────────────────────\n\nfunction parseJsonArray(raw: string | null): unknown[] {\n if (!raw) return [];\n try {\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? parsed : [];\n } catch {\n return [];\n }\n}\n\nfunction parseJsonObject(raw: string | null): Record<string, unknown> {\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw);\n return typeof parsed === \"object\" &&\n parsed !== null &&\n !Array.isArray(parsed)\n ? parsed\n : {};\n } catch {\n return {};\n }\n}\n\n// ── User config snapshots ──────────────────────────────────────────────────\n\nconst USER_CONFIG_SQL = `\n SELECT id, device_name, snapshot_at_ms, content_hash,\n permissions, enabled_plugins, hooks, commands, rules, skills, plugin_hooks\n FROM user_config_snapshots\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readUserConfigSnapshots(\n afterId: number,\n limit: number,\n): { rows: UserConfigSnapshotRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(USER_CONFIG_SQL).all(afterId, limit) as Array<{\n id: number;\n device_name: string;\n snapshot_at_ms: number;\n content_hash: string;\n permissions: string | null;\n enabled_plugins: string | null;\n hooks: string | null;\n commands: string | null;\n rules: string | null;\n skills: string | null;\n plugin_hooks: string | null;\n }>;\n\n const rows: UserConfigSnapshotRecord[] = rawRows.map((r) => ({\n id: r.id,\n deviceName: r.device_name,\n snapshotAtMs: r.snapshot_at_ms,\n contentHash: r.content_hash,\n permissions: parseJsonObject(r.permissions),\n enabledPlugins: parseJsonArray(r.enabled_plugins),\n hooks: parseJsonArray(r.hooks),\n commands: parseJsonArray(r.commands),\n rules: parseJsonArray(r.rules),\n skills: parseJsonArray(r.skills),\n pluginHooks: parseJsonArray(r.plugin_hooks),\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Repo config snapshots ──────────────────────────────────────────────────\n\nconst REPO_CONFIG_SQL = `\n SELECT id, repository, cwd, session_id, snapshot_at_ms, content_hash,\n hooks, mcp_servers, commands, agents, rules,\n local_hooks, local_mcp_servers, local_permissions,\n local_is_gitignored, instructions\n FROM repo_config_snapshots\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readRepoConfigSnapshots(\n afterId: number,\n limit: number,\n): { rows: RepoConfigSnapshotRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(REPO_CONFIG_SQL).all(afterId, limit) as Array<{\n id: number;\n repository: string;\n cwd: string;\n session_id: string | null;\n snapshot_at_ms: number;\n content_hash: string;\n hooks: string | null;\n mcp_servers: string | null;\n commands: string | null;\n agents: string | null;\n rules: string | null;\n local_hooks: string | null;\n local_mcp_servers: string | null;\n local_permissions: string | null;\n local_is_gitignored: number;\n instructions: string | null;\n }>;\n\n const rows: RepoConfigSnapshotRecord[] = rawRows.map((r) => ({\n id: r.id,\n repository: r.repository,\n cwd: r.cwd,\n sessionId: r.session_id,\n snapshotAtMs: r.snapshot_at_ms,\n contentHash: r.content_hash,\n hooks: parseJsonArray(r.hooks),\n mcpServers: parseJsonArray(r.mcp_servers),\n commands: parseJsonArray(r.commands),\n agents: parseJsonArray(r.agents),\n rules: parseJsonArray(r.rules),\n localHooks: parseJsonArray(r.local_hooks),\n localMcpServers: parseJsonArray(r.local_mcp_servers),\n localPermissions: parseJsonObject(r.local_permissions),\n localIsGitignored: r.local_is_gitignored === 1,\n instructions: parseJsonArray(r.instructions),\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Messages ────────────────────────────────────────────────────────────────\n\nconst MESSAGES_SQL = `\n SELECT id, 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\n FROM messages\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readMessages(\n afterId: number,\n limit: number,\n): { rows: MessageSyncRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(MESSAGES_SQL).all(afterId, limit) as Array<{\n id: number;\n session_id: string;\n ordinal: number;\n role: string;\n content: string;\n timestamp_ms: number | null;\n has_thinking: number;\n has_tool_use: number;\n content_length: number;\n is_system: number;\n model: string;\n token_usage: string;\n context_tokens: number;\n output_tokens: number;\n has_context_tokens: number;\n has_output_tokens: number;\n }>;\n\n const rows: MessageSyncRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n ordinal: r.ordinal,\n role: r.role,\n content: r.content,\n timestampMs: r.timestamp_ms,\n hasThinking: r.has_thinking === 1,\n hasToolUse: r.has_tool_use === 1,\n contentLength: r.content_length,\n isSystem: r.is_system === 1,\n model: r.model,\n tokenUsage: r.token_usage,\n contextTokens: r.context_tokens,\n outputTokens: r.output_tokens,\n hasContextTokens: r.has_context_tokens === 1,\n hasOutputTokens: r.has_output_tokens === 1,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Tool calls ──────────────────────────────────────────────────────────────\n\nconst TOOL_CALLS_SQL = `\n SELECT id, message_id, session_id, sync_id, tool_name, category, tool_use_id,\n input_json, skill_name, result_content_length, result_content,\n subagent_session_id\n FROM tool_calls\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n`;\n\nexport function readToolCalls(\n afterId: number,\n limit: number,\n): { rows: ToolCallSyncRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db.prepare(TOOL_CALLS_SQL).all(afterId, limit) as Array<{\n id: number;\n message_id: number;\n session_id: string;\n sync_id: string | null;\n tool_name: string;\n category: string;\n tool_use_id: string | null;\n input_json: string | null;\n skill_name: string | null;\n result_content_length: number | null;\n result_content: string | null;\n subagent_session_id: string | null;\n }>;\n\n const rows: ToolCallSyncRecord[] = rawRows.map((r) => ({\n id: r.id,\n messageId: r.message_id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n toolName: r.tool_name,\n category: r.category,\n toolUseId: r.tool_use_id,\n inputJson: r.input_json,\n skillName: r.skill_name,\n resultContentLength: r.result_content_length,\n resultContent: r.result_content,\n subagentSessionId: r.subagent_session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n// ── Sessions (by ID, no global watermark) ───────────────────────────────────\n\n/**\n * Read specific sessions by ID for syncing.\n * Sessions don't use a global watermark — the sync loop finds which sessions\n * need syncing by comparing sessions.sync_seq against target_session_sync.\n */\nexport function readSessionsByIds(sessionIds: string[]): SessionSyncRecord[] {\n if (sessionIds.length === 0) return [];\n\n const db = getDb();\n const placeholders = sessionIds.map(() => \"?\").join(\", \");\n\n const rawRows = db\n .prepare(\n `SELECT session_id, target, started_at_ms, ended_at_ms, cwd, first_prompt,\n permission_mode, agent_version,\n total_input_tokens, total_output_tokens, total_cache_read_tokens,\n total_cache_creation_tokens, total_reasoning_tokens, turn_count,\n models, summary, tool_counts, hook_tool_counts, event_type_counts, hook_event_type_counts, sync_seq,\n project, machine, message_count, user_message_count,\n parent_session_id, relationship_type, is_automated, created_at\n FROM sessions\n WHERE session_id IN (${placeholders})`,\n )\n .all(...sessionIds) as Array<{\n session_id: string;\n target: string | null;\n started_at_ms: number | null;\n ended_at_ms: number | null;\n cwd: string | null;\n first_prompt: string | null;\n permission_mode: string | null;\n agent_version: string | null;\n total_input_tokens: number | null;\n total_output_tokens: number | null;\n total_cache_read_tokens: number | null;\n total_cache_creation_tokens: number | null;\n total_reasoning_tokens: number | null;\n turn_count: number | null;\n models: string | null;\n summary: string | null;\n tool_counts: string | null;\n hook_tool_counts: string | null;\n event_type_counts: string | null;\n hook_event_type_counts: string | null;\n sync_seq: number;\n project: string | null;\n machine: string;\n message_count: number;\n user_message_count: number;\n parent_session_id: string | null;\n relationship_type: string;\n is_automated: number;\n created_at: number | null;\n }>;\n\n if (rawRows.length === 0) return [];\n\n const repoRows = db\n .prepare(\n `SELECT session_id, repository, first_seen_ms, git_user_name, git_user_email, branch\n FROM session_repositories\n WHERE session_id IN (${placeholders})`,\n )\n .all(...sessionIds) as Array<{\n session_id: string;\n repository: string;\n first_seen_ms: number;\n git_user_name: string | null;\n git_user_email: string | null;\n branch: string | null;\n }>;\n\n const cwdRows = db\n .prepare(\n `SELECT session_id, cwd, first_seen_ms\n FROM session_cwds\n WHERE session_id IN (${placeholders})`,\n )\n .all(...sessionIds) as Array<{\n session_id: string;\n cwd: string;\n first_seen_ms: number;\n }>;\n\n const reposBySession = new Map<string, SessionSyncRecord[\"repositories\"]>();\n for (const r of repoRows) {\n const list = reposBySession.get(r.session_id) ?? [];\n list.push({\n repository: r.repository,\n firstSeenMs: r.first_seen_ms,\n gitUserName: r.git_user_name,\n gitUserEmail: r.git_user_email,\n branch: r.branch,\n });\n reposBySession.set(r.session_id, list);\n }\n\n const cwdsBySession = new Map<string, SessionSyncRecord[\"cwds\"]>();\n for (const r of cwdRows) {\n const list = cwdsBySession.get(r.session_id) ?? [];\n list.push({ cwd: r.cwd, firstSeenMs: r.first_seen_ms });\n cwdsBySession.set(r.session_id, list);\n }\n\n return rawRows.map((r) => ({\n sessionId: r.session_id,\n target: r.target,\n startedAtMs: r.started_at_ms,\n endedAtMs: r.ended_at_ms,\n cwd: r.cwd,\n firstPrompt: r.first_prompt,\n permissionMode: r.permission_mode,\n agentVersion: r.agent_version,\n totalInputTokens: r.total_input_tokens,\n totalOutputTokens: r.total_output_tokens,\n totalCacheReadTokens: r.total_cache_read_tokens,\n totalCacheCreationTokens: r.total_cache_creation_tokens,\n totalReasoningTokens: r.total_reasoning_tokens,\n turnCount: r.turn_count,\n models: r.models,\n summary: r.summary,\n toolCounts: parseJsonObject(r.tool_counts) as Record<string, number>,\n hookToolCounts: parseJsonObject(r.hook_tool_counts) as Record<\n string,\n number\n >,\n eventTypeCounts: parseJsonObject(r.event_type_counts) as Record<\n string,\n number\n >,\n hookEventTypeCounts: parseJsonObject(r.hook_event_type_counts) as Record<\n string,\n number\n >,\n project: r.project,\n machine: r.machine,\n messageCount: r.message_count,\n userMessageCount: r.user_message_count,\n parentSessionId: r.parent_session_id,\n relationshipType: r.relationship_type,\n isAutomated: r.is_automated === 1,\n createdAt: r.created_at,\n repositories: reposBySession.get(r.session_id) ?? [],\n cwds: cwdsBySession.get(r.session_id) ?? [],\n }));\n}\n\n// ── Per-session readers (for gated sync) ──────────────────────────────────\n\nexport function readSessionMessages(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: MessageSyncRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, 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\n FROM messages\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n session_id: string;\n ordinal: number;\n role: string;\n content: string;\n timestamp_ms: number | null;\n has_thinking: number;\n has_tool_use: number;\n content_length: number;\n is_system: number;\n model: string;\n token_usage: string;\n context_tokens: number;\n output_tokens: number;\n has_context_tokens: number;\n has_output_tokens: number;\n }>;\n\n const rows: MessageSyncRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n ordinal: r.ordinal,\n role: r.role,\n content: r.content,\n timestampMs: r.timestamp_ms,\n hasThinking: r.has_thinking === 1,\n hasToolUse: r.has_tool_use === 1,\n contentLength: r.content_length,\n isSystem: r.is_system === 1,\n model: r.model,\n tokenUsage: r.token_usage,\n contextTokens: r.context_tokens,\n outputTokens: r.output_tokens,\n hasContextTokens: r.has_context_tokens === 1,\n hasOutputTokens: r.has_output_tokens === 1,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionToolCalls(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: ToolCallSyncRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, message_id, session_id, sync_id, tool_name, category, tool_use_id,\n input_json, skill_name, result_content_length, result_content,\n subagent_session_id\n FROM tool_calls\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n message_id: number;\n session_id: string;\n sync_id: string | null;\n tool_name: string;\n category: string;\n tool_use_id: string | null;\n input_json: string | null;\n skill_name: string | null;\n result_content_length: number | null;\n result_content: string | null;\n subagent_session_id: string | null;\n }>;\n\n const rows: ToolCallSyncRecord[] = rawRows.map((r) => ({\n id: r.id,\n messageId: r.message_id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n toolName: r.tool_name,\n category: r.category,\n toolUseId: r.tool_use_id,\n inputJson: r.input_json,\n skillName: r.skill_name,\n resultContentLength: r.result_content_length,\n resultContent: r.result_content,\n subagentSessionId: r.subagent_session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionScannerTurns(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: ScannerTurnRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT t.id, t.session_id, t.sync_id, t.source, t.turn_index, t.timestamp_ms,\n t.model, t.role, t.content_preview,\n t.input_tokens, t.output_tokens, t.cache_read_tokens,\n t.cache_creation_tokens, t.reasoning_tokens,\n s.cli_version\n FROM scanner_turns t\n LEFT JOIN sessions s ON s.session_id = t.session_id\n WHERE t.session_id = ? AND t.id > ?\n ORDER BY t.id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n source: string;\n turn_index: number;\n timestamp_ms: number;\n model: string | null;\n role: string | null;\n content_preview: string | null;\n input_tokens: number;\n output_tokens: number;\n cache_read_tokens: number;\n cache_creation_tokens: number;\n reasoning_tokens: number;\n cli_version: string | null;\n }>;\n\n const rows: ScannerTurnRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n source: r.source,\n turnIndex: r.turn_index,\n timestampMs: r.timestamp_ms,\n model: r.model,\n role: r.role,\n contentPreview: r.content_preview,\n inputTokens: r.input_tokens,\n outputTokens: r.output_tokens,\n cacheReadTokens: r.cache_read_tokens,\n cacheCreationTokens: r.cache_creation_tokens,\n reasoningTokens: r.reasoning_tokens,\n cliVersion: r.cli_version,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionScannerEvents(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: ScannerEventRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, session_id, sync_id, source, event_type, timestamp_ms,\n tool_name, tool_input, tool_output, content, metadata\n FROM scanner_events\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n source: string;\n event_type: string;\n timestamp_ms: number;\n tool_name: string | null;\n tool_input: string | null;\n tool_output: string | null;\n content: string | null;\n metadata: string | null;\n }>;\n\n const rows: ScannerEventRecord[] = rawRows.map((r) => ({\n id: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n source: r.source,\n eventType: r.event_type,\n timestampMs: r.timestamp_ms,\n toolName: r.tool_name,\n toolInput: r.tool_input,\n toolOutput: r.tool_output,\n content: r.content,\n metadata: parseJson(r.metadata),\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionHookEvents(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: HookEventRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT h.id, h.session_id, h.sync_id, h.event_type, h.timestamp_ms, h.cwd, h.repository,\n h.tool_name, decompress(h.payload) as payload,\n h.user_prompt, h.file_path, h.command, h.tool_result,\n s.target\n FROM hook_events h\n LEFT JOIN sessions s ON s.session_id = h.session_id\n WHERE h.session_id = ? AND h.id > ?\n ORDER BY h.id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n session_id: string;\n sync_id: string | null;\n event_type: string;\n timestamp_ms: number;\n cwd: string | null;\n repository: string | null;\n tool_name: string | null;\n payload: string | null;\n user_prompt: string | null;\n file_path: string | null;\n command: string | null;\n tool_result: string | null;\n target: string | null;\n }>;\n\n const rows: HookEventRecord[] = rawRows.map((r) => ({\n hookId: r.id,\n sessionId: r.session_id,\n syncId: r.sync_id,\n eventType: r.event_type,\n timestampMs: r.timestamp_ms,\n cwd: r.cwd,\n repository: r.repository,\n toolName: r.tool_name,\n payload: parseJson(r.payload),\n userPrompt: r.user_prompt,\n filePath: r.file_path,\n command: r.command,\n toolResult: r.tool_result,\n target: r.target,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].hookId : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionOtelLogs(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: OtelLogRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, sync_id, timestamp_ns, body, attributes, resource_attributes,\n severity_text, session_id, prompt_id, trace_id, span_id\n FROM otel_logs\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as RawOtelLogRow[];\n\n const rows = mapOtelRows(rawRows);\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionOtelMetrics(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: MetricRow[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, sync_id, timestamp_ns, name, value, metric_type, unit,\n attributes, resource_attributes, session_id\n FROM otel_metrics\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n sync_id: string | null;\n timestamp_ns: number;\n name: string;\n value: number;\n metric_type: string | null;\n unit: string | null;\n attributes: string | null;\n resource_attributes: string | null;\n session_id: string | null;\n }>;\n\n const rows: MetricRow[] = rawRows.map((r) => ({\n id: r.id,\n syncId: r.sync_id,\n timestampNs: r.timestamp_ns,\n name: r.name,\n value: r.value,\n metricType: r.metric_type,\n unit: r.unit,\n attributes: parseJson(r.attributes),\n resourceAttributes: parseJson(r.resource_attributes),\n sessionId: r.session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\nexport function readSessionOtelSpans(\n sessionId: string,\n afterId: number,\n limit: number,\n): { rows: OtelSpanRecord[]; maxId: number } {\n const db = getDb();\n const rawRows = db\n .prepare(\n `SELECT id, trace_id, span_id, parent_span_id, name, kind,\n start_time_ns, end_time_ns, status_code, status_message,\n attributes, resource_attributes, session_id\n FROM otel_spans\n WHERE session_id = ? AND id > ?\n ORDER BY id\n LIMIT ?`,\n )\n .all(sessionId, afterId, limit) as Array<{\n id: number;\n trace_id: string;\n span_id: string;\n parent_span_id: string | null;\n name: string;\n kind: number | null;\n start_time_ns: number;\n end_time_ns: number;\n status_code: number | null;\n status_message: string | null;\n attributes: string | null;\n resource_attributes: string | null;\n session_id: string | null;\n }>;\n\n const rows: OtelSpanRecord[] = rawRows.map((r) => ({\n id: r.id,\n traceId: r.trace_id,\n spanId: r.span_id,\n parentSpanId: r.parent_span_id,\n name: r.name,\n kind: r.kind,\n startTimeNs: r.start_time_ns,\n endTimeNs: r.end_time_ns,\n statusCode: r.status_code,\n statusMessage: r.status_message,\n attributes: parseJson(r.attributes),\n resourceAttributes: parseJson(r.resource_attributes),\n sessionId: r.session_id,\n }));\n\n const maxId = rows.length > 0 ? rows[rows.length - 1].id : afterId;\n return { rows, maxId };\n}\n\n/** Map of table name → per-session reader for gated sync. */\nexport const SESSION_READERS: Record<\n string,\n (\n sessionId: string,\n afterId: number,\n limit: number,\n ) => { rows: unknown[]; maxId: number }\n> = {\n messages: readSessionMessages,\n tool_calls: readSessionToolCalls,\n scanner_turns: readSessionScannerTurns,\n scanner_events: readSessionScannerEvents,\n hook_events: readSessionHookEvents,\n otel_logs: readSessionOtelLogs,\n otel_metrics: readSessionOtelMetrics,\n otel_spans: readSessionOtelSpans,\n};\n","import {\n readHookEvents,\n readMessages,\n readMetrics,\n readOtelLogs,\n readOtelSpans,\n readRepoConfigSnapshots,\n readScannerEvents,\n readScannerTurns,\n readToolCalls,\n readUserConfigSnapshots,\n} from \"./reader.js\";\nimport type { TableSyncDescriptor } from \"./types.js\";\n\n/**\n * Ordered list of table sync descriptors. The order matches the\n * round-robin execution order in the sync loop.\n *\n * All tables sync via POST /v1/sync with {table, rows} payload.\n * Session-linked tables are filtered by repo attribution in the sync loop.\n *\n * NOTE: Sessions are NOT in this registry — they use direct comparison\n * against target_session_sync instead of a global watermark (sync_seq is\n * per-session, not globally monotonic).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const TABLE_SYNC_REGISTRY: TableSyncDescriptor<any>[] = [\n // ── Session-linked tables (filtered by repo attribution) ─────────────────\n\n {\n table: \"messages\",\n logNoun: \"messages\",\n read: (afterId, limit) => readMessages(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"tool_calls\",\n logNoun: \"tool calls\",\n read: (afterId, limit) => readToolCalls(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"scanner_turns\",\n logNoun: \"turns\",\n read: (afterId, limit) => readScannerTurns(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"scanner_events\",\n logNoun: \"events\",\n read: (afterId, limit) => readScannerEvents(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"hook_events\",\n logNoun: \"events\",\n read: (afterId, limit) => readHookEvents(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"otel_logs\",\n logNoun: \"logs\",\n read: (afterId, limit) => readOtelLogs(afterId, limit, false),\n sessionLinked: true,\n },\n\n {\n table: \"otel_metrics\",\n logNoun: \"metrics\",\n read: (afterId, limit) => readMetrics(afterId, limit),\n sessionLinked: true,\n },\n\n {\n table: \"otel_spans\",\n logNoun: \"spans\",\n read: (afterId, limit) => readOtelSpans(afterId, limit),\n sessionLinked: true,\n },\n\n // ── Non-session tables (always synced) ────────────────────────────────────\n\n {\n table: \"user_config_snapshots\",\n logNoun: \"snapshots\",\n read: (afterId, limit) => readUserConfigSnapshots(afterId, limit),\n sessionLinked: false,\n },\n\n {\n table: \"repo_config_snapshots\",\n logNoun: \"snapshots\",\n read: (afterId, limit) => readRepoConfigSnapshots(afterId, limit),\n sessionLinked: false,\n },\n];\n","import { getDb } from \"../db/schema.js\";\nimport { TABLE_SYNC_REGISTRY } from \"./registry.js\";\n\nexport function watermarkKey(table: string, targetName: string): string {\n return `${table}:${targetName}`;\n}\n\nexport function readWatermark(key: string): number {\n const db = getDb();\n const row = db\n .prepare(\"SELECT value FROM watermarks WHERE key = ?\")\n .get(key) as { value: number } | undefined;\n return row?.value ?? 0;\n}\n\nexport function writeWatermark(key: string, value: number): void {\n const db = getDb();\n db.prepare(\n \"INSERT INTO watermarks (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value\",\n ).run(key, value);\n}\n\nexport function resetWatermarks(targetName?: string): void {\n const db = getDb();\n if (targetName) {\n const stmt = db.prepare(\"DELETE FROM watermarks WHERE key = ?\");\n for (const desc of TABLE_SYNC_REGISTRY) {\n stmt.run(watermarkKey(desc.table, targetName));\n }\n db.prepare(\"DELETE FROM target_session_sync WHERE target = ?\").run(\n targetName,\n );\n } else {\n db.prepare(\"DELETE FROM watermarks\").run();\n db.prepare(\"DELETE FROM target_session_sync\").run();\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { config, ensureDataDir } from \"./config.js\";\nimport type { SyncFilter, SyncTarget } from \"./sync/types.js\";\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\nexport interface RetentionConfig {\n maxAgeDays: number;\n maxSizeMb: number;\n /** Delete synced rows older than this many days. Undefined = disabled. */\n syncedMaxAgeDays?: number;\n}\n\nexport interface UnifiedConfig {\n /** Whether panopticon hooks are installed (plugin mode). When true, OTLP sync\n * filters out body types that hooks already cover to avoid double-counting. */\n hooksInstalled?: boolean;\n /** Sentry DSN for error reporting. When set, uncaught exceptions and key\n * error paths are reported to Sentry. */\n sentryDsn?: string;\n sync: {\n targets: SyncTarget[];\n filter?: SyncFilter;\n };\n retention: RetentionConfig;\n}\n\n// ── Defaults ─────────────────────────────────────────────────────────────────\n\nconst DEFAULT_RETENTION: RetentionConfig = {\n maxAgeDays: 90,\n maxSizeMb: 1000,\n};\n\nfunction defaultConfig(): UnifiedConfig {\n return { sync: { targets: [] }, retention: { ...DEFAULT_RETENTION } };\n}\n\n// ── Paths ────────────────────────────────────────────────────────────────────\n\nconst CONFIG_FILE = \"config.json\";\n\nfunction configPath(): string {\n return path.join(config.dataDir, CONFIG_FILE);\n}\n\n// ── Load / Save ──────────────────────────────────────────────────────────────\n\nfunction mergeDefaults(raw: Partial<UnifiedConfig>): UnifiedConfig {\n const retention: RetentionConfig = {\n ...DEFAULT_RETENTION,\n ...(raw.retention ?? {}),\n };\n return {\n hooksInstalled: raw.hooksInstalled,\n sentryDsn: raw.sentryDsn,\n sync: raw.sync ?? { targets: [] },\n retention,\n };\n}\n\nexport function loadUnifiedConfig(): UnifiedConfig {\n try {\n const raw = JSON.parse(fs.readFileSync(configPath(), \"utf-8\"));\n return mergeDefaults(raw);\n } catch {\n return defaultConfig();\n }\n}\n\nexport function saveUnifiedConfig(cfg: UnifiedConfig): void {\n ensureDataDir();\n const p = configPath();\n fs.writeFileSync(p, `${JSON.stringify(cfg, null, 2)}\\n`, { mode: 0o600 });\n}\n\n// ── Convenience ──────────────────────────────────────────────────────────────\n\nexport function loadRetentionConfig(): RetentionConfig {\n return loadUnifiedConfig().retention;\n}\n"],"mappings":"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","names":["fs","fs","path"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/native-check.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/repo.ts","../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/workspaces/superset.ts","../src/convex-client.ts"],"sourcesContent":["import { createRequire } from \"node:module\";\nimport { execSync } from \"node:child_process\";\nimport { rmSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Verify that better-sqlite3's native addon matches the running Node version.\n * If mismatched (e.g. user upgraded Node), attempt an automatic rebuild.\n * Call this before any code that imports better-sqlite3.\n */\nexport function ensureNativeModules(): void {\n const require = createRequire(import.meta.url);\n try {\n require(\"better-sqlite3\");\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"\";\n if (!msg.includes(\"NODE_MODULE_VERSION\")) {\n throw err;\n }\n\n console.error(\n \"[fml] better-sqlite3 was compiled for a different Node.js version, rebuilding...\",\n );\n\n const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), \"..\");\n\n // Remove stale build directory to avoid EEXIST errors from node-gyp\n const sqlitePath = dirname(require.resolve(\"better-sqlite3/package.json\"));\n rmSync(join(sqlitePath, \"build\"), { recursive: true, force: true });\n\n try {\n execSync(\"npm rebuild better-sqlite3\", {\n cwd: pkgRoot,\n stdio: \"inherit\",\n timeout: 60_000,\n });\n console.error(\"[fml] Rebuild complete.\");\n } catch {\n console.error(\n \"[fml] Automatic rebuild failed. Please run manually:\\n\" +\n ` cd ${pkgRoot} && rm -rf node_modules/better-sqlite3/build && npm rebuild better-sqlite3`,\n );\n process.exit(1);\n }\n }\n}\n","import { execFileSync } from \"node:child_process\";\nimport { SupersetProvider } from \"./workspaces/superset.js\";\nimport type { WorkspaceProvider } from \"./workspaces/types.js\";\n\nexport interface RepoInfo {\n repo: string;\n branch?: string | null;\n}\n\n// Cache: cwd → RepoInfo | null\nconst repoCache = new Map<string, RepoInfo | null>();\n\n// Registered workspace providers — checked in order when git fails.\nconst providers: WorkspaceProvider[] = [new SupersetProvider()];\n\n/** Resolve \"org/repo\" from a directory's git remote origin URL. */\nfunction resolveGitRemote(dir: string): string | null {\n try {\n const url = execFileSync(\n \"git\",\n [\"-C\", dir, \"remote\", \"get-url\", \"origin\"],\n {\n encoding: \"utf-8\",\n timeout: 5000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n },\n ).trim();\n\n // SSH: git@github.com:org/repo.git\n const sshMatch = url.match(/git@[^:]+:([^/]+\\/[^/]+?)(?:\\.git)?$/);\n if (sshMatch) return sshMatch[1];\n\n // HTTPS: https://github.com/org/repo.git\n const httpsMatch = url.match(/\\/([^/]+\\/[^/]+?)(?:\\.git)?$/);\n if (httpsMatch) return httpsMatch[1];\n } catch {\n // Not a git repo, no remote, etc.\n }\n return null;\n}\n\n/** Resolve the current git branch for a directory. */\nfunction resolveGitBranch(dir: string): string | null {\n try {\n return (\n execFileSync(\"git\", [\"-C\", dir, \"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\n encoding: \"utf-8\",\n timeout: 5000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null\n );\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the GitHub \"org/repo\" and branch for a working directory.\n * Results are cached for the lifetime of the process.\n *\n * 1. Try git directly on the CWD\n * 2. On failure, ask registered workspace providers (e.g. Superset)\n * for an alternative repo directory to resolve against\n */\nexport function resolveRepoFromCwd(cwd: string): RepoInfo | null {\n if (repoCache.has(cwd)) return repoCache.get(cwd)!;\n\n let result: RepoInfo | null = null;\n\n const repo = resolveGitRemote(cwd);\n if (repo) {\n result = { repo, branch: resolveGitBranch(cwd) };\n } else {\n // Ask workspace providers for a fallback\n for (const provider of providers) {\n if (!provider.canResolve(cwd)) continue;\n const resolved = provider.resolve(cwd);\n if (!resolved) continue;\n\n // Provider returned repo name directly (no git needed)\n if (resolved.repo) {\n result = { repo: resolved.repo, branch: resolved.branch };\n break;\n }\n\n // Provider returned a directory to resolve via git\n if (resolved.repoDir) {\n const fallbackRepo = resolveGitRemote(resolved.repoDir);\n if (fallbackRepo) {\n result = { repo: fallbackRepo, branch: resolved.branch };\n break;\n }\n }\n }\n }\n\n repoCache.set(cwd, result);\n return result;\n}\n\n// Cache: cwd → git identity\nconst gitIdentityCache = new Map<\n string,\n { name: string | null; email: string | null }\n>();\n\n/** Resolve git user.name / user.email for a directory. Cached per-process. */\nexport function resolveGitIdentity(cwd: string): {\n name: string | null;\n email: string | null;\n} {\n const cached = gitIdentityCache.get(cwd);\n if (cached) return cached;\n\n const result = { name: null as string | null, email: null as string | null };\n try {\n result.name =\n execFileSync(\"git\", [\"-C\", cwd, \"config\", \"user.name\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null;\n } catch {\n // no user.name configured\n }\n try {\n result.email =\n execFileSync(\"git\", [\"-C\", cwd, \"config\", \"user.email\"], {\n encoding: \"utf-8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim() || null;\n } catch {\n // no user.email configured\n }\n gitIdentityCache.set(cwd, result);\n return result;\n}\n\n/** Reset caches (for testing). */\nexport function _resetRepoCache(): void {\n repoCache.clear();\n gitIdentityCache.clear();\n for (const p of providers) p.close?.();\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport { Database } from \"../db/driver.js\";\nimport type { WorkspaceProvider } from \"./types.js\";\n\nconst SUPERSET_MARKER = `${path.sep}.superset${path.sep}`;\nconst SUPERSET_DB_PATH = path.join(os.homedir(), \".superset\", \"local.db\");\n\ninterface WorktreeRow {\n main_repo_path: string;\n branch: string | null;\n github_owner: string | null;\n project_name: string;\n}\n\nexport class SupersetProvider implements WorkspaceProvider {\n readonly id = \"superset\";\n\n private db: Database | null = null;\n private dbFailed = false;\n\n canResolve(cwd: string): boolean {\n return cwd.includes(SUPERSET_MARKER);\n }\n\n resolve(\n cwd: string,\n ): { repo?: string; repoDir?: string; branch?: string | null } | null {\n const db = this.getDb();\n if (!db) return null;\n\n try {\n // Try worktrees table first (for ~/.superset/worktrees/ paths)\n const wt = db\n .prepare(\n `SELECT p.main_repo_path, w.branch, p.github_owner, p.name as project_name\n FROM worktrees w\n JOIN projects p ON w.project_id = p.id\n WHERE ? LIKE w.path || '%'\n ORDER BY LENGTH(w.path) DESC\n LIMIT 1`,\n )\n .get(cwd) as WorktreeRow | undefined;\n if (wt) return this.buildResult(wt);\n\n // Fall back to projects table (for ~/.superset/projects/ paths)\n const proj = db\n .prepare(\n `SELECT main_repo_path, default_branch AS branch, github_owner, name as project_name\n FROM projects\n WHERE ? LIKE main_repo_path || '%'\n ORDER BY LENGTH(main_repo_path) DESC\n LIMIT 1`,\n )\n .get(cwd) as WorktreeRow | undefined;\n if (proj) return this.buildResult(proj);\n } catch {\n // Schema mismatch, corrupt DB, etc.\n }\n\n return null;\n }\n\n close(): void {\n if (this.db) {\n try {\n this.db.close();\n } catch {}\n this.db = null;\n }\n this.dbFailed = false;\n }\n\n private buildResult(row: WorktreeRow): {\n repo?: string;\n repoDir?: string;\n branch?: string | null;\n } {\n // Prefer deriving repo name directly from Superset DB (no git needed)\n if (row.github_owner) {\n return {\n repo: `${row.github_owner}/${row.project_name}`,\n branch: row.branch,\n };\n }\n // Fall back to git resolution on the main repo path\n return { repoDir: row.main_repo_path, branch: row.branch };\n }\n\n private getDb(): Database | null {\n if (this.dbFailed) return null;\n if (this.db) return this.db;\n try {\n this.db = new Database(SUPERSET_DB_PATH, {\n readonly: true,\n fileMustExist: true,\n });\n return this.db;\n } catch {\n this.dbFailed = true;\n return null;\n }\n }\n}\n","import type { FunctionReference } from \"convex/server\";\nimport { resolveRepoFromCwd } from \"@fml-inc/panopticon/repo\";\nimport { getSelectedOrg, getValidToken } from \"./auth/token-store.js\";\nimport { CONVEX_URL } from \"./config.js\";\nimport type {\n RepoConfigSnapshotDetail,\n RepoConfigSnapshotSummary,\n ResolvedRepo,\n UserConfigSnapshotDetail,\n UserConfigSnapshotSummary,\n} from \"./types.js\";\n\n// ── Shared plumbing ─────────────────────────────────────────────────────────\n\nexport interface ToolResult {\n ok: boolean;\n result?: unknown;\n error?: string;\n}\n\nexport interface OrgInfo {\n _id: string;\n name: string;\n slug?: string;\n repos?: Array<{\n _id: string;\n fullName: string;\n owner: string;\n name: string;\n private: boolean;\n }>;\n}\n\nfunction ref<T extends \"query\" | \"mutation\" | \"action\">(\n path: string,\n): FunctionReference<T> {\n return path as unknown as FunctionReference<T>;\n}\n\n// ── API client factory ──────────────────────────────────────────────────────\n\nexport function createApiClient(token: string) {\n const isServiceToken = token.startsWith(\"fml_st_\");\n\n let clientPromise: Promise<{\n query: <T>(\n ref: FunctionReference<\"query\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n mutation: <T>(\n ref: FunctionReference<\"mutation\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n action: <T>(\n ref: FunctionReference<\"action\">,\n args: Record<string, unknown>,\n ) => Promise<T>;\n }> | null = null;\n\n function getClient() {\n if (!clientPromise) {\n clientPromise = import(\"convex/browser\").then(({ ConvexHttpClient }) => {\n const client = new ConvexHttpClient(CONVEX_URL);\n client.setAuth(token);\n return client;\n });\n }\n return clientPromise;\n }\n\n /**\n * Derive the Convex site URL (HTTP actions) from the cloud URL.\n * Convex uses paired domains: *.convex.cloud for client APIs,\n * *.convex.site for HTTP actions. This is a stable Convex convention.\n */\n function getSiteUrl(): string {\n return CONVEX_URL.replace(\".convex.cloud\", \".convex.site\").replace(\n /\\/$/,\n \"\",\n );\n }\n\n return {\n // ── Orgs ──────────────────────────────────────────────────────────────\n\n async queryOrgs(): Promise<OrgInfo[]> {\n if (isServiceToken) {\n // Service tokens can't call Convex actions directly — org is embedded in the token\n return [];\n }\n const client = await getClient();\n return await client.query(ref(\"user/plugin:getMyOrgsAndRepos\"), {});\n },\n\n // ── Tool gateway ─────────────────────────────────────────────────────\n\n async callBackend(\n toolName: string,\n args: Record<string, unknown>,\n opts?: { org?: string },\n ): Promise<ToolResult> {\n try {\n if (isServiceToken) {\n // Service token path: POST to HTTP endpoint (Convex can't validate non-JWT tokens)\n // Thread user identity: sandbox agents set FML_USER_EXTERNAL_ID;\n // device-flow logins fall back to the id stashed on the stored token\n // so tools can execute with the real user context even though the\n // token's actAsExternalId is the namespaced system:cli:* string.\n const { readTokens } = await import(\"./auth/token-store.js\");\n const userExternalId =\n process.env.FML_USER_EXTERNAL_ID ?? readTokens()?.user.id;\n const res = await fetch(`${getSiteUrl()}/api/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n toolName,\n args,\n ...(userExternalId && { userExternalId }),\n }),\n });\n const text = await res.text();\n let data: ToolResult;\n try {\n data = JSON.parse(text) as ToolResult;\n } catch {\n return {\n ok: false,\n error: `HTTP ${res.status}: ${text.slice(0, 200)}`,\n };\n }\n if (!res.ok && !data.error) {\n return { ok: false, error: `HTTP ${res.status}` };\n }\n return data;\n }\n\n // JWT path: standard Convex action\n const client = await getClient();\n const actionArgs: Record<string, unknown> = { toolName, args };\n // Explicit org > stored org selection > repo-based inference\n const org = opts?.org ?? getSelectedOrg();\n if (org) actionArgs.org = org;\n const repo = resolveRepoFromCwd(process.cwd());\n if (repo) actionArgs.repo = repo.repo;\n\n const result = await client.action(\n ref<\"action\">(\"user/tool_gateway:executeTool\"),\n actionArgs,\n );\n return { ok: true, result };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes(\"Unauthorized\") || msg.includes(\"not authenticated\")) {\n return {\n ok: false,\n error:\n \"Authentication expired. Run `fml login` to sign in again, then restart Claude Code.\",\n };\n }\n return { ok: false, error: msg };\n }\n },\n\n // ── Repo resolution ──────────────────────────────────────────────────\n\n async resolveRepo(\n orgSlug: string,\n repoFullName: string,\n ): Promise<ResolvedRepo | null> {\n const client = await getClient();\n return await client.query(ref(\"user/cli:resolveRepo\"), {\n orgSlug,\n repoFullName,\n });\n },\n\n // ── Config snapshots ─────────────────────────────────────────────────\n\n async listUserConfigSnapshots(\n orgSlug: string,\n ): Promise<UserConfigSnapshotSummary[]> {\n const client = await getClient();\n const result = await client.query<UserConfigSnapshotSummary[] | null>(\n ref(\"user/config_snapshots:listUserSnapshots\"),\n { orgSlug },\n );\n return result ?? [];\n },\n\n async getUserConfigDetail(\n orgSlug: string,\n githubUsername: string,\n ): Promise<UserConfigSnapshotDetail | null> {\n const client = await getClient();\n return await client.query(ref(\"user/config_snapshots:getUserDetail\"), {\n orgSlug,\n githubUsername,\n });\n },\n\n async listRepoConfigSnapshots(\n orgSlug: string,\n repository?: string,\n ): Promise<RepoConfigSnapshotSummary[]> {\n const client = await getClient();\n const result = await client.query<RepoConfigSnapshotSummary[] | null>(\n ref(\"user/config_snapshots:listRepoSnapshots\"),\n { orgSlug, repository },\n );\n return result ?? [];\n },\n\n async getRepoConfigDetail(\n orgSlug: string,\n repository: string,\n ): Promise<RepoConfigSnapshotDetail | null> {\n const client = await getClient();\n return await client.query(ref(\"user/config_snapshots:getRepoDetail\"), {\n orgSlug,\n repository,\n });\n },\n };\n}\n\n// ── Convenience: auto-authenticated client ──────────────────────────────────\n\n/**\n * Create an API client using the stored auth token.\n * Returns null if not authenticated.\n */\nexport async function getAuthenticatedClient() {\n const token = await getValidToken();\n if (!token) return null;\n return createApiClient(token);\n}\n"],"mappings":";;;;;;;;;;AAAA,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAOvB,SAAS,sBAA4B;AAC1C,QAAMA,WAAU,cAAc,YAAY,GAAG;AAC7C,MAAI;AACF,IAAAA,SAAQ,gBAAgB;AAAA,EAC1B,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU;AACjD,QAAI,CAAC,IAAI,SAAS,qBAAqB,GAAG;AACxC,YAAM;AAAA,IACR;AAEA,YAAQ;AAAA,MACN;AAAA,IACF;AAEA,UAAM,UAAU,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,IAAI;AAGlE,UAAM,aAAa,QAAQA,SAAQ,QAAQ,6BAA6B,CAAC;AACzE,WAAO,KAAK,YAAY,OAAO,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAElE,QAAI;AACF,eAAS,8BAA8B;AAAA,QACrC,KAAK;AAAA,QACL,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AACD,cAAQ,MAAM,yBAAyB;AAAA,IACzC,QAAQ;AACN,cAAQ;AAAA,QACN;AAAA,OACU,OAAO;AAAA,MACnB;AACA,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF;AACF;;;AC9CA,SAAS,oBAAoB;ACA7B,OAAO,QAAQ;AACf,OAAO,UAAU;AAIjB,IAAM,kBAAkB,GAAG,KAAK,GAAG,YAAY,KAAK,GAAG;AACvD,IAAM,mBAAmB,KAAK,KAAK,GAAG,QAAQ,GAAG,aAAa,UAAU;AASjE,IAAM,mBAAN,MAAoD;EAChD,KAAK;EAEN,KAAsB;EACtB,WAAW;EAEnB,WAAW,KAAsB;AAC/B,WAAO,IAAI,SAAS,eAAe;EACrC;EAEA,QACE,KACoE;AACpE,UAAM,KAAK,KAAK,MAAM;AACtB,QAAI,CAAC,GAAI,QAAO;AAEhB,QAAI;AAEF,YAAM,KAAK,GACR;QACC;;;;;;MAMF,EACC,IAAI,GAAG;AACV,UAAI,GAAI,QAAO,KAAK,YAAY,EAAE;AAGlC,YAAM,OAAO,GACV;QACC;;;;;MAKF,EACC,IAAI,GAAG;AACV,UAAI,KAAM,QAAO,KAAK,YAAY,IAAI;IACxC,QAAQ;IAER;AAEA,WAAO;EACT;EAEA,QAAc;AACZ,QAAI,KAAK,IAAI;AACX,UAAI;AACF,aAAK,GAAG,MAAM;MAChB,QAAQ;MAAC;AACT,WAAK,KAAK;IACZ;AACA,SAAK,WAAW;EAClB;EAEQ,YAAY,KAIlB;AAEA,QAAI,IAAI,cAAc;AACpB,aAAO;QACL,MAAM,GAAG,IAAI,YAAY,IAAI,IAAI,YAAY;QAC7C,QAAQ,IAAI;MACd;IACF;AAEA,WAAO,EAAE,SAAS,IAAI,gBAAgB,QAAQ,IAAI,OAAO;EAC3D;EAEQ,QAAyB;AAC/B,QAAI,KAAK,SAAU,QAAO;AAC1B,QAAI,KAAK,GAAI,QAAO,KAAK;AACzB,QAAI;AACF,WAAK,KAAK,IAAI,SAAS,kBAAkB;QACvC,UAAU;QACV,eAAe;MACjB,CAAC;AACD,aAAO,KAAK;IACd,QAAQ;AACN,WAAK,WAAW;AAChB,aAAO;IACT;EACF;AACF;AD7FA,IAAM,YAAY,oBAAI,IAA6B;AAGnD,IAAM,YAAiC,CAAC,IAAI,iBAAiB,CAAC;AAG9D,SAAS,iBAAiB,KAA4B;AACpD,MAAI;AACF,UAAM,MAAM;MACV;MACA,CAAC,MAAM,KAAK,UAAU,WAAW,QAAQ;MACzC;QACE,UAAU;QACV,SAAS;QACT,OAAO,CAAC,UAAU,QAAQ,QAAQ;MACpC;IACF,EAAE,KAAK;AAGP,UAAM,WAAW,IAAI,MAAM,sCAAsC;AACjE,QAAI,SAAU,QAAO,SAAS,CAAC;AAG/B,UAAM,aAAa,IAAI,MAAM,8BAA8B;AAC3D,QAAI,WAAY,QAAO,WAAW,CAAC;EACrC,QAAQ;EAER;AACA,SAAO;AACT;AAGA,SAAS,iBAAiB,KAA4B;AACpD,MAAI;AACF,WACE,aAAa,OAAO,CAAC,MAAM,KAAK,aAAa,gBAAgB,MAAM,GAAG;MACpE,UAAU;MACV,SAAS;MACT,OAAO,CAAC,UAAU,QAAQ,QAAQ;IACpC,CAAC,EAAE,KAAK,KAAK;EAEjB,QAAQ;AACN,WAAO;EACT;AACF;AAUO,SAAS,mBAAmB,KAA8B;AAC/D,MAAI,UAAU,IAAI,GAAG,EAAG,QAAO,UAAU,IAAI,GAAG;AAEhD,MAAI,SAA0B;AAE9B,QAAM,OAAO,iBAAiB,GAAG;AACjC,MAAI,MAAM;AACR,aAAS,EAAE,MAAM,QAAQ,iBAAiB,GAAG,EAAE;EACjD,OAAO;AAEL,eAAW,YAAY,WAAW;AAChC,UAAI,CAAC,SAAS,WAAW,GAAG,EAAG;AAC/B,YAAM,WAAW,SAAS,QAAQ,GAAG;AACrC,UAAI,CAAC,SAAU;AAGf,UAAI,SAAS,MAAM;AACjB,iBAAS,EAAE,MAAM,SAAS,MAAM,QAAQ,SAAS,OAAO;AACxD;MACF;AAGA,UAAI,SAAS,SAAS;AACpB,cAAM,eAAe,iBAAiB,SAAS,OAAO;AACtD,YAAI,cAAc;AAChB,mBAAS,EAAE,MAAM,cAAc,QAAQ,SAAS,OAAO;AACvD;QACF;MACF;IACF;EACF;AAEA,YAAU,IAAI,KAAK,MAAM;AACzB,SAAO;AACT;;;AEjEA,SAAS,IACPC,OACsB;AACtB,SAAOA;AACT;AAIO,SAAS,gBAAgB,OAAe;AAC7C,QAAM,iBAAiB,MAAM,WAAW,SAAS;AAEjD,MAAI,gBAaQ;AAEZ,WAAS,YAAY;AACnB,QAAI,CAAC,eAAe;AAClB,sBAAgB,OAAO,gBAAgB,EAAE,KAAK,CAAC,EAAE,iBAAiB,MAAM;AACtE,cAAM,SAAS,IAAI,iBAAiB,UAAU;AAC9C,eAAO,QAAQ,KAAK;AACpB,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAOA,WAAS,aAAqB;AAC5B,WAAO,WAAW,QAAQ,iBAAiB,cAAc,EAAE;AAAA,MACzD;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA;AAAA,IAGL,MAAM,YAAgC;AACpC,UAAI,gBAAgB;AAElB,eAAO,CAAC;AAAA,MACV;AACA,YAAM,SAAS,MAAM,UAAU;AAC/B,aAAO,MAAM,OAAO,MAAM,IAAI,+BAA+B,GAAG,CAAC,CAAC;AAAA,IACpE;AAAA;AAAA,IAIA,MAAM,YACJ,UACA,MACA,MACqB;AACrB,UAAI;AACF,YAAI,gBAAgB;AAMlB,gBAAM,EAAE,WAAW,IAAI,MAAM,OAAO,2BAAuB;AAC3D,gBAAM,iBACJ,QAAQ,IAAI,wBAAwB,WAAW,GAAG,KAAK;AACzD,gBAAM,MAAM,MAAM,MAAM,GAAG,WAAW,CAAC,sBAAsB;AAAA,YAC3D,QAAQ;AAAA,YACR,SAAS;AAAA,cACP,gBAAgB;AAAA,cAChB,eAAe,UAAU,KAAK;AAAA,YAChC;AAAA,YACA,MAAM,KAAK,UAAU;AAAA,cACnB;AAAA,cACA;AAAA,cACA,GAAI,kBAAkB,EAAE,eAAe;AAAA,YACzC,CAAC;AAAA,UACH,CAAC;AACD,gBAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,cAAI;AACJ,cAAI;AACF,mBAAO,KAAK,MAAM,IAAI;AAAA,UACxB,QAAQ;AACN,mBAAO;AAAA,cACL,IAAI;AAAA,cACJ,OAAO,QAAQ,IAAI,MAAM,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC;AAAA,YAClD;AAAA,UACF;AACA,cAAI,CAAC,IAAI,MAAM,CAAC,KAAK,OAAO;AAC1B,mBAAO,EAAE,IAAI,OAAO,OAAO,QAAQ,IAAI,MAAM,GAAG;AAAA,UAClD;AACA,iBAAO;AAAA,QACT;AAGA,cAAM,SAAS,MAAM,UAAU;AAC/B,cAAM,aAAsC,EAAE,UAAU,KAAK;AAE7D,cAAM,MAAM,MAAM,OAAO,eAAe;AACxC,YAAI,IAAK,YAAW,MAAM;AAC1B,cAAM,OAAO,mBAAmB,QAAQ,IAAI,CAAC;AAC7C,YAAI,KAAM,YAAW,OAAO,KAAK;AAEjC,cAAM,SAAS,MAAM,OAAO;AAAA,UAC1B,IAAc,+BAA+B;AAAA,UAC7C;AAAA,QACF;AACA,eAAO,EAAE,IAAI,MAAM,OAAO;AAAA,MAC5B,SAAS,KAAK;AACZ,cAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAI,IAAI,SAAS,cAAc,KAAK,IAAI,SAAS,mBAAmB,GAAG;AACrE,iBAAO;AAAA,YACL,IAAI;AAAA,YACJ,OACE;AAAA,UACJ;AAAA,QACF;AACA,eAAO,EAAE,IAAI,OAAO,OAAO,IAAI;AAAA,MACjC;AAAA,IACF;AAAA;AAAA,IAIA,MAAM,YACJ,SACA,cAC8B;AAC9B,YAAM,SAAS,MAAM,UAAU;AAC/B,aAAO,MAAM,OAAO,MAAM,IAAI,sBAAsB,GAAG;AAAA,QACrD;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA;AAAA,IAIA,MAAM,wBACJ,SACsC;AACtC,YAAM,SAAS,MAAM,UAAU;AAC/B,YAAM,SAAS,MAAM,OAAO;AAAA,QAC1B,IAAI,yCAAyC;AAAA,QAC7C,EAAE,QAAQ;AAAA,MACZ;AACA,aAAO,UAAU,CAAC;AAAA,IACpB;AAAA,IAEA,MAAM,oBACJ,SACA,gBAC0C;AAC1C,YAAM,SAAS,MAAM,UAAU;AAC/B,aAAO,MAAM,OAAO,MAAM,IAAI,qCAAqC,GAAG;AAAA,QACpE;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,wBACJ,SACA,YACsC;AACtC,YAAM,SAAS,MAAM,UAAU;AAC/B,YAAM,SAAS,MAAM,OAAO;AAAA,QAC1B,IAAI,yCAAyC;AAAA,QAC7C,EAAE,SAAS,WAAW;AAAA,MACxB;AACA,aAAO,UAAU,CAAC;AAAA,IACpB;AAAA,IAEA,MAAM,oBACJ,SACA,YAC0C;AAC1C,YAAM,SAAS,MAAM,UAAU;AAC/B,aAAO,MAAM,OAAO,MAAM,IAAI,qCAAqC,GAAG;AAAA,QACpE;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAQA,eAAsB,yBAAyB;AAC7C,QAAM,QAAQ,MAAM,cAAc;AAClC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,gBAAgB,KAAK;AAC9B;","names":["require","path"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auth/token-store.ts","../src/config.ts","../src/sentry.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { AUTH_STORE_PATH, CONVEX_URL, WORKOS_API_URL } from \"../config.js\";\nimport { FML_DATA_DIR } from \"../dirs.js\";\nimport { Sentry } from \"../sentry.js\";\n\ninterface StoredAuth {\n accessToken: string;\n refreshToken: string;\n expiresAt: number; // Unix timestamp in ms\n user: {\n id: string;\n email: string;\n name: string;\n };\n orgSlug?: string;\n /** WorkOS client ID used to obtain these tokens (needed for refresh) */\n workosClientId?: string;\n /** Token type: \"oauth\" (default) for WorkOS JWT, \"service\" for device flow tokens */\n tokenType?: \"oauth\" | \"service\";\n}\n\nfunction ensureDir(): void {\n const dir = path.dirname(AUTH_STORE_PATH);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n}\n\nexport function readTokens(): StoredAuth | null {\n try {\n const data = fs.readFileSync(AUTH_STORE_PATH, \"utf-8\");\n return JSON.parse(data) as StoredAuth;\n } catch {\n return null;\n }\n}\n\nexport function writeTokens(auth: StoredAuth): void {\n ensureDir();\n fs.writeFileSync(AUTH_STORE_PATH, JSON.stringify(auth, null, 2), {\n mode: 0o600,\n });\n}\n\nexport function getSelectedOrg(): string | null {\n return readTokens()?.orgSlug ?? null;\n}\n\nexport function setSelectedOrg(orgSlug: string): void {\n const stored = readTokens();\n if (!stored) return;\n writeTokens({ ...stored, orgSlug });\n}\n\n// ── Service Token Refresh (sandbox fml_srt_* → fml_st_*) ────────────────────\n\ninterface ServiceTokenCache {\n accessToken: string;\n expiresAt: number;\n}\n\nlet serviceTokenCache: ServiceTokenCache | null = null;\nlet serviceRefreshPromise: Promise<string | null> | null = null;\n\n/** File where the current access token is written for panopticon's tokenCommand */\nconst SERVICE_ACCESS_TOKEN_PATH = path.join(FML_DATA_DIR, \"access_token\");\n\nfunction getSiteUrl(): string {\n const explicit = process.env.CONVEX_SITE_URL;\n if (explicit) return explicit.replace(/\\/$/, \"\");\n return CONVEX_URL.replace(\".convex.cloud\", \".convex.site\");\n}\n\nasync function refreshServiceToken(\n refreshToken: string,\n): Promise<string | null> {\n try {\n const response = await fetch(`${getSiteUrl()}/api/tokens/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${refreshToken}`,\n },\n });\n\n if (!response.ok) {\n console.error(\n `[fml] Service token refresh failed: HTTP ${response.status}`,\n );\n return null;\n }\n\n const data = (await response.json()) as {\n ok: boolean;\n accessToken: string;\n expiresAt: number;\n error?: string;\n };\n\n if (!data.ok || !data.accessToken) {\n console.error(`[fml] Service token refresh error: ${data.error}`);\n return null;\n }\n\n serviceTokenCache = {\n accessToken: data.accessToken,\n expiresAt: data.expiresAt,\n };\n\n // Write to file for panopticon's tokenCommand\n try {\n const dir = path.dirname(SERVICE_ACCESS_TOKEN_PATH);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n fs.writeFileSync(SERVICE_ACCESS_TOKEN_PATH, data.accessToken, {\n mode: 0o600,\n });\n } catch {\n // Non-fatal — in-memory cache still works for the plugin itself\n }\n\n return data.accessToken;\n } catch (err: unknown) {\n Sentry.captureException(err);\n console.error(`[fml] Service token refresh error: ${err}`);\n return null;\n }\n}\n\n// ── Token Resolution ────────────────────────────────────────────────────────\n\n// Mutex to prevent concurrent token refresh races\nlet refreshPromise: Promise<string | null> | null = null;\n\n/**\n * Get a valid access token, refreshing if expired.\n *\n * Three paths:\n * 1. FML_TOKEN=fml_srt_* → exchange refresh token for short-lived access token\n * 2. FML_TOKEN=fml_st_* → use directly (legacy/CI)\n * 3. No FML_TOKEN → OAuth flow (interactive user)\n */\nexport async function getValidToken(): Promise<string | null> {\n const envToken = process.env.FML_TOKEN;\n\n // Service refresh token → exchange for short-lived access token\n if (envToken?.startsWith(\"fml_srt_\")) {\n if (\n serviceTokenCache &&\n serviceTokenCache.expiresAt > Date.now() + 60_000\n ) {\n return serviceTokenCache.accessToken;\n }\n\n if (serviceRefreshPromise) return serviceRefreshPromise;\n serviceRefreshPromise = refreshServiceToken(envToken);\n try {\n return await serviceRefreshPromise;\n } finally {\n serviceRefreshPromise = null;\n }\n }\n\n // Static service token (legacy) or plain env override\n if (envToken) return envToken;\n\n // Stored token path (OAuth or device flow)\n const stored = readTokens();\n if (!stored) {\n console.error(\"[fml] Auth: no stored tokens\");\n return null;\n }\n\n // If token expires in more than 60 seconds, use it\n if (stored.expiresAt > Date.now() + 60_000) {\n return stored.accessToken;\n }\n\n // Device flow tokens: refresh via /api/tokens/refresh (same as sandbox tokens)\n if (stored.tokenType === \"service\") {\n if (serviceRefreshPromise) return serviceRefreshPromise;\n serviceRefreshPromise = (async () => {\n const newAccessToken = await refreshServiceToken(stored.refreshToken);\n if (newAccessToken && serviceTokenCache) {\n writeTokens({\n ...stored,\n accessToken: newAccessToken,\n expiresAt: serviceTokenCache.expiresAt,\n });\n }\n return newAccessToken;\n })();\n try {\n return await serviceRefreshPromise;\n } finally {\n serviceRefreshPromise = null;\n }\n }\n\n // OAuth tokens: refresh via WorkOS\n if (refreshPromise) return refreshPromise;\n\n refreshPromise = refreshToken(stored);\n try {\n return await refreshPromise;\n } finally {\n refreshPromise = null;\n }\n}\n\nasync function refreshToken(stored: StoredAuth): Promise<string | null> {\n if (!stored.workosClientId) {\n console.error(\n \"[fml] Auth: token expired, missing workosClientId — run `fml login`\",\n );\n return null;\n }\n\n try {\n const response = await fetch(\n `${WORKOS_API_URL}/user_management/authenticate`,\n {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n grant_type: \"refresh_token\",\n client_id: stored.workosClientId,\n refresh_token: stored.refreshToken,\n }),\n },\n );\n\n if (!response.ok) {\n console.error(\n `[fml] Auth: refresh failed (HTTP ${response.status}) — token preserved`,\n );\n return null;\n }\n\n const data = (await response.json()) as {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n user: {\n id: string;\n email: string;\n first_name?: string;\n last_name?: string;\n };\n };\n\n const userName = [data.user.first_name, data.user.last_name]\n .filter(Boolean)\n .join(\" \");\n\n const refreshed: StoredAuth = {\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresAt: Date.now() + (data.expires_in ?? 300) * 1000,\n user: {\n id: data.user.id,\n email: data.user.email,\n name: userName || stored.user.name,\n },\n orgSlug: stored.orgSlug,\n workosClientId: stored.workosClientId,\n };\n\n writeTokens(refreshed);\n return refreshed.accessToken;\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(\n `[fml] Auth: refresh failed (network error: ${msg}) — token preserved`,\n );\n Sentry.captureException(err);\n return null;\n }\n}\n","import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { FML_DATA_DIR } from \"./dirs.js\";\n\n// ── Default production deployment ───────────────────────────────────────────\n\nexport const DEFAULT_PROD_URL =\n \"https://trustworthy-chihuahua-382.convex.cloud\";\n\n/** Convex site URL for sync/HTTP actions — derived from DEFAULT_PROD_URL. */\nexport const DEFAULT_SYNC_URL = DEFAULT_PROD_URL.replace(\n \".convex.cloud\",\n \".convex.site\",\n);\n\n/** Default sync target name created by `fml install` */\nexport const DEFAULT_TARGET_NAME = \"fml\";\n\n// ── Persistent env selection ────────────────────────────────────────────────\n\ninterface EnvConfig {\n /** Name of the active sync target */\n active: string;\n}\n\nconst ENV_CONFIG_PATH = path.join(FML_DATA_DIR, \"env.json\");\n\nfunction readEnvConfig(): EnvConfig {\n try {\n const raw = fs.readFileSync(ENV_CONFIG_PATH, \"utf-8\");\n return JSON.parse(raw) as EnvConfig;\n } catch {\n return { active: DEFAULT_TARGET_NAME };\n }\n}\n\nexport function writeEnvConfig(config: EnvConfig): void {\n const dir = path.dirname(ENV_CONFIG_PATH);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n fs.writeFileSync(ENV_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\\n`, {\n mode: 0o600,\n });\n}\n\n/**\n * Resolve the active environment by looking up the sync target.\n * Derives the .convex.cloud URL from the target's .convex.site URL.\n * Returns null convexUrl if the target is not found (panopticon not installed).\n */\nexport function getActiveEnv(): { name: string; convexUrl: string | null } {\n const config = readEnvConfig();\n const name = config.active;\n\n try {\n const panoDataDir =\n process.env.PANOPTICON_DATA_DIR ??\n (process.platform === \"darwin\"\n ? path.join(\n os.homedir(),\n \"Library\",\n \"Application Support\",\n \"panopticon\",\n )\n : process.platform === \"win32\"\n ? path.join(\n process.env.APPDATA ??\n path.join(os.homedir(), \"AppData\", \"Roaming\"),\n \"panopticon\",\n )\n : path.join(os.homedir(), \".local\", \"share\", \"panopticon\"));\n const raw = fs.readFileSync(path.join(panoDataDir, \"config.json\"), \"utf-8\");\n const panoConf = JSON.parse(raw) as {\n sync?: { targets?: Array<{ name: string; url: string }> };\n };\n const target = panoConf.sync?.targets?.find((t) => t.name === name);\n if (target) {\n const convexUrl = target.url.replace(\".convex.site\", \".convex.cloud\");\n return { name, convexUrl };\n }\n } catch {\n // Panopticon config not available\n }\n\n return { name, convexUrl: null };\n}\n\n/**\n * Require a resolved Convex URL. Exits with an error if the sync target\n * is not configured (panopticon not installed).\n */\nexport function requireConvexUrl(): string {\n const { name, convexUrl } = getActiveEnv();\n if (!convexUrl) {\n console.error(\n `Sync target \"${name}\" not found. Run \\`fml install\\` or \\`fml sync setup\\`.`,\n );\n process.exit(1);\n }\n return convexUrl;\n}\n\n// ── Exports ─────────────────────────────────────────────────────────────────\n\nconst env = getActiveEnv();\n\n/**\n * Convex deployment URL (switches with `fml env`).\n *\n * Resolution order:\n * 1. `FML_CONVEX_URL` env var (dev/preview overrides)\n * 2. Active panopticon sync target\n * 3. `DEFAULT_PROD_URL` — lets a fresh `npm install -g` log in against\n * prod before `fml install` has had a chance to seed a sync target.\n */\nexport const CONVEX_URL: string = (\n process.env.FML_CONVEX_URL ??\n env.convexUrl ??\n DEFAULT_PROD_URL\n).replace(/\\/$/, \"\");\n\n/** WorkOS API base URL */\nexport const WORKOS_API_URL = \"https://api.workos.com\";\n\n/** WorkOS authorization base URL */\nexport const WORKOS_AUTH_URL = \"https://auth.fml.inc\";\n\n/** Path to auth token store (per-environment) */\nexport const AUTH_STORE_PATH = path.join(FML_DATA_DIR, `auth.${env.name}.json`);\n\n/** Convex site URL (HTTP actions) — derived from CONVEX_URL */\nexport function getSiteUrl(): string {\n return CONVEX_URL.replace(\".convex.cloud\", \".convex.site\").replace(\n /:\\d+$/,\n \"\",\n );\n}\n","declare const __SENTRY_DSN__: string;\n\nlet initialized = false;\nlet sentry: typeof import(\"@sentry/core\") | null = null;\n\nasync function loadSentry(): Promise<typeof import(\"@sentry/core\") | null> {\n if (sentry) return sentry;\n try {\n sentry = await import(\"@sentry/core\");\n return sentry;\n } catch {\n return null;\n }\n}\n\nexport async function initSentry(): Promise<void> {\n if (initialized) return;\n initialized = true;\n\n const mod = await loadSentry();\n if (!mod) return;\n\n const client = new mod.ServerRuntimeClient({\n dsn: __SENTRY_DSN__,\n tracesSampleRate: 0,\n integrations: [],\n stackParser: mod.createStackParser(mod.nodeStackLineParser()),\n transport: (options) =>\n mod.createTransport(options, async (request) => {\n const res = await fetch(options.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-sentry-envelope\" },\n body: request.body as BodyInit,\n });\n return { statusCode: res.status };\n }),\n });\n\n mod.setCurrentClient(client);\n client.init();\n}\n\nexport const Sentry = {\n captureException: (err: unknown) => sentry?.captureException(err),\n captureMessage: (msg: string) => sentry?.captureMessage(msg),\n} as const;\n"],"mappings":"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","names":["fs","path","path","fs","getSiteUrl","refreshToken"]}
|
package/dist/chunk-UALBZA5X.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/dist/chunk-7PHTUJSI.js
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
var sqlite = process.getBuiltinModule(
|
|
4
|
-
"node:sqlite"
|
|
5
|
-
);
|
|
6
|
-
var { DatabaseSync } = sqlite;
|
|
7
|
-
function normalizeArgs(args) {
|
|
8
|
-
return args.map((a) => a === void 0 ? null : a);
|
|
9
|
-
}
|
|
10
|
-
function bigintToNumber(value) {
|
|
11
|
-
if (typeof value === "bigint") return Number(value);
|
|
12
|
-
if (value !== null && typeof value === "object") {
|
|
13
|
-
for (const key of Object.keys(value)) {
|
|
14
|
-
const v = value[key];
|
|
15
|
-
if (typeof v === "bigint") {
|
|
16
|
-
value[key] = Number(v);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
var Statement = class {
|
|
23
|
-
stmt;
|
|
24
|
-
constructor(stmt) {
|
|
25
|
-
this.stmt = stmt;
|
|
26
|
-
this.stmt.setReadBigInts(true);
|
|
27
|
-
}
|
|
28
|
-
run(...args) {
|
|
29
|
-
const r = this.stmt.run(...normalizeArgs(args));
|
|
30
|
-
return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };
|
|
31
|
-
}
|
|
32
|
-
/** Returns the first row, or `undefined` if there is none (matching better-sqlite3). */
|
|
33
|
-
get(...args) {
|
|
34
|
-
const row = this.stmt.get(...normalizeArgs(args));
|
|
35
|
-
return row == null ? void 0 : bigintToNumber(row);
|
|
36
|
-
}
|
|
37
|
-
all(...args) {
|
|
38
|
-
const rows = this.stmt.all(...normalizeArgs(args));
|
|
39
|
-
for (const row of rows) bigintToNumber(row);
|
|
40
|
-
return rows;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
var Database = class {
|
|
44
|
-
db;
|
|
45
|
-
inTx = false;
|
|
46
|
-
constructor(filename, options = {}) {
|
|
47
|
-
if (options.fileMustExist && !fs.existsSync(filename)) {
|
|
48
|
-
throw new Error(`SqliteError: unable to open database file: ${filename}`);
|
|
49
|
-
}
|
|
50
|
-
this.db = new DatabaseSync(filename, {
|
|
51
|
-
readOnly: options.readonly === true
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
prepare(sql) {
|
|
55
|
-
return new Statement(this.db.prepare(sql));
|
|
56
|
-
}
|
|
57
|
-
exec(sql) {
|
|
58
|
-
this.db.exec(sql);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* better-sqlite3-compatible PRAGMA helper.
|
|
62
|
-
*
|
|
63
|
-
* db.pragma("journal_mode = WAL") → setter, no result
|
|
64
|
-
* db.pragma("user_version", { simple: true }) → first column of first row
|
|
65
|
-
* db.pragma("table_info(sessions)") → array of result rows
|
|
66
|
-
*/
|
|
67
|
-
pragma(source, opts = {}) {
|
|
68
|
-
const sql = `PRAGMA ${source}`;
|
|
69
|
-
if (source.includes("=")) {
|
|
70
|
-
this.db.exec(sql);
|
|
71
|
-
return void 0;
|
|
72
|
-
}
|
|
73
|
-
const stmt = this.db.prepare(sql);
|
|
74
|
-
if (opts.simple) {
|
|
75
|
-
const row = stmt.get();
|
|
76
|
-
if (!row) return void 0;
|
|
77
|
-
const firstKey = Object.keys(row)[0];
|
|
78
|
-
return firstKey ? row[firstKey] : void 0;
|
|
79
|
-
}
|
|
80
|
-
return stmt.all();
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Wrap `fn` so calling the returned function runs it inside a transaction.
|
|
84
|
-
*
|
|
85
|
-
* const tx = db.transaction((id) => { ... });
|
|
86
|
-
* tx("session-1");
|
|
87
|
-
*
|
|
88
|
-
* Nested calls (already inside a transaction) just invoke `fn` directly,
|
|
89
|
-
* so callers can compose transactions safely.
|
|
90
|
-
*/
|
|
91
|
-
transaction(fn) {
|
|
92
|
-
return (...args) => {
|
|
93
|
-
if (this.inTx) return fn(...args);
|
|
94
|
-
this.db.exec("BEGIN");
|
|
95
|
-
this.inTx = true;
|
|
96
|
-
try {
|
|
97
|
-
const result = fn(...args);
|
|
98
|
-
this.db.exec("COMMIT");
|
|
99
|
-
return result;
|
|
100
|
-
} catch (err) {
|
|
101
|
-
try {
|
|
102
|
-
this.db.exec("ROLLBACK");
|
|
103
|
-
} catch {
|
|
104
|
-
}
|
|
105
|
-
throw err;
|
|
106
|
-
} finally {
|
|
107
|
-
this.inTx = false;
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
/** Register a user-defined SQL function. */
|
|
112
|
-
function(name, fn) {
|
|
113
|
-
this.db.function(name, fn);
|
|
114
|
-
}
|
|
115
|
-
close() {
|
|
116
|
-
this.db.close();
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
export {
|
|
121
|
-
Database
|
|
122
|
-
};
|
|
123
|
-
//# sourceMappingURL=chunk-UALBZA5X.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../node_modules/.pnpm/@fml-inc+panopticon@0.2.2/node_modules/@fml-inc/panopticon/src/db/driver.ts"],"sourcesContent":["/**\n * Synchronous SQLite driver backed by Node's built-in `node:sqlite`.\n *\n * Wraps `node:sqlite` in a small better-sqlite3-compatible API so the rest\n * of the codebase can keep using the same call shape (`db.prepare(...).run/get/all`,\n * `db.transaction(fn)()`, `db.pragma(...)`, etc.) while shipping zero native\n * dependencies. This is what lets the published plugin run from a copied\n * Claude Code plugin cache without needing a node_modules tree.\n *\n * `node:sqlite` is currently flagged experimental in Node — the runtime\n * warning is suppressed at the entry-point binaries (see bin/*).\n */\n\nimport fs from \"node:fs\";\nimport type {\n DatabaseSync as DatabaseSyncType,\n SQLInputValue,\n SQLOutputValue,\n StatementSync,\n} from \"node:sqlite\";\n\n// `node:sqlite` is a \"prefix-mandatory\" builtin — Node accepts it ONLY as\n// `node:sqlite`, not bare `sqlite`. esbuild strips the `node:` prefix from\n// builtin imports when bundling, which produces a bundle that crashes at\n// runtime with `Cannot find package 'sqlite'`. Reaching for the module via\n// `process.getBuiltinModule()` (added in Node 22.3) bypasses esbuild's\n// static import analysis, so the runtime resolution is left untouched.\nconst sqlite = process.getBuiltinModule(\n \"node:sqlite\",\n) as typeof import(\"node:sqlite\");\nconst { DatabaseSync } = sqlite;\ntype DatabaseSync = DatabaseSyncType;\n\nexport interface OpenOptions {\n /** Open the file read-only. Maps to node:sqlite's `readOnly`. */\n readonly?: boolean;\n /** Throw if the file does not already exist. node:sqlite has no equivalent — emulated via fs.existsSync. */\n fileMustExist?: boolean;\n}\n\n/** Bind value accepted by node:sqlite's prepared statements. */\ntype BindArg = null | number | bigint | string | Uint8Array;\n\n/**\n * better-sqlite3 accepts `undefined` as \"no value bound\"; node:sqlite throws\n * on undefined. Normalize to null at the boundary so call sites that pass\n * potentially-undefined values keep working.\n */\nfunction normalizeArgs(args: unknown[]): BindArg[] {\n return args.map((a) => (a === undefined ? null : (a as BindArg)));\n}\n\n/**\n * better-sqlite3 returns SQLite INTEGER columns as JS `number` by default,\n * silently losing precision for values that exceed `Number.MAX_SAFE_INTEGER`\n * (e.g. OTel nanosecond timestamps). node:sqlite refuses to read those\n * values as `number` and throws unless you opt into BigInt mode per\n * statement (`stmt.setReadBigInts(true)`).\n *\n * To preserve drop-in compatibility we enable BigInt reads on every\n * statement and convert bigint → number at the result boundary, matching\n * better-sqlite3's lossy behavior. Call sites that need full precision\n * should be migrated to read bigints explicitly later.\n */\nfunction bigintToNumber(value: unknown): unknown {\n if (typeof value === \"bigint\") return Number(value);\n if (value !== null && typeof value === \"object\") {\n // Row objects from node:sqlite are plain objects keyed by column name.\n // Mutate in place — the StatementSync allocates a fresh object per row.\n for (const key of Object.keys(value)) {\n const v = (value as Record<string, unknown>)[key];\n if (typeof v === \"bigint\") {\n (value as Record<string, unknown>)[key] = Number(v);\n }\n }\n }\n return value;\n}\n\nexport interface RunResult {\n changes: number;\n lastInsertRowid: number | bigint;\n}\n\nexport class Statement {\n private readonly stmt: StatementSync;\n\n constructor(stmt: StatementSync) {\n this.stmt = stmt;\n // Read INTEGER columns as bigint so node:sqlite doesn't throw on values\n // that exceed Number.MAX_SAFE_INTEGER. We convert back to number at the\n // boundary in get/all to keep call sites unchanged.\n this.stmt.setReadBigInts(true);\n }\n\n run(...args: unknown[]): RunResult {\n const r = this.stmt.run(...normalizeArgs(args));\n return { changes: Number(r.changes), lastInsertRowid: r.lastInsertRowid };\n }\n\n /** Returns the first row, or `undefined` if there is none (matching better-sqlite3). */\n get(...args: unknown[]): unknown {\n const row = this.stmt.get(...normalizeArgs(args));\n // node:sqlite returns null on no row; better-sqlite3 returns undefined.\n return row == null ? undefined : bigintToNumber(row);\n }\n\n all(...args: unknown[]): unknown[] {\n const rows = this.stmt.all(...normalizeArgs(args)) as unknown[];\n for (const row of rows) bigintToNumber(row);\n return rows;\n }\n}\n\n/**\n * Function shape returned by {@link Database.transaction}. Calling it runs\n * the wrapped function inside a `BEGIN`/`COMMIT` block (or just calls it\n * directly if already inside a transaction).\n */\nexport type TransactionFn<Args extends unknown[], R> = (...args: Args) => R;\n\nexport class Database {\n private readonly db: DatabaseSync;\n private inTx = false;\n\n constructor(filename: string, options: OpenOptions = {}) {\n if (options.fileMustExist && !fs.existsSync(filename)) {\n throw new Error(`SqliteError: unable to open database file: ${filename}`);\n }\n this.db = new DatabaseSync(filename, {\n readOnly: options.readonly === true,\n });\n }\n\n prepare(sql: string): Statement {\n return new Statement(this.db.prepare(sql));\n }\n\n exec(sql: string): void {\n this.db.exec(sql);\n }\n\n /**\n * better-sqlite3-compatible PRAGMA helper.\n *\n * db.pragma(\"journal_mode = WAL\") → setter, no result\n * db.pragma(\"user_version\", { simple: true }) → first column of first row\n * db.pragma(\"table_info(sessions)\") → array of result rows\n */\n pragma(source: string, opts: { simple?: boolean } = {}): unknown {\n const sql = `PRAGMA ${source}`;\n if (source.includes(\"=\")) {\n // Setter pragmas: no result row.\n this.db.exec(sql);\n return undefined;\n }\n const stmt = this.db.prepare(sql);\n if (opts.simple) {\n const row = stmt.get() as Record<string, unknown> | null;\n if (!row) return undefined;\n const firstKey = Object.keys(row)[0];\n return firstKey ? row[firstKey] : undefined;\n }\n return stmt.all();\n }\n\n /**\n * Wrap `fn` so calling the returned function runs it inside a transaction.\n *\n * const tx = db.transaction((id) => { ... });\n * tx(\"session-1\");\n *\n * Nested calls (already inside a transaction) just invoke `fn` directly,\n * so callers can compose transactions safely.\n */\n transaction<Args extends unknown[], R>(\n fn: (...args: Args) => R,\n ): TransactionFn<Args, R> {\n return (...args: Args): R => {\n if (this.inTx) return fn(...args);\n this.db.exec(\"BEGIN\");\n this.inTx = true;\n try {\n const result = fn(...args);\n this.db.exec(\"COMMIT\");\n return result;\n } catch (err) {\n try {\n this.db.exec(\"ROLLBACK\");\n } catch {\n // Best-effort rollback — original error is what matters.\n }\n throw err;\n } finally {\n this.inTx = false;\n }\n };\n }\n\n /** Register a user-defined SQL function. */\n function(\n name: string,\n fn: (...args: SQLOutputValue[]) => SQLInputValue,\n ): void {\n this.db.function(name, fn);\n }\n\n close(): void {\n this.db.close();\n }\n}\n\nexport default Database;\n"],"mappings":";AAaA,OAAO,QAAQ;AAcf,IAAM,SAAS,QAAQ;EACrB;AACF;AACA,IAAM,EAAE,aAAa,IAAI;AAkBzB,SAAS,cAAc,MAA4B;AACjD,SAAO,KAAK,IAAI,CAAC,MAAO,MAAM,SAAY,OAAQ,CAAc;AAClE;AAcA,SAAS,eAAe,OAAyB;AAC/C,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK;AAClD,MAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAG/C,eAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,YAAM,IAAK,MAAkC,GAAG;AAChD,UAAI,OAAO,MAAM,UAAU;AACxB,cAAkC,GAAG,IAAI,OAAO,CAAC;MACpD;IACF;EACF;AACA,SAAO;AACT;AAOO,IAAM,YAAN,MAAgB;EACJ;EAEjB,YAAY,MAAqB;AAC/B,SAAK,OAAO;AAIZ,SAAK,KAAK,eAAe,IAAI;EAC/B;EAEA,OAAO,MAA4B;AACjC,UAAM,IAAI,KAAK,KAAK,IAAI,GAAG,cAAc,IAAI,CAAC;AAC9C,WAAO,EAAE,SAAS,OAAO,EAAE,OAAO,GAAG,iBAAiB,EAAE,gBAAgB;EAC1E;;EAGA,OAAO,MAA0B;AAC/B,UAAM,MAAM,KAAK,KAAK,IAAI,GAAG,cAAc,IAAI,CAAC;AAEhD,WAAO,OAAO,OAAO,SAAY,eAAe,GAAG;EACrD;EAEA,OAAO,MAA4B;AACjC,UAAM,OAAO,KAAK,KAAK,IAAI,GAAG,cAAc,IAAI,CAAC;AACjD,eAAW,OAAO,KAAM,gBAAe,GAAG;AAC1C,WAAO;EACT;AACF;AASO,IAAM,WAAN,MAAe;EACH;EACT,OAAO;EAEf,YAAY,UAAkB,UAAuB,CAAC,GAAG;AACvD,QAAI,QAAQ,iBAAiB,CAAC,GAAG,WAAW,QAAQ,GAAG;AACrD,YAAM,IAAI,MAAM,8CAA8C,QAAQ,EAAE;IAC1E;AACA,SAAK,KAAK,IAAI,aAAa,UAAU;MACnC,UAAU,QAAQ,aAAa;IACjC,CAAC;EACH;EAEA,QAAQ,KAAwB;AAC9B,WAAO,IAAI,UAAU,KAAK,GAAG,QAAQ,GAAG,CAAC;EAC3C;EAEA,KAAK,KAAmB;AACtB,SAAK,GAAG,KAAK,GAAG;EAClB;;;;;;;;EASA,OAAO,QAAgB,OAA6B,CAAC,GAAY;AAC/D,UAAM,MAAM,UAAU,MAAM;AAC5B,QAAI,OAAO,SAAS,GAAG,GAAG;AAExB,WAAK,GAAG,KAAK,GAAG;AAChB,aAAO;IACT;AACA,UAAM,OAAO,KAAK,GAAG,QAAQ,GAAG;AAChC,QAAI,KAAK,QAAQ;AACf,YAAM,MAAM,KAAK,IAAI;AACrB,UAAI,CAAC,IAAK,QAAO;AACjB,YAAM,WAAW,OAAO,KAAK,GAAG,EAAE,CAAC;AACnC,aAAO,WAAW,IAAI,QAAQ,IAAI;IACpC;AACA,WAAO,KAAK,IAAI;EAClB;;;;;;;;;;EAWA,YACE,IACwB;AACxB,WAAO,IAAI,SAAkB;AAC3B,UAAI,KAAK,KAAM,QAAO,GAAG,GAAG,IAAI;AAChC,WAAK,GAAG,KAAK,OAAO;AACpB,WAAK,OAAO;AACZ,UAAI;AACF,cAAM,SAAS,GAAG,GAAG,IAAI;AACzB,aAAK,GAAG,KAAK,QAAQ;AACrB,eAAO;MACT,SAAS,KAAK;AACZ,YAAI;AACF,eAAK,GAAG,KAAK,UAAU;QACzB,QAAQ;QAER;AACA,cAAM;MACR,UAAA;AACE,aAAK,OAAO;MACd;IACF;EACF;;EAGA,SACE,MACA,IACM;AACN,SAAK,GAAG,SAAS,MAAM,EAAE;EAC3B;EAEA,QAAc;AACZ,SAAK,GAAG,MAAM;EAChB;AACF;","names":[]}
|