@tekmidian/pai 0.11.0 → 0.12.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/README.md +84 -10
- package/dist/{auto-route-D54Kv7yI.mjs → auto-route-CLYcToZ5.mjs} +4 -4
- package/dist/{auto-route-D54Kv7yI.mjs.map → auto-route-CLYcToZ5.mjs.map} +1 -1
- package/dist/cli/index.mjs +20 -10275
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.d.mts +11 -0
- package/dist/cli/program.d.mts.map +1 -0
- package/dist/cli/program.mjs +249 -0
- package/dist/cli/program.mjs.map +1 -0
- package/dist/{clusters-BwmdKB-V.mjs → clusters-BYPw7vfW.mjs} +2 -2
- package/dist/{clusters-BwmdKB-V.mjs.map → clusters-BYPw7vfW.mjs.map} +1 -1
- package/dist/{config-DqBY3aT0.mjs → config-BuhHWyOK.mjs} +1 -1
- package/dist/{config-DqBY3aT0.mjs.map → config-BuhHWyOK.mjs.map} +1 -1
- package/dist/daemon/index.mjs +16 -18
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-CSKj8Xm9.mjs → daemon-BldK8-57.mjs} +197 -43
- package/dist/daemon-BldK8-57.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +2 -2
- package/dist/{db-CYmBWcjh.mjs → db-CmYbAVCD.mjs} +1 -1
- package/dist/{db-CYmBWcjh.mjs.map → db-CmYbAVCD.mjs.map} +1 -1
- package/dist/{detect-CdaA48EI.mjs → detect-KjycLtXM.mjs} +1 -1
- package/dist/{detect-CdaA48EI.mjs.map → detect-KjycLtXM.mjs.map} +1 -1
- package/dist/{detector-Q_lVXtj-.mjs → detector-ZiOhHszd.mjs} +2 -2
- package/dist/{detector-Q_lVXtj-.mjs.map → detector-ZiOhHszd.mjs.map} +1 -1
- package/dist/{embeddings-DGRAPAYb.mjs → embeddings-BJPOcbik.mjs} +1 -1
- package/dist/{embeddings-DGRAPAYb.mjs.map → embeddings-BJPOcbik.mjs.map} +1 -1
- package/dist/factory-Q88X1bAN.mjs +69 -0
- package/dist/factory-Q88X1bAN.mjs.map +1 -0
- package/dist/{helpers-crDEr6S2.mjs → helpers-IjZkXBhj.mjs} +1 -1
- package/dist/{helpers-crDEr6S2.mjs.map → helpers-IjZkXBhj.mjs.map} +1 -1
- package/dist/hooks/observe.mjs +21 -0
- package/dist/hooks/observe.mjs.map +2 -2
- package/dist/index.mjs +9 -10
- package/dist/{indexer-backend-DcYThufd.mjs → indexer-backend-Cm5RS7y0.mjs} +3 -3
- package/dist/{indexer-backend-DcYThufd.mjs.map → indexer-backend-Cm5RS7y0.mjs.map} +1 -1
- package/dist/{ipc-client-C3pjwy2m.mjs → ipc-client-CoyUHPod.mjs} +1 -1
- package/dist/{ipc-client-C3pjwy2m.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
- package/dist/{kg-entity-DVzTsy6G.mjs → kg-entity-LXblD7LZ.mjs} +1 -1
- package/dist/{kg-entity-DVzTsy6G.mjs.map → kg-entity-LXblD7LZ.mjs.map} +1 -1
- package/dist/{kg-extraction-iR1BKKWK.mjs → kg-extraction-C8DEUHTS.mjs} +17 -6
- package/dist/kg-extraction-C8DEUHTS.mjs.map +1 -0
- package/dist/{latent-ideas-dWVuI_dg.mjs → latent-ideas-RyC6eyqI.mjs} +4 -4
- package/dist/{latent-ideas-dWVuI_dg.mjs.map → latent-ideas-RyC6eyqI.mjs.map} +1 -1
- package/dist/main-resolver-uFxNiDy7.mjs +1339 -0
- package/dist/main-resolver-uFxNiDy7.mjs.map +1 -0
- package/dist/{migrate-fLD6rAdO.mjs → migrate-B02wDDgS.mjs} +2 -2
- package/dist/{migrate-fLD6rAdO.mjs.map → migrate-B02wDDgS.mjs.map} +1 -1
- package/dist/{neighborhood-DixmpfPT.mjs → neighborhood-CklGIB8r.mjs} +2 -2
- package/dist/{neighborhood-DixmpfPT.mjs.map → neighborhood-CklGIB8r.mjs.map} +1 -1
- package/dist/{note-context-BY7rfGdl.mjs → note-context-qvZlVXVO.mjs} +1 -1
- package/dist/{note-context-BY7rfGdl.mjs.map → note-context-qvZlVXVO.mjs.map} +1 -1
- package/dist/{pai-marker-CXQPX2P6.mjs → pai-marker-HVBwwBW-.mjs} +1 -1
- package/dist/{pai-marker-CXQPX2P6.mjs.map → pai-marker-HVBwwBW-.mjs.map} +1 -1
- package/dist/pick-7Kg9DX7S.mjs +8250 -0
- package/dist/pick-7Kg9DX7S.mjs.map +1 -0
- package/dist/{postgres-Cp25xZes.mjs → postgres-B451Hnkm.mjs} +2 -2
- package/dist/{postgres-Cp25xZes.mjs.map → postgres-B451Hnkm.mjs.map} +1 -1
- package/dist/{query-feedback-C0hKUHVz.mjs → query-feedback-fapbqRgh.mjs} +1 -1
- package/dist/{query-feedback-C0hKUHVz.mjs.map → query-feedback-fapbqRgh.mjs.map} +1 -1
- package/dist/{reranker-CMNZcfVx.mjs → reranker-C08R99zn.mjs} +1 -1
- package/dist/{reranker-CMNZcfVx.mjs.map → reranker-C08R99zn.mjs.map} +1 -1
- package/dist/{search-i2nlQ-JM.mjs → search-HcdKtMla.mjs} +3 -3
- package/dist/{search-i2nlQ-JM.mjs.map → search-HcdKtMla.mjs.map} +1 -1
- package/dist/{sqlite-B3JHfJc2.mjs → sqlite-DQpY1Esi.mjs} +3 -3
- package/dist/{sqlite-B3JHfJc2.mjs.map → sqlite-DQpY1Esi.mjs.map} +1 -1
- package/dist/{state-BHZAhLou.mjs → state-BIlxNRUn.mjs} +1 -1
- package/dist/{state-BHZAhLou.mjs.map → state-BIlxNRUn.mjs.map} +1 -1
- package/dist/{stop-words-BaMEGVeY.mjs → stop-words-BwplsQ3z.mjs} +1 -1
- package/dist/{stop-words-BaMEGVeY.mjs.map → stop-words-BwplsQ3z.mjs.map} +1 -1
- package/dist/{sync-CmBKOL3K.mjs → sync-uPR4g438.mjs} +205 -6
- package/dist/sync-uPR4g438.mjs.map +1 -0
- package/dist/{themes-DO8mUVwD.mjs → themes-Cg8oG9Ra.mjs} +3 -3
- package/dist/{themes-DO8mUVwD.mjs.map → themes-Cg8oG9Ra.mjs.map} +1 -1
- package/dist/{tools-C1JNgya7.mjs → tools-Op3C2Nm6.mjs} +26 -26
- package/dist/{tools-C1JNgya7.mjs.map → tools-Op3C2Nm6.mjs.map} +1 -1
- package/dist/{trace-TYCLMCkY.mjs → trace-CLK-NPkb.mjs} +1 -1
- package/dist/{trace-TYCLMCkY.mjs.map → trace-CLK-NPkb.mjs.map} +1 -1
- package/dist/{utils-BAxjW3j8.mjs → utils-CqqgB0dH.mjs} +1 -1
- package/dist/{utils-BAxjW3j8.mjs.map → utils-CqqgB0dH.mjs.map} +1 -1
- package/dist/{vault-indexer-vNnL3Azb.mjs → vault-indexer-Dn_BccVK.mjs} +2 -2
- package/dist/{vault-indexer-vNnL3Azb.mjs.map → vault-indexer-Dn_BccVK.mjs.map} +1 -1
- package/dist/{work-queue-worker-BxsBtz6j.mjs → work-queue-worker-DHOIVVQ7.mjs} +5 -5
- package/dist/{work-queue-worker-BxsBtz6j.mjs.map → work-queue-worker-DHOIVVQ7.mjs.map} +1 -1
- package/dist/{zettelkasten-B-94vghH.mjs → zettelkasten-NwBD1NsT.mjs} +4 -4
- package/dist/{zettelkasten-B-94vghH.mjs.map → zettelkasten-NwBD1NsT.mjs.map} +1 -1
- package/docs/commands/README.md +151 -0
- package/docs/commands/_examples/kg.md +13 -0
- package/docs/commands/_examples/memory.md +13 -0
- package/docs/commands/_examples/observation.md +13 -0
- package/docs/commands/_examples/projects.md +16 -0
- package/docs/commands/_examples/skill.md +13 -0
- package/docs/commands/_examples/zettel.md +13 -0
- package/docs/commands/backup.md +24 -0
- package/docs/commands/clear-names.md +27 -0
- package/docs/commands/daemon.md +78 -0
- package/docs/commands/db.md +74 -0
- package/docs/commands/end.md +24 -0
- package/docs/commands/help.md +30 -0
- package/docs/commands/kg.md +89 -0
- package/docs/commands/mcp.md +35 -0
- package/docs/commands/memory.md +125 -0
- package/docs/commands/notify.md +83 -0
- package/docs/commands/observation.md +80 -0
- package/docs/commands/obsidian.md +60 -0
- package/docs/commands/pause.md +35 -0
- package/docs/commands/project.md +345 -0
- package/docs/commands/projects.md +364 -0
- package/docs/commands/registry.md +68 -0
- package/docs/commands/restore.md +31 -0
- package/docs/commands/sessions.md +25 -0
- package/docs/commands/setup.md +20 -0
- package/docs/commands/shell-init.md +18 -0
- package/docs/commands/skill.md +54 -0
- package/docs/commands/topic.md +46 -0
- package/docs/commands/update.md +18 -0
- package/docs/commands/zettel.md +151 -0
- package/docs/mcp-skill-guide.md +292 -0
- package/package.json +4 -2
- package/scripts/build-docs.mjs +371 -0
- package/src/hooks/ts/post-tool-use/observe.ts +43 -0
- package/dist/daemon-CSKj8Xm9.mjs.map +0 -1
- package/dist/db-BtuN768f.mjs +0 -206
- package/dist/db-BtuN768f.mjs.map +0 -1
- package/dist/factory-BufouUQ1.mjs +0 -44
- package/dist/factory-BufouUQ1.mjs.map +0 -1
- package/dist/indexer-D7MvSQPY.mjs +0 -1
- package/dist/kg-extraction-iR1BKKWK.mjs.map +0 -1
- package/dist/sync-CmBKOL3K.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-BldK8-57.mjs","names":["getQueueStats"],"sources":["../src/notifications/config.ts","../src/daemon/daemon/scheduler.ts","../src/notifications/providers/ntfy.ts","../src/notifications/providers/whatsapp.ts","../src/notifications/providers/macos.ts","../src/notifications/providers/cli.ts","../src/notifications/router.ts","../src/observations/store.ts","../src/daemon/daemon/dispatcher.ts","../src/daemon/daemon/handler.ts","../src/daemon/daemon/server.ts","../src/daemon/daemon.ts"],"sourcesContent":["/**\n * config.ts — Notification config persistence helpers\n *\n * Reads and writes the `notifications` section of ~/.config/pai/config.json.\n * Deep-merges with defaults so partial configs work fine.\n *\n * This module is intentionally separate from the daemon's config loader\n * so it can be used standalone (e.g. from CLI commands).\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n mkdirSync,\n} from \"node:fs\";\nimport {\n CONFIG_FILE,\n CONFIG_DIR,\n expandHome,\n} from \"../daemon/config.js\";\nimport type {\n NotificationConfig,\n ChannelConfigs,\n RoutingTable,\n NotificationMode,\n} from \"./types.js\";\nimport {\n DEFAULT_NOTIFICATION_CONFIG,\n DEFAULT_CHANNELS,\n DEFAULT_ROUTING,\n} from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// Deep merge helper (same approach as daemon/config.ts)\n// ---------------------------------------------------------------------------\n\nfunction deepMerge<T extends object>(\n target: T,\n source: Record<string, unknown>\n): T {\n const result = { ...target };\n for (const key of Object.keys(source)) {\n const srcVal = source[key];\n if (srcVal === undefined || srcVal === null) continue;\n const tgtVal = (target as Record<string, unknown>)[key];\n if (\n typeof srcVal === \"object\" &&\n !Array.isArray(srcVal) &&\n typeof tgtVal === \"object\" &&\n tgtVal !== null &&\n !Array.isArray(tgtVal)\n ) {\n (result as Record<string, unknown>)[key] = deepMerge(\n tgtVal as object,\n srcVal as Record<string, unknown>\n );\n } else {\n (result as Record<string, unknown>)[key] = srcVal;\n }\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Load\n// ---------------------------------------------------------------------------\n\n/**\n * Load the notification config from the PAI config file.\n * Returns defaults merged with any stored values.\n */\nexport function loadNotificationConfig(): NotificationConfig {\n if (!existsSync(CONFIG_FILE)) {\n return { ...DEFAULT_NOTIFICATION_CONFIG };\n }\n\n let raw: string;\n try {\n raw = readFileSync(CONFIG_FILE, \"utf-8\");\n } catch {\n return { ...DEFAULT_NOTIFICATION_CONFIG };\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return { ...DEFAULT_NOTIFICATION_CONFIG };\n }\n\n const stored = parsed[\"notifications\"];\n if (!stored || typeof stored !== \"object\") {\n return { ...DEFAULT_NOTIFICATION_CONFIG };\n }\n\n return deepMerge(\n DEFAULT_NOTIFICATION_CONFIG,\n stored as Record<string, unknown>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Save\n// ---------------------------------------------------------------------------\n\n/**\n * Persist the notification config by merging it into the existing\n * ~/.config/pai/config.json. Creates the file if it does not exist.\n */\nexport function saveNotificationConfig(config: NotificationConfig): void {\n // Ensure the config dir exists\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n }\n\n // Read current full config\n let full: Record<string, unknown> = {};\n if (existsSync(CONFIG_FILE)) {\n try {\n full = JSON.parse(readFileSync(CONFIG_FILE, \"utf-8\")) as Record<\n string,\n unknown\n >;\n } catch {\n // Start fresh if the file is unreadable\n }\n }\n\n // Replace the notifications section\n full[\"notifications\"] = config;\n\n writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2) + \"\\n\", \"utf-8\");\n}\n\n// ---------------------------------------------------------------------------\n// Patch helpers (used by the set command)\n// ---------------------------------------------------------------------------\n\n/**\n * Apply a partial update to the current notification config and persist it.\n * Returns the new merged config.\n */\nexport function patchNotificationConfig(patch: {\n mode?: NotificationMode;\n channels?: Partial<Partial<ChannelConfigs>>;\n routing?: Partial<RoutingTable>;\n}): NotificationConfig {\n const current = loadNotificationConfig();\n\n if (patch.mode !== undefined) {\n current.mode = patch.mode;\n }\n\n if (patch.channels) {\n current.channels = deepMerge(\n current.channels,\n patch.channels as Record<string, unknown>\n );\n }\n\n if (patch.routing) {\n current.routing = deepMerge(\n current.routing,\n patch.routing as Record<string, unknown>\n );\n }\n\n saveNotificationConfig(current);\n return current;\n}\n\n// Re-export defaults for convenience\nexport { DEFAULT_NOTIFICATION_CONFIG, DEFAULT_CHANNELS, DEFAULT_ROUTING };\nexport { expandHome };\n","/**\n * Index, embed, vault index, and registry scan schedulers for the PAI daemon.\n * Exports run* functions (also called on-demand by the IPC handler)\n * and the start* functions invoked once at daemon startup.\n */\n\nimport { indexAll } from \"../../memory/indexer.js\";\nimport type { SQLiteBackendWithDb } from \"./types.js\";\nimport {\n registryDb,\n storageBackend,\n daemonConfig,\n indexInProgress,\n embedInProgress,\n vaultIndexInProgress,\n shutdownRequested,\n setIndexInProgress,\n setLastIndexTime,\n setIndexSchedulerTimer,\n setEmbedInProgress,\n setLastEmbedTime,\n setEmbedSchedulerTimer,\n setVaultIndexInProgress,\n setLastVaultIndexTime,\n} from \"./state.js\";\n\n// ---------------------------------------------------------------------------\n// Index scheduler\n// ---------------------------------------------------------------------------\n\n/** Minimum interval between vault index runs (30 minutes). */\nconst VAULT_INDEX_MIN_INTERVAL_MS = 30 * 60 * 1000;\n\n/**\n * Run a full index pass. Guards against overlapping runs with indexInProgress.\n * Called both by the scheduler and by the index_now IPC method.\n */\nexport async function runIndex(): Promise<void> {\n if (indexInProgress) {\n process.stderr.write(\"[pai-daemon] Index already in progress, skipping.\\n\");\n return;\n }\n\n if (embedInProgress) {\n process.stderr.write(\"[pai-daemon] Embed in progress, deferring index run.\\n\");\n return;\n }\n\n setIndexInProgress(true);\n const t0 = Date.now();\n\n try {\n process.stderr.write(\"[pai-daemon] Starting scheduled index run...\\n\");\n\n if (storageBackend.backendType === \"sqlite\") {\n const { SQLiteBackend } = await import(\"../../storage/sqlite.js\");\n if (storageBackend instanceof SQLiteBackend) {\n const db = (storageBackend as SQLiteBackendWithDb).getRawDb();\n const { projects, result } = await indexAll(db, registryDb);\n const elapsed = Date.now() - t0;\n setLastIndexTime(Date.now());\n process.stderr.write(\n `[pai-daemon] Index complete: ${projects} projects, ` +\n `${result.filesProcessed} files, ${result.chunksCreated} chunks ` +\n `(${elapsed}ms)\\n`\n );\n }\n } else {\n const { indexAllWithBackend } = await import(\"../../memory/indexer-backend.js\");\n const { projects, result } = await indexAllWithBackend(storageBackend, registryDb);\n const elapsed = Date.now() - t0;\n setLastIndexTime(Date.now());\n process.stderr.write(\n `[pai-daemon] Index complete (postgres): ${projects} projects, ` +\n `${result.filesProcessed} files, ${result.chunksCreated} chunks ` +\n `(${elapsed}ms)\\n`\n );\n }\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Index error: ${msg}\\n`);\n } finally {\n setIndexInProgress(false);\n }\n}\n\n/**\n * Run a vault index pass. Guards against overlapping runs with vaultIndexInProgress.\n * Skips if no vaultPath is configured, or if project index/embed is in progress.\n */\nexport async function runVaultIndex(): Promise<void> {\n if (!daemonConfig.vaultPath) return;\n\n if (vaultIndexInProgress) {\n process.stderr.write(\"[pai-daemon] Vault index already in progress, skipping.\\n\");\n return;\n }\n\n if (indexInProgress || embedInProgress) {\n process.stderr.write(\"[pai-daemon] Index/embed in progress, deferring vault index.\\n\");\n return;\n }\n\n // Import lastVaultIndexTime from state (re-read each call since it may change)\n const { lastVaultIndexTime } = await import(\"./state.js\");\n if (lastVaultIndexTime > 0 && Date.now() - lastVaultIndexTime < VAULT_INDEX_MIN_INTERVAL_MS) {\n return;\n }\n\n let vaultProjectId = daemonConfig.vaultProjectId;\n if (!vaultProjectId) {\n const row = registryDb\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(daemonConfig.vaultPath) as { id: number } | undefined;\n vaultProjectId = row?.id ?? 999;\n if (!row) {\n process.stderr.write(\"[pai-daemon] Vault not in project registry — using synthetic project ID 999.\\n\");\n }\n }\n\n setVaultIndexInProgress(true);\n const t0 = Date.now();\n\n process.stderr.write(\"[pai-daemon] Starting vault index run...\\n\");\n\n try {\n const { indexVault } = await import(\"../../memory/vault-indexer.js\");\n const r = await indexVault(storageBackend, vaultProjectId, daemonConfig.vaultPath!);\n const elapsed = Date.now() - t0;\n setLastVaultIndexTime(Date.now());\n process.stderr.write(\n `[pai-daemon] Vault index complete: ${r.filesIndexed} files, ` +\n `${r.linksExtracted} links, ${r.deadLinksFound} dead, ` +\n `${r.orphansFound} orphans (${elapsed}ms)\\n`\n );\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Vault index error: ${msg}\\n`);\n } finally {\n setVaultIndexInProgress(false);\n }\n}\n\n/**\n * Start the periodic index scheduler. Runs an initial pass 2 seconds after startup.\n */\nexport function startIndexScheduler(): void {\n const intervalMs = daemonConfig.indexIntervalSecs * 1_000;\n\n process.stderr.write(\n `[pai-daemon] Index scheduler: every ${daemonConfig.indexIntervalSecs}s\\n`\n );\n\n setTimeout(() => {\n runIndex()\n .then(() => runVaultIndex())\n .catch((e) => {\n process.stderr.write(`[pai-daemon] Startup index error: ${e}\\n`);\n });\n }, 2_000);\n\n const timer = setInterval(() => {\n runIndex()\n .then(() => runVaultIndex())\n .catch((e) => {\n process.stderr.write(`[pai-daemon] Scheduled index error: ${e}\\n`);\n });\n }, intervalMs);\n\n if (timer.unref) timer.unref();\n setIndexSchedulerTimer(timer);\n}\n\n// ---------------------------------------------------------------------------\n// Embed scheduler\n// ---------------------------------------------------------------------------\n\n/**\n * Run an embedding pass for all unembedded chunks (Postgres backend only).\n */\nexport async function runEmbed(): Promise<void> {\n if (embedInProgress) {\n process.stderr.write(\"[pai-daemon] Embed already in progress, skipping.\\n\");\n return;\n }\n\n if (indexInProgress) {\n process.stderr.write(\"[pai-daemon] Index in progress, deferring embed pass.\\n\");\n return;\n }\n\n if (storageBackend.backendType !== \"postgres\") {\n return;\n }\n\n setEmbedInProgress(true);\n const t0 = Date.now();\n\n try {\n process.stderr.write(\"[pai-daemon] Starting scheduled embed pass...\\n\");\n\n const projectNames = new Map<number, string>();\n try {\n const rows = registryDb\n .prepare(\"SELECT id, slug FROM projects WHERE status = 'active'\")\n .all() as Array<{ id: number; slug: string }>;\n for (const r of rows) projectNames.set(r.id, r.slug);\n } catch { /* registry unavailable — IDs will be used instead */ }\n\n const { embedChunksWithBackend } = await import(\"../../memory/indexer-backend.js\");\n const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested, projectNames);\n\n let vaultEmbedCount = 0;\n if (daemonConfig.vaultPath) {\n try {\n const { SQLiteBackend } = await import(\"../../storage/sqlite.js\");\n const { openFederation } = await import(\"../../memory/db.js\");\n const federationDb = openFederation();\n const vaultSqliteBackend = new SQLiteBackend(federationDb);\n\n const vaultProjectNames = new Map(projectNames);\n if (!vaultProjectNames.has(999)) {\n vaultProjectNames.set(999, \"obsidian-vault\");\n }\n\n vaultEmbedCount = await embedChunksWithBackend(\n vaultSqliteBackend,\n () => shutdownRequested,\n vaultProjectNames,\n );\n\n try { federationDb.close(); } catch { /* ignore */ }\n\n if (vaultEmbedCount > 0) {\n process.stderr.write(\n `[pai-daemon] Vault embed pass complete: ${vaultEmbedCount} vault chunks embedded\\n`\n );\n }\n } catch (ve) {\n const vmsg = ve instanceof Error ? ve.message : String(ve);\n process.stderr.write(`[pai-daemon] Vault embed error: ${vmsg}\\n`);\n }\n }\n\n const elapsed = Date.now() - t0;\n setLastEmbedTime(Date.now());\n process.stderr.write(\n `[pai-daemon] Embed pass complete: ${count} postgres chunks + ${vaultEmbedCount} vault chunks embedded (${elapsed}ms)\\n`\n );\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Embed error: ${msg}\\n`);\n } finally {\n setEmbedInProgress(false);\n }\n}\n\n/**\n * Start the periodic embed scheduler. Initial run is 60 seconds after startup.\n */\nexport function startEmbedScheduler(): void {\n const intervalMs = daemonConfig.embedIntervalSecs * 1_000;\n\n process.stderr.write(\n `[pai-daemon] Embed scheduler: every ${daemonConfig.embedIntervalSecs}s\\n`\n );\n\n setTimeout(() => {\n runEmbed().catch((e) => {\n process.stderr.write(`[pai-daemon] Startup embed error: ${e}\\n`);\n });\n }, 60_000);\n\n const timer = setInterval(() => {\n runEmbed().catch((e) => {\n process.stderr.write(`[pai-daemon] Scheduled embed error: ${e}\\n`);\n });\n }, intervalMs);\n\n if (timer.unref) timer.unref();\n setEmbedSchedulerTimer(timer);\n}\n\n// ---------------------------------------------------------------------------\n// Registry scan scheduler\n// ---------------------------------------------------------------------------\n\nconst REGISTRY_SCAN_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes\nconst REGISTRY_SCAN_STARTUP_DELAY_MS = 10_000; // 10 seconds after daemon start\n\n/**\n * Start the periodic registry scan scheduler.\n * First scan runs 10 seconds after startup (enough time for IPC to stabilise).\n * Subsequent scans run every 30 minutes.\n * Uses the work-queue so the scan runs on the worker thread without blocking IPC.\n */\nexport function startRegistryScanScheduler(): void {\n process.stderr.write(\n \"[pai-daemon] Registry scan scheduler: every 30min (first in 10s)\\n\"\n );\n\n setTimeout(() => {\n import(\"../../daemon/work-queue-worker.js\")\n .then(({ enqueueRegistryScan }) => enqueueRegistryScan())\n .catch((e) => {\n process.stderr.write(`[pai-daemon] Startup registry scan error: ${e}\\n`);\n });\n }, REGISTRY_SCAN_STARTUP_DELAY_MS);\n\n const timer = setInterval(() => {\n import(\"../../daemon/work-queue-worker.js\")\n .then(({ enqueueRegistryScan }) => enqueueRegistryScan())\n .catch((e) => {\n process.stderr.write(`[pai-daemon] Scheduled registry scan error: ${e}\\n`);\n });\n }, REGISTRY_SCAN_INTERVAL_MS);\n\n if (timer.unref) timer.unref();\n}\n","/**\n * ntfy.ts — ntfy.sh notification provider\n *\n * Sends notifications to a configured ntfy.sh topic via HTTP.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class NtfyProvider implements NotificationProvider {\n readonly channelId = \"ntfy\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.ntfy;\n if (!cfg.enabled || !cfg.url) return false;\n\n try {\n const headers: Record<string, string> = {\n \"Content-Type\": \"text/plain; charset=utf-8\",\n };\n\n if (payload.title) {\n headers[\"Title\"] = payload.title;\n }\n\n if (cfg.priority && cfg.priority !== \"default\") {\n headers[\"Priority\"] = cfg.priority;\n }\n\n const response = await fetch(cfg.url, {\n method: \"POST\",\n headers,\n body: payload.message,\n });\n\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n","/**\n * whatsapp.ts — WhatsApp notification provider (via Whazaa MCP)\n *\n * Sends notifications via the Whazaa Unix Domain Socket IPC protocol.\n * Falls back gracefully if Whazaa is not running.\n *\n * Whazaa IPC socket: /tmp/whazaa.sock (standard Whazaa path)\n *\n * We use the same connect-per-call pattern as PaiClient to avoid\n * requiring any persistent connection state.\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nconst WHAZAA_SOCKET = \"/tmp/whazaa.sock\";\nconst WHAZAA_TIMEOUT_MS = 10_000;\n\n/**\n * Send a single IPC call to the Whazaa socket.\n * Returns true on success, false if Whazaa is not available or errors.\n */\nfunction callWhazaa(\n method: string,\n params: Record<string, unknown>\n): Promise<boolean> {\n return new Promise((resolve) => {\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(ok: boolean): void {\n if (done) return;\n done = true;\n if (timer) { clearTimeout(timer); timer = null; }\n try { socket?.destroy(); } catch { /* ignore */ }\n resolve(ok);\n }\n\n const socket = connect(WHAZAA_SOCKET, () => {\n const request = {\n jsonrpc: \"2.0\",\n id: randomUUID(),\n method,\n params,\n };\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n try {\n const resp = JSON.parse(buffer.slice(0, nl)) as { error?: unknown };\n finish(!resp.error);\n } catch {\n finish(false);\n }\n });\n\n socket.on(\"error\", () => finish(false));\n socket.on(\"end\", () => finish(false));\n\n timer = setTimeout(() => finish(false), WHAZAA_TIMEOUT_MS);\n });\n}\n\nexport class WhatsAppProvider implements NotificationProvider {\n readonly channelId = \"whatsapp\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.whatsapp;\n if (!cfg.enabled) return false;\n\n const isVoiceMode = config.mode === \"voice\" || config.channels.voice.enabled;\n\n const params: Record<string, unknown> = {\n message: payload.message,\n };\n\n if (cfg.recipient) {\n params.recipient = cfg.recipient;\n }\n\n if (isVoiceMode && config.mode === \"voice\") {\n const voiceName = config.channels.voice.voiceName ?? \"bm_george\";\n params.voice = voiceName;\n }\n\n return callWhazaa(\"whatsapp_send\", params);\n }\n}\n","/**\n * macos.ts — macOS notification provider\n *\n * Uses the `osascript` command to display a macOS system notification.\n * Non-blocking: spawns the process and returns success without waiting.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class MacOsProvider implements NotificationProvider {\n readonly channelId = \"macos\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.macos;\n if (!cfg.enabled) return false;\n\n try {\n const title = payload.title ?? \"PAI\";\n // Escape single quotes in title and message for AppleScript\n const safeTitle = title.replace(/'/g, \"\\\\'\");\n const safeMessage = payload.message.replace(/'/g, \"\\\\'\");\n\n const script = `display notification \"${safeMessage}\" with title \"${safeTitle}\"`;\n\n return new Promise((resolve) => {\n const child = spawn(\"osascript\", [\"-e\", script], {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n // Give the process a moment to start, then assume success.\n // osascript is always present on macOS.\n child.on(\"error\", () => resolve(false));\n\n // Resolve after a short timeout — osascript exits quickly\n setTimeout(() => resolve(true), 200);\n });\n } catch {\n return false;\n }\n }\n}\n","/**\n * cli.ts — CLI notification provider\n *\n * Writes notifications to the PAI daemon log (stderr).\n * Always succeeds — it's the fallback channel.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class CliProvider implements NotificationProvider {\n readonly channelId = \"cli\" as const;\n\n async send(\n payload: NotificationPayload,\n _config: NotificationConfig\n ): Promise<boolean> {\n const prefix = `[pai-notify:${payload.event}]`;\n const title = payload.title ? ` ${payload.title}:` : \"\";\n process.stderr.write(`${prefix}${title} ${payload.message}\\n`);\n return true;\n }\n}\n","/**\n * router.ts — Notification router\n *\n * Routes notification events to the appropriate channels based on the\n * current mode and per-event routing config.\n *\n * Channel providers are instantiated lazily and cached.\n */\n\nimport type {\n NotificationPayload,\n NotificationConfig,\n NotificationProvider,\n ChannelId,\n SendResult,\n NotificationMode,\n} from \"./types.js\";\nimport { NtfyProvider } from \"./providers/ntfy.js\";\nimport { WhatsAppProvider } from \"./providers/whatsapp.js\";\nimport { MacOsProvider } from \"./providers/macos.js\";\nimport { CliProvider } from \"./providers/cli.js\";\n\n// ---------------------------------------------------------------------------\n// Provider registry (singletons — stateless, safe to reuse)\n// ---------------------------------------------------------------------------\n\nconst PROVIDERS: Record<ChannelId, NotificationProvider> = {\n ntfy: new NtfyProvider(),\n whatsapp: new WhatsAppProvider(),\n macos: new MacOsProvider(),\n voice: new WhatsAppProvider(), // Voice uses WhatsApp TTS; handled in WhatsAppProvider\n cli: new CliProvider(),\n};\n\n// ---------------------------------------------------------------------------\n// Channel resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Given the current config, resolve which channels should receive a\n * notification for the given event type.\n *\n * Mode overrides:\n * \"off\" → no channels\n * \"auto\" → use routing table, filtered by enabled channels\n * \"voice\" → whatsapp (TTS enabled in provider)\n * \"whatsapp\" → whatsapp\n * \"ntfy\" → ntfy\n * \"macos\" → macos\n * \"cli\" → cli\n */\nfunction resolveChannels(\n config: NotificationConfig,\n event: NotificationPayload[\"event\"]\n): ChannelId[] {\n const { mode, channels, routing } = config;\n\n if (mode === \"off\") return [];\n\n // Non-auto modes: force a single channel\n const modeToChannel: Partial<Record<NotificationMode, ChannelId>> = {\n voice: \"whatsapp\", // WhatsAppProvider checks mode === \"voice\" for TTS\n whatsapp: \"whatsapp\",\n ntfy: \"ntfy\",\n macos: \"macos\",\n cli: \"cli\",\n };\n\n if (mode !== \"auto\") {\n const ch = modeToChannel[mode];\n if (!ch) return [];\n // Check the channel is enabled\n const cfg = channels[ch];\n if (cfg && !cfg.enabled) return [ch]; // Still send — mode override bypasses enabled check\n return [ch];\n }\n\n // Auto mode: use routing table, filter to enabled channels\n const candidates = routing[event] ?? [];\n return candidates.filter((ch) => {\n const cfg = channels[ch];\n // \"voice\" channel is virtual — it overlaps with whatsapp.\n // Skip \"voice\" as an independent channel; voice is handled by checking config.mode.\n if (ch === \"voice\") return false;\n return cfg?.enabled === true;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/**\n * Route a notification to the appropriate channels.\n *\n * Sends to all resolved channels in parallel.\n * Individual channel failures are non-fatal and logged to stderr.\n *\n * @param payload The notification to send\n * @param config The current notification config (from daemon state)\n */\nexport async function routeNotification(\n payload: NotificationPayload,\n config: NotificationConfig\n): Promise<SendResult> {\n const channels = resolveChannels(config, payload.event);\n\n if (channels.length === 0) {\n return {\n channelsAttempted: [],\n channelsSucceeded: [],\n channelsFailed: [],\n mode: config.mode,\n };\n }\n\n const results = await Promise.allSettled(\n channels.map(async (ch) => {\n const provider = PROVIDERS[ch];\n const ok = await provider.send(payload, config);\n if (!ok) {\n process.stderr.write(\n `[pai-notify] Channel ${ch} failed for event ${payload.event}\\n`\n );\n }\n return { ch, ok };\n })\n );\n\n const succeeded: ChannelId[] = [];\n const failed: ChannelId[] = [];\n\n for (const r of results) {\n if (r.status === \"fulfilled\") {\n if (r.value.ok) {\n succeeded.push(r.value.ch);\n } else {\n failed.push(r.value.ch);\n }\n } else {\n // Provider threw — treat as failure\n failed.push(channels[results.indexOf(r)]);\n }\n }\n\n return {\n channelsAttempted: channels,\n channelsSucceeded: succeeded,\n channelsFailed: failed,\n mode: config.mode,\n };\n}\n","/**\n * store.ts — PostgreSQL persistence for PAI observations.\n *\n * All functions accept a pg.Pool and are safe to call concurrently.\n * Schema is initialized lazily via ensureObservationTables().\n *\n * Content-hash deduplication: observations with the same hash\n * created within a 30-second window are silently dropped to prevent\n * duplicate entries from rapid repeated tool calls.\n */\n\nimport { sha256 } from '../utils/hash.js';\nimport type { Pool } from 'pg';\nimport type { ClassifiedObservation } from './classifier.js';\n\n// ---------------------------------------------------------------------------\n// Row types\n// ---------------------------------------------------------------------------\n\nexport interface ObservationRow {\n id: number;\n session_id: string;\n project_id: number | null;\n project_slug: string | null;\n type: string;\n title: string;\n narrative: string | null;\n tool_name: string | null;\n tool_input_summary: string | null;\n files_read: string[];\n files_modified: string[];\n concepts: string[];\n content_hash: string | null;\n created_at: Date;\n}\n\nexport interface SessionSummaryRow {\n id: number;\n session_id: string;\n project_id: number | null;\n project_slug: string | null;\n request: string | null;\n investigated: string | null;\n learned: string | null;\n completed: string | null;\n next_steps: string | null;\n observation_count: number;\n created_at: Date;\n}\n\n// ---------------------------------------------------------------------------\n// Input types\n// ---------------------------------------------------------------------------\n\nexport interface StoreObservationInput extends Omit<ClassifiedObservation, 'narrative'> {\n session_id: string;\n project_id?: number | null;\n project_slug?: string | null;\n narrative?: string | null;\n}\n\nexport interface StoreSessionSummaryInput {\n session_id: string;\n project_id?: number | null;\n project_slug?: string | null;\n request?: string | null;\n investigated?: string | null;\n learned?: string | null;\n completed?: string | null;\n next_steps?: string | null;\n observation_count?: number;\n}\n\nexport interface QueryObservationsOptions {\n projectId?: number;\n sessionId?: string;\n type?: string;\n limit?: number;\n offset?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Schema initialisation\n// ---------------------------------------------------------------------------\n\nlet _tablesEnsured = false;\n\n/**\n * Inlined schema DDL — avoids runtime file reads that break in bundled code\n * (the bundler puts this in a shared chunk whose __dirname differs from src/).\n */\nconst SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS pai_observations (\n id SERIAL PRIMARY KEY,\n session_id TEXT NOT NULL,\n project_id INTEGER,\n project_slug TEXT,\n type TEXT NOT NULL CHECK (type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),\n title TEXT NOT NULL,\n narrative TEXT,\n tool_name TEXT,\n tool_input_summary TEXT,\n files_read JSONB DEFAULT '[]'::jsonb,\n files_modified JSONB DEFAULT '[]'::jsonb,\n concepts JSONB DEFAULT '[]'::jsonb,\n content_hash TEXT,\n created_at TIMESTAMPTZ DEFAULT NOW()\n);\n\nCREATE INDEX IF NOT EXISTS idx_obs_project ON pai_observations(project_id);\nCREATE INDEX IF NOT EXISTS idx_obs_session ON pai_observations(session_id);\nCREATE INDEX IF NOT EXISTS idx_obs_type ON pai_observations(type);\nCREATE INDEX IF NOT EXISTS idx_obs_created ON pai_observations(created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_obs_hash ON pai_observations(content_hash);\n\nCREATE TABLE IF NOT EXISTS pai_session_summaries (\n id SERIAL PRIMARY KEY,\n session_id TEXT NOT NULL UNIQUE,\n project_id INTEGER,\n project_slug TEXT,\n request TEXT,\n investigated TEXT,\n learned TEXT,\n completed TEXT,\n next_steps TEXT,\n observation_count INTEGER DEFAULT 0,\n created_at TIMESTAMPTZ DEFAULT NOW()\n);\n\nCREATE INDEX IF NOT EXISTS idx_ss_project ON pai_session_summaries(project_id);\nCREATE INDEX IF NOT EXISTS idx_ss_session ON pai_session_summaries(session_id);\n`;\n\n/**\n * Run schema DDL idempotently against the given pool.\n * Uses a module-level flag so subsequent calls are no-ops within the same\n * process lifetime (the SQL itself uses IF NOT EXISTS so it is safe to re-run).\n */\nexport async function ensureObservationTables(pool: Pool): Promise<void> {\n if (_tablesEnsured) return;\n await pool.query(SCHEMA_SQL);\n _tablesEnsured = true;\n}\n\n// ---------------------------------------------------------------------------\n// Content-hash deduplication\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a 16-character hex content hash for deduplication.\n * Hash = SHA256(session_id + tool_name + title).slice(0, 16)\n */\nfunction computeContentHash(sessionId: string, toolName: string, title: string): string {\n return sha256(sessionId + '\\x00' + toolName + '\\x00' + title).slice(0, 16);\n}\n\n// ---------------------------------------------------------------------------\n// Store observation\n// ---------------------------------------------------------------------------\n\n/**\n * Insert an observation, skipping duplicates within a 30-second window.\n * Returns the inserted row's id, or null if the insert was suppressed.\n */\nexport async function storeObservation(\n pool: Pool,\n obs: StoreObservationInput\n): Promise<number | null> {\n await ensureObservationTables(pool);\n\n const hash = computeContentHash(obs.session_id, obs.tool_name, obs.title);\n\n // Check for a recent duplicate (30-second window)\n const dupCheck = await pool.query<{ id: number }>(\n `SELECT id FROM pai_observations\n WHERE content_hash = $1\n AND session_id = $2\n AND created_at >= NOW() - INTERVAL '30 seconds'\n LIMIT 1`,\n [hash, obs.session_id]\n );\n\n if (dupCheck.rowCount && dupCheck.rowCount > 0) {\n // Duplicate within dedup window — silently skip\n return null;\n }\n\n const result = await pool.query<{ id: number }>(\n `INSERT INTO pai_observations\n (session_id, project_id, project_slug, type, title, narrative,\n tool_name, tool_input_summary, files_read, files_modified, concepts, content_hash)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12)\n RETURNING id`,\n [\n obs.session_id,\n obs.project_id ?? null,\n obs.project_slug ?? null,\n obs.type,\n obs.title,\n obs.narrative ?? null,\n obs.tool_name,\n obs.tool_input_summary ?? null,\n JSON.stringify(obs.files_read),\n JSON.stringify(obs.files_modified),\n JSON.stringify(obs.concepts),\n hash,\n ]\n );\n\n return result.rows[0]?.id ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Query observations\n// ---------------------------------------------------------------------------\n\n/**\n * Filtered query for observations with optional projectId, sessionId, type,\n * limit, and offset. Returns results ordered by created_at DESC.\n */\nexport async function queryObservations(\n pool: Pool,\n opts: QueryObservationsOptions = {}\n): Promise<ObservationRow[]> {\n await ensureObservationTables(pool);\n\n const conditions: string[] = [];\n const params: unknown[] = [];\n let idx = 1;\n\n if (opts.projectId !== undefined) {\n conditions.push(`project_id = $${idx++}`);\n params.push(opts.projectId);\n }\n if (opts.sessionId !== undefined) {\n conditions.push(`session_id = $${idx++}`);\n params.push(opts.sessionId);\n }\n if (opts.type !== undefined) {\n conditions.push(`type = $${idx++}`);\n params.push(opts.type);\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n const limit = opts.limit ?? 50;\n const offset = opts.offset ?? 0;\n\n params.push(limit, offset);\n\n const result = await pool.query<ObservationRow>(\n `SELECT id, session_id, project_id, project_slug, type, title, narrative,\n tool_name, tool_input_summary,\n files_read, files_modified, concepts,\n content_hash, created_at\n FROM pai_observations\n ${where}\n ORDER BY created_at DESC\n LIMIT $${idx++} OFFSET $${idx}`,\n params\n );\n\n return result.rows;\n}\n\n/**\n * Most recent observations for a project, ordered by created_at DESC.\n */\nexport async function queryRecentObservations(\n pool: Pool,\n projectId: number,\n limit: number\n): Promise<ObservationRow[]> {\n await ensureObservationTables(pool);\n\n const result = await pool.query<ObservationRow>(\n `SELECT id, session_id, project_id, project_slug, type, title, narrative,\n tool_name, tool_input_summary,\n files_read, files_modified, concepts,\n content_hash, created_at\n FROM pai_observations\n WHERE project_id = $1\n ORDER BY created_at DESC\n LIMIT $2`,\n [projectId, limit]\n );\n\n return result.rows;\n}\n\n/**\n * All observations for a specific session, ordered chronologically.\n */\nexport async function querySessionObservations(\n pool: Pool,\n sessionId: string\n): Promise<ObservationRow[]> {\n await ensureObservationTables(pool);\n\n const result = await pool.query<ObservationRow>(\n `SELECT id, session_id, project_id, project_slug, type, title, narrative,\n tool_name, tool_input_summary,\n files_read, files_modified, concepts,\n content_hash, created_at\n FROM pai_observations\n WHERE session_id = $1\n ORDER BY created_at ASC`,\n [sessionId]\n );\n\n return result.rows;\n}\n\n// ---------------------------------------------------------------------------\n// Session summaries\n// ---------------------------------------------------------------------------\n\n/**\n * Upsert a session summary. Uses ON CONFLICT on session_id so calling this\n * multiple times with updated content is safe.\n */\nexport async function storeSessionSummary(\n pool: Pool,\n summary: StoreSessionSummaryInput\n): Promise<void> {\n await ensureObservationTables(pool);\n\n await pool.query(\n `INSERT INTO pai_session_summaries\n (session_id, project_id, project_slug, request, investigated,\n learned, completed, next_steps, observation_count)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (session_id) DO UPDATE SET\n project_id = EXCLUDED.project_id,\n project_slug = EXCLUDED.project_slug,\n request = EXCLUDED.request,\n investigated = EXCLUDED.investigated,\n learned = EXCLUDED.learned,\n completed = EXCLUDED.completed,\n next_steps = EXCLUDED.next_steps,\n observation_count = EXCLUDED.observation_count`,\n [\n summary.session_id,\n summary.project_id ?? null,\n summary.project_slug ?? null,\n summary.request ?? null,\n summary.investigated ?? null,\n summary.learned ?? null,\n summary.completed ?? null,\n summary.next_steps ?? null,\n summary.observation_count ?? 0,\n ]\n );\n}\n\n// ---------------------------------------------------------------------------\n// Skill telemetry — self-educating skill system, Phase 1 (capture)\n// ---------------------------------------------------------------------------\n\nexport interface SkillTelemetryRow {\n id: number;\n scope: string;\n skill_name: string;\n source: string;\n status: string;\n trigger_count: number;\n accept_count: number;\n first_triggered: Date;\n last_triggered: Date;\n context_projects: string[];\n hash: string | null;\n audit_status: string | null;\n last_audited: Date | null;\n}\n\nexport interface RecordSkillInvocationInput {\n skill_name: string;\n /** 'local' | 'skills.sh' | repo slug */\n source?: string;\n /** governance seam — defaults to 'default' */\n scope?: string;\n /** project slug for context_projects rollup */\n project_slug?: string | null;\n}\n\nexport interface QuerySkillTelemetryOptions {\n scope?: string;\n status?: string;\n limit?: number;\n}\n\nconst SKILL_TELEMETRY_SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS pai_skill_telemetry (\n id SERIAL PRIMARY KEY,\n scope TEXT NOT NULL DEFAULT 'default',\n skill_name TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'local',\n status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('trial','active','archived')),\n trigger_count INTEGER NOT NULL DEFAULT 0,\n accept_count INTEGER NOT NULL DEFAULT 0,\n first_triggered TIMESTAMPTZ DEFAULT NOW(),\n last_triggered TIMESTAMPTZ DEFAULT NOW(),\n context_projects JSONB DEFAULT '[]'::jsonb,\n hash TEXT,\n audit_status TEXT,\n last_audited TIMESTAMPTZ,\n UNIQUE(scope, skill_name, source)\n);\n\nCREATE INDEX IF NOT EXISTS idx_skilltel_scope ON pai_skill_telemetry(scope);\nCREATE INDEX IF NOT EXISTS idx_skilltel_status ON pai_skill_telemetry(scope, status);\nCREATE INDEX IF NOT EXISTS idx_skilltel_last ON pai_skill_telemetry(last_triggered DESC);\n`;\n\nlet _skillTelemetryEnsured = false;\n\nexport async function ensureSkillTelemetryTable(pool: Pool): Promise<void> {\n if (_skillTelemetryEnsured) return;\n await pool.query(SKILL_TELEMETRY_SCHEMA_SQL);\n _skillTelemetryEnsured = true;\n}\n\n/**\n * Record a single skill invocation. Upserts on (scope, skill_name, source):\n * increments trigger_count, refreshes last_triggered, and dedup-merges the\n * project slug into context_projects.\n */\nexport async function recordSkillInvocation(\n pool: Pool,\n input: RecordSkillInvocationInput\n): Promise<void> {\n await ensureSkillTelemetryTable(pool);\n\n const scope = input.scope ?? 'default';\n const source = input.source ?? 'local';\n const proj = input.project_slug ?? null;\n const initialProjects = JSON.stringify(proj ? [proj] : []);\n\n await pool.query(\n `INSERT INTO pai_skill_telemetry\n (scope, skill_name, source, trigger_count, context_projects)\n VALUES ($1, $2, $3, 1, $4::jsonb)\n ON CONFLICT (scope, skill_name, source) DO UPDATE SET\n trigger_count = pai_skill_telemetry.trigger_count + 1,\n last_triggered = NOW(),\n context_projects = CASE\n WHEN $5::text IS NULL THEN pai_skill_telemetry.context_projects\n WHEN pai_skill_telemetry.context_projects @> to_jsonb($5::text)\n THEN pai_skill_telemetry.context_projects\n ELSE pai_skill_telemetry.context_projects || to_jsonb($5::text)\n END`,\n [scope, input.skill_name, source, initialProjects, proj]\n );\n}\n\n/**\n * List skill telemetry rows, most-triggered first.\n */\nexport async function querySkillTelemetry(\n pool: Pool,\n opts: QuerySkillTelemetryOptions = {}\n): Promise<SkillTelemetryRow[]> {\n await ensureSkillTelemetryTable(pool);\n\n const conditions: string[] = [];\n const params: unknown[] = [];\n let idx = 1;\n\n conditions.push(`scope = $${idx++}`);\n params.push(opts.scope ?? 'default');\n\n if (opts.status !== undefined) {\n conditions.push(`status = $${idx++}`);\n params.push(opts.status);\n }\n\n const where = `WHERE ${conditions.join(' AND ')}`;\n const limit = opts.limit ?? 100;\n params.push(limit);\n\n const result = await pool.query<SkillTelemetryRow>(\n `SELECT id, scope, skill_name, source, status,\n trigger_count, accept_count,\n first_triggered, last_triggered,\n context_projects, hash, audit_status, last_audited\n FROM pai_skill_telemetry\n ${where}\n ORDER BY trigger_count DESC, last_triggered DESC\n LIMIT $${idx}`,\n params\n );\n\n return result.rows;\n}\n\n/**\n * Most recent session summaries for a project, ordered by created_at DESC.\n */\nexport async function queryRecentSummaries(\n pool: Pool,\n projectId: number,\n limit: number\n): Promise<SessionSummaryRow[]> {\n await ensureObservationTables(pool);\n\n const result = await pool.query<SessionSummaryRow>(\n `SELECT id, session_id, project_id, project_slug,\n request, investigated, learned, completed, next_steps,\n observation_count, created_at\n FROM pai_session_summaries\n WHERE project_id = $1\n ORDER BY created_at DESC\n LIMIT $2`,\n [projectId, limit]\n );\n\n return result.rows;\n}\n","/**\n * Tool dispatcher — maps IPC method names to PAI tool functions.\n */\n\nimport {\n toolMemorySearch,\n toolMemoryGet,\n toolProjectInfo,\n toolProjectList,\n toolSessionList,\n toolRegistrySearch,\n toolProjectDetect,\n toolProjectHealth,\n toolProjectTodo,\n toolSessionRoute,\n toolMemoryWakeup,\n toolMemoryTaxonomy,\n toolMemoryFeedback,\n toolMemoryKgSearch,\n} from \"../../mcp/tools.js\";\nimport { detectTopicShift } from \"../../topics/detector.js\";\nimport { registryDb, storageBackend, daemonConfig } from \"./state.js\";\nimport { openFederation } from \"../../memory/db.js\";\nimport type { PostgresBackendWithPool } from \"./types.js\";\n\n/**\n * Dispatch an IPC tool call to the appropriate tool function.\n * Returns the tool result or throws on unknown/failed methods.\n */\nexport async function dispatchTool(\n method: string,\n params: Record<string, unknown>\n): Promise<unknown> {\n // Cast through unknown to satisfy TypeScript's strict overlap check on\n // Record<string, unknown> → specific param types. Runtime validation is\n // the responsibility of each tool function (they surface errors gracefully).\n const p = params as unknown;\n\n switch (method) {\n case \"memory_search\":\n return toolMemorySearch(registryDb, storageBackend, p as Parameters<typeof toolMemorySearch>[2]);\n\n case \"memory_get\":\n return toolMemoryGet(registryDb, p as Parameters<typeof toolMemoryGet>[1]);\n\n case \"project_info\":\n return toolProjectInfo(registryDb, p as Parameters<typeof toolProjectInfo>[1]);\n\n case \"project_list\":\n return toolProjectList(registryDb, p as Parameters<typeof toolProjectList>[1]);\n\n case \"session_list\":\n return toolSessionList(registryDb, p as Parameters<typeof toolSessionList>[1]);\n\n case \"registry_search\":\n return toolRegistrySearch(registryDb, p as Parameters<typeof toolRegistrySearch>[1]);\n\n case \"project_detect\":\n return toolProjectDetect(registryDb, p as Parameters<typeof toolProjectDetect>[1]);\n\n case \"project_health\":\n return toolProjectHealth(registryDb, p as Parameters<typeof toolProjectHealth>[1]);\n\n case \"project_todo\":\n return toolProjectTodo(registryDb, p as Parameters<typeof toolProjectTodo>[1]);\n\n case \"memory_wakeup\":\n return toolMemoryWakeup(registryDb, p as Parameters<typeof toolMemoryWakeup>[1]);\n\n case \"memory_taxonomy\":\n return toolMemoryTaxonomy(registryDb, storageBackend, p as Parameters<typeof toolMemoryTaxonomy>[2]);\n\n case \"topic_check\":\n return detectTopicShift(\n registryDb,\n storageBackend,\n p as Parameters<typeof detectTopicShift>[2]\n );\n\n case \"session_auto_route\":\n return toolSessionRoute(\n registryDb,\n storageBackend,\n p as Parameters<typeof toolSessionRoute>[2]\n );\n\n case \"zettel_explore\":\n case \"zettel_health\":\n case \"zettel_surprise\":\n case \"zettel_suggest\":\n case \"zettel_converse\":\n case \"zettel_themes\":\n case \"zettel_god_notes\":\n case \"zettel_communities\": {\n const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes, toolZettelGodNotes, toolZettelCommunities } = await import(\"../../mcp/tools.js\");\n\n switch (method) {\n case \"zettel_explore\": return toolZettelExplore(storageBackend, p as Parameters<typeof toolZettelExplore>[1]);\n case \"zettel_health\": return toolZettelHealth(storageBackend, p as Parameters<typeof toolZettelHealth>[1]);\n case \"zettel_surprise\": return toolZettelSurprise(storageBackend, p as Parameters<typeof toolZettelSurprise>[1]);\n case \"zettel_suggest\": return toolZettelSuggest(storageBackend, p as Parameters<typeof toolZettelSuggest>[1]);\n case \"zettel_converse\": return toolZettelConverse(storageBackend, p as Parameters<typeof toolZettelConverse>[1]);\n case \"zettel_themes\": return toolZettelThemes(storageBackend, p as Parameters<typeof toolZettelThemes>[1]);\n case \"zettel_god_notes\": return toolZettelGodNotes(storageBackend, p as Parameters<typeof toolZettelGodNotes>[1]);\n case \"zettel_communities\": return toolZettelCommunities(storageBackend, p as Parameters<typeof toolZettelCommunities>[1]);\n }\n break;\n }\n\n case \"graph_clusters\": {\n const { handleGraphClusters } = await import(\"../../graph/clusters.js\");\n const pgPool = (storageBackend as PostgresBackendWithPool).getPool?.() ?? null;\n return handleGraphClusters(pgPool, storageBackend, p as Parameters<typeof handleGraphClusters>[2]);\n }\n\n case \"graph_neighborhood\": {\n const { handleGraphNeighborhood } = await import(\"../../graph/neighborhood.js\");\n const pgPool = (storageBackend as PostgresBackendWithPool).getPool?.() ?? null;\n return handleGraphNeighborhood(pgPool, storageBackend, p as Parameters<typeof handleGraphNeighborhood>[2]);\n }\n\n case \"graph_note_context\": {\n const { handleGraphNoteContext } = await import(\"../../graph/note-context.js\");\n const pgPool = (storageBackend as PostgresBackendWithPool).getPool?.() ?? null;\n return handleGraphNoteContext(pgPool, storageBackend, p as Parameters<typeof handleGraphNoteContext>[2]);\n }\n\n case \"graph_trace\": {\n const { handleGraphTrace } = await import(\"../../graph/trace.js\");\n return handleGraphTrace(storageBackend, p as Parameters<typeof handleGraphTrace>[1]);\n }\n\n case \"graph_latent_ideas\": {\n const { handleGraphLatentIdeas } = await import(\"../../graph/latent-ideas.js\");\n return handleGraphLatentIdeas(storageBackend, p as Parameters<typeof handleGraphLatentIdeas>[1]);\n }\n\n case \"idea_materialize\": {\n const { handleIdeaMaterialize } = await import(\"../../graph/latent-ideas.js\");\n if (!daemonConfig.vaultPath) {\n throw new Error(\"idea_materialize requires vaultPath to be configured in the daemon config\");\n }\n return handleIdeaMaterialize(\n p as Parameters<typeof handleIdeaMaterialize>[0],\n daemonConfig.vaultPath\n );\n }\n\n case \"kg_add\":\n case \"kg_query\":\n case \"kg_invalidate\":\n case \"kg_contradictions\": {\n const { toolKgAdd, toolKgQuery, toolKgInvalidate, toolKgContradictions } = await import(\"../../mcp/tools.js\");\n const pgPool = (storageBackend as PostgresBackendWithPool).getPool?.() ?? null;\n if (!pgPool) {\n throw new Error(`${method} requires a Postgres storage backend`);\n }\n switch (method) {\n case \"kg_add\": return toolKgAdd(pgPool, p as Parameters<typeof toolKgAdd>[1]);\n case \"kg_query\": return toolKgQuery(pgPool, p as Parameters<typeof toolKgQuery>[1]);\n case \"kg_invalidate\": return toolKgInvalidate(pgPool, p as Parameters<typeof toolKgInvalidate>[1]);\n case \"kg_contradictions\": return toolKgContradictions(pgPool, p as Parameters<typeof toolKgContradictions>[1]);\n }\n break;\n }\n\n case \"memory_tunnels\": {\n const { toolMemoryTunnels } = await import(\"../../mcp/tools.js\");\n return toolMemoryTunnels(registryDb, storageBackend, p as Parameters<typeof toolMemoryTunnels>[2]);\n }\n\n case \"memory_feedback\": {\n // MR2: feedback weight loop — uses federation.db (SQLite) directly\n const federationDb = openFederation();\n try {\n return await toolMemoryFeedback(federationDb, p as Parameters<typeof toolMemoryFeedback>[1]);\n } finally {\n federationDb.close();\n }\n }\n\n case \"memory_kg_search\": {\n // MR1: graph-completion retrieval — federation.db (SQLite) + Postgres pool\n const federationDb = openFederation();\n const pgPool = (storageBackend as PostgresBackendWithPool).getPool?.() ?? null;\n if (!pgPool) {\n federationDb.close();\n throw new Error(\"memory_kg_search requires a Postgres storage backend for KG triple expansion\");\n }\n try {\n return await toolMemoryKgSearch(federationDb, pgPool, p as Parameters<typeof toolMemoryKgSearch>[2]);\n } finally {\n federationDb.close();\n }\n }\n\n default:\n throw new Error(`Unknown method: ${method}`);\n }\n}\n","/**\n * IPC request handler — processes all inbound IPC methods and sends responses.\n */\n\nimport type { Socket } from \"node:net\";\nimport type { IpcRequest, IpcResponse, PostgresBackendWithPool } from \"./types.js\";\nimport type { NotificationMode } from \"../../notifications/types.js\";\nimport {\n patchNotificationConfig,\n} from \"../../notifications/config.js\";\nimport { routeNotification } from \"../../notifications/router.js\";\nimport {\n ensureObservationTables,\n storeObservation,\n queryObservations,\n queryRecentObservations,\n storeSessionSummary,\n recordSkillInvocation,\n querySkillTelemetry,\n} from \"../../observations/store.js\";\nimport {\n registryDb,\n storageBackend,\n daemonConfig,\n startTime,\n indexInProgress,\n lastIndexTime,\n embedInProgress,\n lastEmbedTime,\n vaultIndexInProgress,\n lastVaultIndexTime,\n notificationConfig,\n setNotificationConfig,\n} from \"./state.js\";\nimport { runIndex, runVaultIndex } from \"./scheduler.js\";\nimport { dispatchTool } from \"./dispatcher.js\";\nimport { enqueue, getStats as getQueueStats } from \"../../daemon/work-queue.js\";\nimport { notifyNewWork } from \"../../daemon/work-queue-worker.js\";\n\n// ---------------------------------------------------------------------------\n// Helper\n// ---------------------------------------------------------------------------\n\nexport function sendResponse(socket: Socket, response: IpcResponse): void {\n try {\n socket.write(JSON.stringify(response) + \"\\n\");\n } catch {\n // Socket may already be closed\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\n/**\n * Handle a single IPC request.\n */\nexport async function handleRequest(\n request: IpcRequest,\n socket: Socket\n): Promise<void> {\n const { id, method, params } = request;\n\n // Special: status\n if (method === \"status\") {\n const dbStats = await (async () => {\n try {\n if (!storageBackend) return null;\n const fedStats = await storageBackend.getStats();\n const projects = (\n registryDb\n .prepare(\"SELECT COUNT(*) AS n FROM projects\")\n .get() as { n: number }\n ).n;\n return { files: fedStats.files, chunks: fedStats.chunks, projects };\n } catch {\n return null;\n }\n })();\n\n sendResponse(socket, {\n id,\n ok: true,\n result: {\n uptime: Math.floor((Date.now() - startTime) / 1000),\n indexInProgress,\n lastIndexTime: lastIndexTime ? new Date(lastIndexTime).toISOString() : null,\n indexIntervalSecs: daemonConfig.indexIntervalSecs,\n embedInProgress,\n lastEmbedTime: lastEmbedTime ? new Date(lastEmbedTime).toISOString() : null,\n embedIntervalSecs: daemonConfig.embedIntervalSecs,\n socketPath: daemonConfig.socketPath,\n storageBackend: storageBackend?.backendType ?? \"initializing\",\n db: dbStats,\n vaultIndexInProgress,\n lastVaultIndexTime: lastVaultIndexTime ? new Date(lastVaultIndexTime).toISOString() : null,\n vaultPath: daemonConfig.vaultPath ?? null,\n workQueue: getQueueStats(),\n },\n });\n socket.end();\n return;\n }\n\n // Special: index_now — trigger immediate index (non-blocking response)\n if (method === \"index_now\") {\n runIndex().catch((e) => {\n process.stderr.write(`[pai-daemon] index_now error: ${e}\\n`);\n });\n sendResponse(socket, { id, ok: true, result: { triggered: true } });\n socket.end();\n return;\n }\n\n // Special: vault_index_now — trigger immediate vault index (non-blocking response)\n if (method === \"vault_index_now\") {\n runVaultIndex().catch((e) => {\n process.stderr.write(`[pai-daemon] vault_index_now error: ${e}\\n`);\n });\n sendResponse(socket, { id, ok: true, result: { triggered: true } });\n socket.end();\n return;\n }\n\n // Special: notification_get_config\n if (method === \"notification_get_config\") {\n sendResponse(socket, {\n id,\n ok: true,\n result: {\n config: notificationConfig,\n activeChannels: Object.entries(notificationConfig.channels)\n .filter(([ch, cfg]) => ch !== \"voice\" && (cfg as { enabled: boolean }).enabled)\n .map(([ch]) => ch),\n },\n });\n socket.end();\n return;\n }\n\n // Special: notification_set_config\n if (method === \"notification_set_config\") {\n try {\n const p = params as {\n mode?: NotificationMode;\n channels?: Record<string, unknown>;\n routing?: Record<string, unknown>;\n };\n const updated = patchNotificationConfig({\n mode: p.mode,\n channels: p.channels as Parameters<typeof patchNotificationConfig>[0][\"channels\"],\n routing: p.routing as Parameters<typeof patchNotificationConfig>[0][\"routing\"],\n });\n setNotificationConfig(updated);\n sendResponse(socket, { id, ok: true, result: { config: updated } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // Special: notification_send\n if (method === \"notification_send\") {\n const p = params as {\n event?: string;\n message?: string;\n title?: string;\n };\n\n if (!p.message) {\n sendResponse(socket, { id, ok: false, error: \"notification_send: message is required\" });\n socket.end();\n return;\n }\n\n const event = (p.event as Parameters<typeof routeNotification>[0][\"event\"]) ?? \"info\";\n\n routeNotification(\n { event, message: p.message, title: p.title },\n notificationConfig\n ).then((result) => {\n sendResponse(socket, { id, ok: true, result });\n socket.end();\n }).catch((e) => {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n socket.end();\n });\n return;\n }\n\n // ---- Observation methods (Postgres only) --------------------------------\n\n if (method === \"observation_store\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n session_id: string;\n type: string;\n title: string;\n narrative?: string;\n tool_name: string;\n tool_input_summary?: string;\n files_read?: string[];\n files_modified?: string[];\n concepts?: string[];\n content_hash?: string;\n cwd?: string;\n };\n\n let project_id: number | null = null;\n let project_slug: string | null = null;\n if (p.cwd) {\n const row = registryDb.prepare(\n \"SELECT id, slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1\"\n ).get(p.cwd) as { id: number; slug: string } | undefined;\n if (row) {\n project_id = row.id;\n project_slug = row.slug;\n }\n }\n\n await ensureObservationTables(pool);\n const insertedId = await storeObservation(pool, {\n session_id: p.session_id,\n project_id,\n project_slug,\n type: p.type as \"decision\" | \"bugfix\" | \"feature\" | \"refactor\" | \"discovery\" | \"change\",\n title: p.title,\n narrative: p.narrative ?? null,\n tool_name: p.tool_name,\n tool_input_summary: p.tool_input_summary ?? null,\n files_read: p.files_read ?? [],\n files_modified: p.files_modified ?? [],\n concepts: p.concepts ?? [],\n });\n\n sendResponse(socket, { id, ok: true, result: { ok: true, id: insertedId } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // ---- Skill telemetry (Postgres only) ------------------------------------\n\n if (method === \"skill_telemetry_record\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Skill telemetry requires Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n skill_name: string;\n source?: string;\n scope?: string;\n cwd?: string;\n };\n\n let project_slug: string | null = null;\n if (p.cwd) {\n const row = registryDb.prepare(\n \"SELECT slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1\"\n ).get(p.cwd) as { slug: string } | undefined;\n if (row) project_slug = row.slug;\n }\n\n await recordSkillInvocation(pool, {\n skill_name: p.skill_name,\n source: p.source,\n scope: p.scope,\n project_slug,\n });\n\n sendResponse(socket, { id, ok: true, result: { ok: true } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"skill_telemetry_query\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Skill telemetry requires Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as { scope?: string; status?: string; limit?: number };\n const rows = await querySkillTelemetry(pool, {\n scope: p.scope,\n status: p.status,\n limit: p.limit,\n });\n sendResponse(socket, { id, ok: true, result: rows });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"observation_query\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n project_id?: number;\n session_id?: string;\n type?: string;\n limit?: number;\n offset?: number;\n };\n\n const rows = await queryObservations(pool, {\n projectId: p.project_id,\n sessionId: p.session_id,\n type: p.type,\n limit: p.limit,\n offset: p.offset,\n });\n sendResponse(socket, { id, ok: true, result: rows });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"observation_recent\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as { project_id?: number; cwd?: string; limit?: number };\n const limit = p.limit ?? 20;\n\n let resolvedProjectId = p.project_id;\n let resolvedProjectSlug: string | undefined;\n if (resolvedProjectId === undefined && p.cwd) {\n const row = registryDb.prepare(\n \"SELECT id, slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1\"\n ).get(p.cwd) as { id: number; slug: string } | undefined;\n if (row) {\n resolvedProjectId = row.id;\n resolvedProjectSlug = row.slug;\n }\n }\n\n let rows;\n if (resolvedProjectId !== undefined) {\n rows = await queryRecentObservations(pool, resolvedProjectId, limit);\n } else {\n rows = await queryObservations(pool, { limit });\n }\n sendResponse(socket, { id, ok: true, result: { rows, project_slug: resolvedProjectSlug } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // observation_list — alias for observation_query with project slug resolution\n if (method === \"observation_list\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n project_slug?: string;\n session_id?: string;\n type?: string;\n limit?: number;\n offset?: number;\n };\n\n let projectId: number | undefined;\n if (p.project_slug) {\n const row = registryDb.prepare(\n \"SELECT id FROM projects WHERE slug = ?\"\n ).get(p.project_slug) as { id: number } | undefined;\n projectId = row?.id;\n }\n\n const rows = await queryObservations(pool, {\n projectId,\n sessionId: p.session_id,\n type: p.type,\n limit: p.limit ?? 20,\n offset: p.offset ?? 0,\n });\n sendResponse(socket, { id, ok: true, result: rows });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // observation_stats — aggregate statistics\n if (method === \"observation_stats\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Observations require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n await ensureObservationTables(pool);\n const [totalRes, byTypeRes, byProjectRes, recentRes] = await Promise.all([\n pool.query<{ count: string }>(\"SELECT COUNT(*) as count FROM pai_observations\"),\n pool.query<{ type: string; count: string }>(\n \"SELECT type, COUNT(*) as count FROM pai_observations GROUP BY type ORDER BY count DESC\"\n ),\n pool.query<{ project_slug: string | null; count: string }>(\n \"SELECT project_slug, COUNT(*) as count FROM pai_observations GROUP BY project_slug ORDER BY count DESC LIMIT 15\"\n ),\n pool.query<{ created_at: string }>(\n \"SELECT created_at FROM pai_observations ORDER BY created_at DESC LIMIT 1\"\n ),\n ]);\n\n sendResponse(socket, {\n id,\n ok: true,\n result: {\n total: parseInt(totalRes.rows[0]?.count ?? \"0\", 10),\n by_type: byTypeRes.rows.map(r => ({ type: r.type, count: parseInt(r.count, 10) })),\n by_project: byProjectRes.rows.map(r => ({ project_slug: r.project_slug, count: parseInt(r.count, 10) })),\n most_recent: recentRes.rows[0]?.created_at ?? null,\n },\n });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n if (method === \"session_summary_store\") {\n const pool = (storageBackend as PostgresBackendWithPool).getPool?.();\n if (!pool) {\n sendResponse(socket, { id, ok: false, error: \"Session summaries require Postgres backend\" });\n socket.end();\n return;\n }\n try {\n const p = params as {\n session_id: string;\n project_id?: number;\n project_slug?: string;\n cwd?: string;\n request?: string;\n investigated?: string;\n learned?: string;\n completed?: string;\n next_steps?: string;\n observation_count?: number;\n };\n\n let resolvedProjectId = p.project_id ?? null;\n let resolvedProjectSlug = p.project_slug ?? null;\n if (resolvedProjectId === null && p.cwd) {\n const row = registryDb.prepare(\n \"SELECT id, slug FROM projects WHERE status = 'active' AND ? LIKE root_path || '%' ORDER BY length(root_path) DESC LIMIT 1\"\n ).get(p.cwd) as { id: number; slug: string } | undefined;\n if (row) {\n resolvedProjectId = row.id;\n resolvedProjectSlug = row.slug;\n }\n }\n\n await storeSessionSummary(pool, {\n session_id: p.session_id,\n project_id: resolvedProjectId,\n project_slug: resolvedProjectSlug,\n request: p.request ?? null,\n investigated: p.investigated ?? null,\n learned: p.learned ?? null,\n completed: p.completed ?? null,\n next_steps: p.next_steps ?? null,\n observation_count: p.observation_count ?? 0,\n });\n\n sendResponse(socket, { id, ok: true, result: { ok: true } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // work_queue_enqueue — hooks push work items here instead of doing heavy work inline\n if (method === \"work_queue_enqueue\") {\n try {\n const p = params as {\n type?: string;\n priority?: number;\n payload?: Record<string, unknown>;\n maxAttempts?: number;\n };\n\n if (!p.type) {\n sendResponse(socket, { id, ok: false, error: \"work_queue_enqueue: type is required\" });\n socket.end();\n return;\n }\n\n const item = enqueue({\n type: p.type as import(\"../../daemon/work-queue.js\").WorkItemType,\n priority: p.priority,\n payload: p.payload ?? {},\n maxAttempts: p.maxAttempts,\n });\n\n notifyNewWork();\n\n sendResponse(socket, { id, ok: true, result: { id: item.id, status: item.status } });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n return;\n }\n\n // work_queue_stats — return current queue statistics\n if (method === \"work_queue_stats\") {\n sendResponse(socket, { id, ok: true, result: getQueueStats() });\n socket.end();\n return;\n }\n\n // All other methods: PAI tool dispatch\n try {\n const result = await dispatchTool(method, params);\n sendResponse(socket, { id, ok: true, result });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id, ok: false, error: msg });\n }\n socket.end();\n}\n","/**\n * IPC server and daemon entry point.\n * Owns: isSocketLive, startIpcServer, serve (exported).\n */\n\nimport { existsSync, unlinkSync } from \"node:fs\";\nimport { createServer, connect, Socket, Server } from \"node:net\";\nimport { setPriority } from \"node:os\";\nimport { openRegistry } from \"../../registry/db.js\";\nimport { createStorageBackend } from \"../../storage/factory.js\";\nimport { configureEmbeddingModel } from \"../../memory/embeddings.js\";\nimport { loadNotificationConfig } from \"../../notifications/config.js\";\nimport type { PaiDaemonConfig } from \"../config.js\";\nimport type { IpcRequest } from \"./types.js\";\nimport {\n setRegistryDb,\n setStorageBackend,\n setDaemonConfig,\n setStartTime,\n setNotificationConfig,\n setShutdownRequested,\n indexInProgress,\n embedInProgress,\n indexSchedulerTimer,\n embedSchedulerTimer,\n storageBackend,\n} from \"./state.js\";\nimport { startIndexScheduler, startEmbedScheduler, startRegistryScanScheduler } from \"./scheduler.js\";\nimport { handleRequest, sendResponse } from \"./handler.js\";\nimport { loadQueue } from \"../../daemon/work-queue.js\";\nimport { startWorker, stopWorker } from \"../../daemon/work-queue-worker.js\";\n\n// ---------------------------------------------------------------------------\n// IPC helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether an existing socket file is actually being served by a live process.\n */\nfunction isSocketLive(path: string): Promise<boolean> {\n return new Promise((resolve) => {\n const client = connect(path);\n const timer = setTimeout(() => { client.destroy(); resolve(false); }, 500);\n client.on(\"connect\", () => { clearTimeout(timer); client.end(); resolve(true); });\n client.on(\"error\", () => { clearTimeout(timer); resolve(false); });\n });\n}\n\n/**\n * Start the Unix Domain Socket IPC server.\n */\nasync function startIpcServer(socketPath: string): Promise<Server> {\n if (existsSync(socketPath)) {\n const live = await isSocketLive(socketPath);\n if (live) {\n throw new Error(\"Another daemon is already running — socket is live. Aborting startup.\");\n }\n try {\n unlinkSync(socketPath);\n process.stderr.write(\"[pai-daemon] Removed stale socket file.\\n\");\n } catch {\n // If we can't remove it, bind will fail with a clear error\n }\n }\n\n const server = createServer((socket: Socket) => {\n let buffer = \"\";\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString();\n let nl: number;\n while ((nl = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, nl);\n buffer = buffer.slice(nl + 1);\n\n if (line.trim() === \"\") continue;\n\n let request: IpcRequest;\n try {\n request = JSON.parse(line) as IpcRequest;\n } catch {\n sendResponse(socket, { id: \"?\", ok: false, error: \"Invalid JSON\" });\n socket.destroy();\n return;\n }\n\n handleRequest(request, socket).catch((e: unknown) => {\n const msg = e instanceof Error ? e.message : String(e);\n sendResponse(socket, { id: request.id, ok: false, error: msg });\n socket.destroy();\n });\n }\n });\n\n socket.on(\"error\", () => {\n // Client disconnected — nothing to do\n });\n });\n\n server.on(\"error\", (e) => {\n process.stderr.write(`[pai-daemon] IPC server error: ${e}\\n`);\n });\n\n server.listen(socketPath, () => {\n process.stderr.write(\n `[pai-daemon] IPC server listening on ${socketPath}\\n`\n );\n });\n\n return server;\n}\n\n// ---------------------------------------------------------------------------\n// Main daemon entry point\n// ---------------------------------------------------------------------------\n\nexport async function serve(config: PaiDaemonConfig): Promise<void> {\n setDaemonConfig(config);\n setStartTime(Date.now());\n\n setNotificationConfig(loadNotificationConfig());\n\n process.stderr.write(\"[pai-daemon] Starting daemon...\\n\");\n process.stderr.write(`[pai-daemon] Socket: ${config.socketPath}\\n`);\n process.stderr.write(`[pai-daemon] Storage backend: ${config.storageBackend}\\n`);\n const { notificationConfig } = await import(\"./state.js\");\n process.stderr.write(\n `[pai-daemon] Notification mode: ${notificationConfig.mode}\\n`\n );\n\n // Lower scheduling priority so the daemon yields CPU to interactive sessions\n try { setPriority(process.pid, 10); } catch { /* non-fatal */ }\n\n configureEmbeddingModel(config.embeddingModel);\n\n try {\n setRegistryDb(openRegistry());\n process.stderr.write(\"[pai-daemon] Registry database opened.\\n\");\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(`[pai-daemon] Fatal: Could not open registry: ${msg}\\n`);\n process.exit(1);\n }\n\n // Start the IPC server immediately so `pai daemon status` answers even while\n // we are still waiting for the federation backend (e.g. Postgres coming up\n // after a reboot). The status handler tolerates a not-yet-ready backend.\n const server = await startIpcServer(config.socketPath);\n\n // Registry scan scheduler only needs the registry DB — safe to start now.\n startRegistryScanScheduler();\n\n // Connect the federation backend in the background. When storageBackend is\n // \"postgres\" this retries forever (never silently falls back to SQLite), so\n // the boot race with Docker/Postgres resolves itself once Postgres is up.\n // Only then do we start the federation-dependent schedulers and work queue —\n // this also keeps the first index run from firing against an unready backend.\n void createStorageBackend(config, { waitForPostgres: true })\n .then((backend) => {\n setStorageBackend(backend);\n process.stderr.write(\n `[pai-daemon] Federation backend: ${backend.backendType}\\n`\n );\n\n startIndexScheduler();\n\n if (backend.backendType === \"postgres\") {\n startEmbedScheduler();\n } else {\n process.stderr.write(\n \"[pai-daemon] Embed scheduler: disabled (SQLite backend)\\n\"\n );\n }\n\n // Work queue — load persisted items and start worker loop\n loadQueue();\n startWorker();\n })\n .catch((e) => {\n const msg = e instanceof Error ? e.message : String(e);\n process.stderr.write(\n `[pai-daemon] Fatal: Could not open federation storage: ${msg}\\n`\n );\n process.exit(1);\n });\n\n const shutdown = async (signal: string): Promise<void> => {\n process.stderr.write(`\\n[pai-daemon] ${signal} received. Stopping.\\n`);\n\n setShutdownRequested(true);\n\n if (indexSchedulerTimer) clearInterval(indexSchedulerTimer);\n if (embedSchedulerTimer) clearInterval(embedSchedulerTimer);\n\n stopWorker();\n\n server.close();\n\n const SHUTDOWN_TIMEOUT_MS = 10_000;\n const POLL_INTERVAL_MS = 100;\n const deadline = Date.now() + SHUTDOWN_TIMEOUT_MS;\n\n if (indexInProgress || embedInProgress) {\n process.stderr.write(\n `[pai-daemon] Waiting for in-progress operations to finish ` +\n `(index=${indexInProgress}, embed=${embedInProgress})...\\n`\n );\n\n while ((indexInProgress || embedInProgress) && Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n if (indexInProgress || embedInProgress) {\n process.stderr.write(\"[pai-daemon] Shutdown timeout reached — forcing exit.\\n\");\n } else {\n process.stderr.write(\"[pai-daemon] In-progress operations finished.\\n\");\n }\n }\n\n try {\n await storageBackend.close();\n } catch {\n // ignore\n }\n\n try {\n unlinkSync(config.socketPath);\n } catch {\n // ignore\n }\n\n process.exit(0);\n };\n\n process.on(\"SIGINT\", () => { shutdown(\"SIGINT\").catch(() => process.exit(0)); });\n process.on(\"SIGTERM\", () => { shutdown(\"SIGTERM\").catch(() => process.exit(0)); });\n\n // Keep process alive\n await new Promise(() => {});\n}\n","/**\n * Shim — re-exports serve() from daemon/ directory so existing importers\n * continue to work without modification. See daemon/index.ts.\n */\nexport { serve } from \"./daemon/index.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAS,UACP,QACA,QACG;CACH,MAAM,SAAS,EAAE,GAAG,QAAQ;AAC5B,MAAK,MAAM,OAAO,OAAO,KAAK,OAAO,EAAE;EACrC,MAAM,SAAS,OAAO;AACtB,MAAI,WAAW,UAAa,WAAW,KAAM;EAC7C,MAAM,SAAU,OAAmC;AACnD,MACE,OAAO,WAAW,YAClB,CAAC,MAAM,QAAQ,OAAO,IACtB,OAAO,WAAW,YAClB,WAAW,QACX,CAAC,MAAM,QAAQ,OAAO,CAEtB,CAAC,OAAmC,OAAO,UACzC,QACA,OACD;MAED,CAAC,OAAmC,OAAO;;AAG/C,QAAO;;;;;;AAWT,SAAgB,yBAA6C;AAC3D,KAAI,CAAC,WAAW,YAAY,CAC1B,QAAO,EAAE,GAAG,6BAA6B;CAG3C,IAAI;AACJ,KAAI;AACF,QAAM,aAAa,aAAa,QAAQ;SAClC;AACN,SAAO,EAAE,GAAG,6BAA6B;;CAG3C,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;SAClB;AACN,SAAO,EAAE,GAAG,6BAA6B;;CAG3C,MAAM,SAAS,OAAO;AACtB,KAAI,CAAC,UAAU,OAAO,WAAW,SAC/B,QAAO,EAAE,GAAG,6BAA6B;AAG3C,QAAO,UACL,6BACA,OACD;;;;;;AAWH,SAAgB,uBAAuB,QAAkC;AAEvE,KAAI,CAAC,WAAW,WAAW,CACzB,WAAU,YAAY,EAAE,WAAW,MAAM,CAAC;CAI5C,IAAI,OAAgC,EAAE;AACtC,KAAI,WAAW,YAAY,CACzB,KAAI;AACF,SAAO,KAAK,MAAM,aAAa,aAAa,QAAQ,CAAC;SAI/C;AAMV,MAAK,mBAAmB;AAExB,eAAc,aAAa,KAAK,UAAU,MAAM,MAAM,EAAE,GAAG,MAAM,QAAQ;;;;;;AAW3E,SAAgB,wBAAwB,OAIjB;CACrB,MAAM,UAAU,wBAAwB;AAExC,KAAI,MAAM,SAAS,OACjB,SAAQ,OAAO,MAAM;AAGvB,KAAI,MAAM,SACR,SAAQ,WAAW,UACjB,QAAQ,UACR,MAAM,SACP;AAGH,KAAI,MAAM,QACR,SAAQ,UAAU,UAChB,QAAQ,SACR,MAAM,QACP;AAGH,wBAAuB,QAAQ;AAC/B,QAAO;;;;;;;;;;;AC1IT,MAAM,8BAA8B,OAAU;;;;;AAM9C,eAAsB,WAA0B;AAC9C,KAAI,iBAAiB;AACnB,UAAQ,OAAO,MAAM,sDAAsD;AAC3E;;AAGF,KAAI,iBAAiB;AACnB,UAAQ,OAAO,MAAM,yDAAyD;AAC9E;;AAGF,oBAAmB,KAAK;CACxB,MAAM,KAAK,KAAK,KAAK;AAErB,KAAI;AACF,UAAQ,OAAO,MAAM,iDAAiD;AAEtE,MAAI,eAAe,gBAAgB,UAAU;GAC3C,MAAM,EAAE,kBAAkB,MAAM,OAAO;AACvC,OAAI,0BAA0B,eAAe;IAE3C,MAAM,EAAE,UAAU,WAAW,MAAM,SADvB,eAAuC,UAAU,EACb,WAAW;IAC3D,MAAM,UAAU,KAAK,KAAK,GAAG;AAC7B,qBAAiB,KAAK,KAAK,CAAC;AAC5B,YAAQ,OAAO,MACb,gCAAgC,SAAS,aACpC,OAAO,eAAe,UAAU,OAAO,cAAc,WACpD,QAAQ,OACf;;SAEE;GACL,MAAM,EAAE,wBAAwB,MAAM,OAAO;GAC7C,MAAM,EAAE,UAAU,WAAW,MAAM,oBAAoB,gBAAgB,WAAW;GAClF,MAAM,UAAU,KAAK,KAAK,GAAG;AAC7B,oBAAiB,KAAK,KAAK,CAAC;AAC5B,WAAQ,OAAO,MACb,2CAA2C,SAAS,aAC/C,OAAO,eAAe,UAAU,OAAO,cAAc,WACpD,QAAQ,OACf;;UAEI,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,OAAO,MAAM,6BAA6B,IAAI,IAAI;WAClD;AACR,qBAAmB,MAAM;;;;;;;AAQ7B,eAAsB,gBAA+B;AACnD,KAAI,CAAC,aAAa,UAAW;AAE7B,KAAI,sBAAsB;AACxB,UAAQ,OAAO,MAAM,4DAA4D;AACjF;;AAGF,KAAI,mBAAmB,iBAAiB;AACtC,UAAQ,OAAO,MAAM,iEAAiE;AACtF;;CAIF,MAAM,EAAE,uBAAuB,MAAM,OAAO;AAC5C,KAAI,qBAAqB,KAAK,KAAK,KAAK,GAAG,qBAAqB,4BAC9D;CAGF,IAAI,iBAAiB,aAAa;AAClC,KAAI,CAAC,gBAAgB;EACnB,MAAM,MAAM,WACT,QAAQ,8CAA8C,CACtD,IAAI,aAAa,UAAU;AAC9B,mBAAiB,KAAK,MAAM;AAC5B,MAAI,CAAC,IACH,SAAQ,OAAO,MAAM,iFAAiF;;AAI1G,yBAAwB,KAAK;CAC7B,MAAM,KAAK,KAAK,KAAK;AAErB,SAAQ,OAAO,MAAM,6CAA6C;AAElE,KAAI;EACF,MAAM,EAAE,eAAe,MAAM,OAAO;EACpC,MAAM,IAAI,MAAM,WAAW,gBAAgB,gBAAgB,aAAa,UAAW;EACnF,MAAM,UAAU,KAAK,KAAK,GAAG;AAC7B,wBAAsB,KAAK,KAAK,CAAC;AACjC,UAAQ,OAAO,MACb,sCAAsC,EAAE,aAAa,UAClD,EAAE,eAAe,UAAU,EAAE,eAAe,SAC5C,EAAE,aAAa,YAAY,QAAQ,OACvC;UACM,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,OAAO,MAAM,mCAAmC,IAAI,IAAI;WACxD;AACR,0BAAwB,MAAM;;;;;;AAOlC,SAAgB,sBAA4B;CAC1C,MAAM,aAAa,aAAa,oBAAoB;AAEpD,SAAQ,OAAO,MACb,uCAAuC,aAAa,kBAAkB,KACvE;AAED,kBAAiB;AACf,YAAU,CACP,WAAW,eAAe,CAAC,CAC3B,OAAO,MAAM;AACZ,WAAQ,OAAO,MAAM,qCAAqC,EAAE,IAAI;IAChE;IACH,IAAM;CAET,MAAM,QAAQ,kBAAkB;AAC9B,YAAU,CACP,WAAW,eAAe,CAAC,CAC3B,OAAO,MAAM;AACZ,WAAQ,OAAO,MAAM,uCAAuC,EAAE,IAAI;IAClE;IACH,WAAW;AAEd,KAAI,MAAM,MAAO,OAAM,OAAO;AAC9B,wBAAuB,MAAM;;;;;AAU/B,eAAsB,WAA0B;AAC9C,KAAI,iBAAiB;AACnB,UAAQ,OAAO,MAAM,sDAAsD;AAC3E;;AAGF,KAAI,iBAAiB;AACnB,UAAQ,OAAO,MAAM,0DAA0D;AAC/E;;AAGF,KAAI,eAAe,gBAAgB,WACjC;AAGF,oBAAmB,KAAK;CACxB,MAAM,KAAK,KAAK,KAAK;AAErB,KAAI;AACF,UAAQ,OAAO,MAAM,kDAAkD;EAEvE,MAAM,+BAAe,IAAI,KAAqB;AAC9C,MAAI;GACF,MAAM,OAAO,WACV,QAAQ,wDAAwD,CAChE,KAAK;AACR,QAAK,MAAM,KAAK,KAAM,cAAa,IAAI,EAAE,IAAI,EAAE,KAAK;UAC9C;EAER,MAAM,EAAE,2BAA2B,MAAM,OAAO;EAChD,MAAM,QAAQ,MAAM,uBAAuB,sBAAsB,mBAAmB,aAAa;EAEjG,IAAI,kBAAkB;AACtB,MAAI,aAAa,UACf,KAAI;GACF,MAAM,EAAE,kBAAkB,MAAM,OAAO;GACvC,MAAM,EAAE,mBAAmB,MAAM,OAAO;GACxC,MAAM,eAAe,gBAAgB;GACrC,MAAM,qBAAqB,IAAI,cAAc,aAAa;GAE1D,MAAM,oBAAoB,IAAI,IAAI,aAAa;AAC/C,OAAI,CAAC,kBAAkB,IAAI,IAAI,CAC7B,mBAAkB,IAAI,KAAK,iBAAiB;AAG9C,qBAAkB,MAAM,uBACtB,0BACM,mBACN,kBACD;AAED,OAAI;AAAE,iBAAa,OAAO;WAAU;AAEpC,OAAI,kBAAkB,EACpB,SAAQ,OAAO,MACb,2CAA2C,gBAAgB,0BAC5D;WAEI,IAAI;GACX,MAAM,OAAO,cAAc,QAAQ,GAAG,UAAU,OAAO,GAAG;AAC1D,WAAQ,OAAO,MAAM,mCAAmC,KAAK,IAAI;;EAIrE,MAAM,UAAU,KAAK,KAAK,GAAG;AAC7B,mBAAiB,KAAK,KAAK,CAAC;AAC5B,UAAQ,OAAO,MACb,qCAAqC,MAAM,qBAAqB,gBAAgB,0BAA0B,QAAQ,OACnH;UACM,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,OAAO,MAAM,6BAA6B,IAAI,IAAI;WAClD;AACR,qBAAmB,MAAM;;;;;;AAO7B,SAAgB,sBAA4B;CAC1C,MAAM,aAAa,aAAa,oBAAoB;AAEpD,SAAQ,OAAO,MACb,uCAAuC,aAAa,kBAAkB,KACvE;AAED,kBAAiB;AACf,YAAU,CAAC,OAAO,MAAM;AACtB,WAAQ,OAAO,MAAM,qCAAqC,EAAE,IAAI;IAChE;IACD,IAAO;CAEV,MAAM,QAAQ,kBAAkB;AAC9B,YAAU,CAAC,OAAO,MAAM;AACtB,WAAQ,OAAO,MAAM,uCAAuC,EAAE,IAAI;IAClE;IACD,WAAW;AAEd,KAAI,MAAM,MAAO,OAAM,OAAO;AAC9B,wBAAuB,MAAM;;AAO/B,MAAM,4BAA4B,OAAU;AAC5C,MAAM,iCAAiC;;;;;;;AAQvC,SAAgB,6BAAmC;AACjD,SAAQ,OAAO,MACb,qEACD;AAED,kBAAiB;AACf,SAAO,qDACJ,MAAM,EAAE,0BAA0B,qBAAqB,CAAC,CACxD,OAAO,MAAM;AACZ,WAAQ,OAAO,MAAM,6CAA6C,EAAE,IAAI;IACxE;IACH,+BAA+B;CAElC,MAAM,QAAQ,kBAAkB;AAC9B,SAAO,qDACJ,MAAM,EAAE,0BAA0B,qBAAqB,CAAC,CACxD,OAAO,MAAM;AACZ,WAAQ,OAAO,MAAM,+CAA+C,EAAE,IAAI;IAC1E;IACH,0BAA0B;AAE7B,KAAI,MAAM,MAAO,OAAM,OAAO;;;;;ACjThC,IAAa,eAAb,MAA0D;CACxD,AAAS,YAAY;CAErB,MAAM,KACJ,SACA,QACkB;EAClB,MAAM,MAAM,OAAO,SAAS;AAC5B,MAAI,CAAC,IAAI,WAAW,CAAC,IAAI,IAAK,QAAO;AAErC,MAAI;GACF,MAAM,UAAkC,EACtC,gBAAgB,6BACjB;AAED,OAAI,QAAQ,MACV,SAAQ,WAAW,QAAQ;AAG7B,OAAI,IAAI,YAAY,IAAI,aAAa,UACnC,SAAQ,cAAc,IAAI;AAS5B,WANiB,MAAM,MAAM,IAAI,KAAK;IACpC,QAAQ;IACR;IACA,MAAM,QAAQ;IACf,CAAC,EAEc;UACV;AACN,UAAO;;;;;;;;;;;;;;;;;;ACvBb,MAAM,gBAAgB;AACtB,MAAM,oBAAoB;;;;;AAM1B,SAAS,WACP,QACA,QACkB;AAClB,QAAO,IAAI,SAAS,YAAY;EAC9B,IAAI,OAAO;EACX,IAAI,SAAS;EACb,IAAI,QAA8C;EAElD,SAAS,OAAO,IAAmB;AACjC,OAAI,KAAM;AACV,UAAO;AACP,OAAI,OAAO;AAAE,iBAAa,MAAM;AAAE,YAAQ;;AAC1C,OAAI;AAAE,YAAQ,SAAS;WAAU;AACjC,WAAQ,GAAG;;EAGb,MAAM,SAAS,QAAQ,qBAAqB;GAC1C,MAAM,UAAU;IACd,SAAS;IACT,IAAI,YAAY;IAChB;IACA;IACD;AACD,UAAO,MAAM,KAAK,UAAU,QAAQ,GAAG,KAAK;IAC5C;AAEF,SAAO,GAAG,SAAS,UAAkB;AACnC,aAAU,MAAM,UAAU;GAC1B,MAAM,KAAK,OAAO,QAAQ,KAAK;AAC/B,OAAI,OAAO,GAAI;AACf,OAAI;AAEF,WAAO,CADM,KAAK,MAAM,OAAO,MAAM,GAAG,GAAG,CAAC,CAC/B,MAAM;WACb;AACN,WAAO,MAAM;;IAEf;AAEF,SAAO,GAAG,eAAe,OAAO,MAAM,CAAC;AACvC,SAAO,GAAG,aAAa,OAAO,MAAM,CAAC;AAErC,UAAQ,iBAAiB,OAAO,MAAM,EAAE,kBAAkB;GAC1D;;AAGJ,IAAa,mBAAb,MAA8D;CAC5D,AAAS,YAAY;CAErB,MAAM,KACJ,SACA,QACkB;EAClB,MAAM,MAAM,OAAO,SAAS;AAC5B,MAAI,CAAC,IAAI,QAAS,QAAO;EAEzB,MAAM,cAAc,OAAO,SAAS,WAAW,OAAO,SAAS,MAAM;EAErE,MAAM,SAAkC,EACtC,SAAS,QAAQ,SAClB;AAED,MAAI,IAAI,UACN,QAAO,YAAY,IAAI;AAGzB,MAAI,eAAe,OAAO,SAAS,QAEjC,QAAO,QADW,OAAO,SAAS,MAAM,aAAa;AAIvD,SAAO,WAAW,iBAAiB,OAAO;;;;;;;;;;;;ACpF9C,IAAa,gBAAb,MAA2D;CACzD,AAAS,YAAY;CAErB,MAAM,KACJ,SACA,QACkB;AAElB,MAAI,CADQ,OAAO,SAAS,MACnB,QAAS,QAAO;AAEzB,MAAI;GAGF,MAAM,aAFQ,QAAQ,SAAS,OAEP,QAAQ,MAAM,MAAM;GAG5C,MAAM,SAAS,yBAFK,QAAQ,QAAQ,QAAQ,MAAM,MAAM,CAEJ,gBAAgB,UAAU;AAE9E,UAAO,IAAI,SAAS,YAAY;IAC9B,MAAM,QAAQ,MAAM,aAAa,CAAC,MAAM,OAAO,EAAE;KAC/C,UAAU;KACV,OAAO;KACR,CAAC;AACF,UAAM,OAAO;AAIb,UAAM,GAAG,eAAe,QAAQ,MAAM,CAAC;AAGvC,qBAAiB,QAAQ,KAAK,EAAE,IAAI;KACpC;UACI;AACN,UAAO;;;;;;;AClCb,IAAa,cAAb,MAAyD;CACvD,AAAS,YAAY;CAErB,MAAM,KACJ,SACA,SACkB;EAClB,MAAM,SAAS,eAAe,QAAQ,MAAM;EAC5C,MAAM,QAAQ,QAAQ,QAAQ,IAAI,QAAQ,MAAM,KAAK;AACrD,UAAQ,OAAO,MAAM,GAAG,SAAS,MAAM,GAAG,QAAQ,QAAQ,IAAI;AAC9D,SAAO;;;;;;ACGX,MAAM,YAAqD;CACzD,MAAW,IAAI,cAAc;CAC7B,UAAW,IAAI,kBAAkB;CACjC,OAAW,IAAI,eAAe;CAC9B,OAAW,IAAI,kBAAkB;CACjC,KAAW,IAAI,aAAa;CAC7B;;;;;;;;;;;;;;AAmBD,SAAS,gBACP,QACA,OACa;CACb,MAAM,EAAE,MAAM,UAAU,YAAY;AAEpC,KAAI,SAAS,MAAO,QAAO,EAAE;CAG7B,MAAM,gBAA8D;EAClE,OAAW;EACX,UAAW;EACX,MAAW;EACX,OAAW;EACX,KAAW;EACZ;AAED,KAAI,SAAS,QAAQ;EACnB,MAAM,KAAK,cAAc;AACzB,MAAI,CAAC,GAAI,QAAO,EAAE;EAElB,MAAM,MAAM,SAAS;AACrB,MAAI,OAAO,CAAC,IAAI,QAAS,QAAO,CAAC,GAAG;AACpC,SAAO,CAAC,GAAG;;AAKb,SADmB,QAAQ,UAAU,EAAE,EACrB,QAAQ,OAAO;EAC/B,MAAM,MAAM,SAAS;AAGrB,MAAI,OAAO,QAAS,QAAO;AAC3B,SAAO,KAAK,YAAY;GACxB;;;;;;;;;;;AAgBJ,eAAsB,kBACpB,SACA,QACqB;CACrB,MAAM,WAAW,gBAAgB,QAAQ,QAAQ,MAAM;AAEvD,KAAI,SAAS,WAAW,EACtB,QAAO;EACL,mBAAmB,EAAE;EACrB,mBAAmB,EAAE;EACrB,gBAAgB,EAAE;EAClB,MAAM,OAAO;EACd;CAGH,MAAM,UAAU,MAAM,QAAQ,WAC5B,SAAS,IAAI,OAAO,OAAO;EAEzB,MAAM,KAAK,MADM,UAAU,IACD,KAAK,SAAS,OAAO;AAC/C,MAAI,CAAC,GACH,SAAQ,OAAO,MACb,wBAAwB,GAAG,oBAAoB,QAAQ,MAAM,IAC9D;AAEH,SAAO;GAAE;GAAI;GAAI;GACjB,CACH;CAED,MAAM,YAAyB,EAAE;CACjC,MAAM,SAAsB,EAAE;AAE9B,MAAK,MAAM,KAAK,QACd,KAAI,EAAE,WAAW,YACf,KAAI,EAAE,MAAM,GACV,WAAU,KAAK,EAAE,MAAM,GAAG;KAE1B,QAAO,KAAK,EAAE,MAAM,GAAG;KAIzB,QAAO,KAAK,SAAS,QAAQ,QAAQ,EAAE,EAAE;AAI7C,QAAO;EACL,mBAAmB;EACnB,mBAAmB;EACnB,gBAAgB;EAChB,MAAM,OAAO;EACd;;;;;;;;;;;;;;;ACjEH,IAAI,iBAAiB;;;;;AAMrB,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CnB,eAAsB,wBAAwB,MAA2B;AACvE,KAAI,eAAgB;AACpB,OAAM,KAAK,MAAM,WAAW;AAC5B,kBAAiB;;;;;;AAWnB,SAAS,mBAAmB,WAAmB,UAAkB,OAAuB;AACtF,QAAO,OAAO,YAAY,OAAS,WAAW,OAAS,MAAM,CAAC,MAAM,GAAG,GAAG;;;;;;AAW5E,eAAsB,iBACpB,MACA,KACwB;AACxB,OAAM,wBAAwB,KAAK;CAEnC,MAAM,OAAO,mBAAmB,IAAI,YAAY,IAAI,WAAW,IAAI,MAAM;CAGzE,MAAM,WAAW,MAAM,KAAK,MAC1B;;;;eAKA,CAAC,MAAM,IAAI,WAAW,CACvB;AAED,KAAI,SAAS,YAAY,SAAS,WAAW,EAE3C,QAAO;AAyBT,SAtBe,MAAM,KAAK,MACxB;;;;oBAKA;EACE,IAAI;EACJ,IAAI,cAAc;EAClB,IAAI,gBAAgB;EACpB,IAAI;EACJ,IAAI;EACJ,IAAI,aAAa;EACjB,IAAI;EACJ,IAAI,sBAAsB;EAC1B,KAAK,UAAU,IAAI,WAAW;EAC9B,KAAK,UAAU,IAAI,eAAe;EAClC,KAAK,UAAU,IAAI,SAAS;EAC5B;EACD,CACF,EAEa,KAAK,IAAI,MAAM;;;;;;AAW/B,eAAsB,kBACpB,MACA,OAAiC,EAAE,EACR;AAC3B,OAAM,wBAAwB,KAAK;CAEnC,MAAM,aAAuB,EAAE;CAC/B,MAAM,SAAoB,EAAE;CAC5B,IAAI,MAAM;AAEV,KAAI,KAAK,cAAc,QAAW;AAChC,aAAW,KAAK,iBAAiB,QAAQ;AACzC,SAAO,KAAK,KAAK,UAAU;;AAE7B,KAAI,KAAK,cAAc,QAAW;AAChC,aAAW,KAAK,iBAAiB,QAAQ;AACzC,SAAO,KAAK,KAAK,UAAU;;AAE7B,KAAI,KAAK,SAAS,QAAW;AAC3B,aAAW,KAAK,WAAW,QAAQ;AACnC,SAAO,KAAK,KAAK,KAAK;;CAGxB,MAAM,QAAQ,WAAW,SAAS,IAAI,SAAS,WAAW,KAAK,QAAQ,KAAK;CAC5E,MAAM,QAAQ,KAAK,SAAS;CAC5B,MAAM,SAAS,KAAK,UAAU;AAE9B,QAAO,KAAK,OAAO,OAAO;AAc1B,SAZe,MAAM,KAAK,MACxB;;;;;OAKG,MAAM;;cAEC,MAAM,WAAW,OAC3B,OACD,EAEa;;;;;AAMhB,eAAsB,wBACpB,MACA,WACA,OAC2B;AAC3B,OAAM,wBAAwB,KAAK;AAcnC,SAZe,MAAM,KAAK,MACxB;;;;;;;gBAQA,CAAC,WAAW,MAAM,CACnB,EAEa;;;;;;AAkChB,eAAsB,oBACpB,MACA,SACe;AACf,OAAM,wBAAwB,KAAK;AAEnC,OAAM,KAAK,MACT;;;;;;;;;;;;wDAaA;EACE,QAAQ;EACR,QAAQ,cAAc;EACtB,QAAQ,gBAAgB;EACxB,QAAQ,WAAW;EACnB,QAAQ,gBAAgB;EACxB,QAAQ,WAAW;EACnB,QAAQ,aAAa;EACrB,QAAQ,cAAc;EACtB,QAAQ,qBAAqB;EAC9B,CACF;;AAuCH,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;AAuBnC,IAAI,yBAAyB;AAE7B,eAAsB,0BAA0B,MAA2B;AACzE,KAAI,uBAAwB;AAC5B,OAAM,KAAK,MAAM,2BAA2B;AAC5C,0BAAyB;;;;;;;AAQ3B,eAAsB,sBACpB,MACA,OACe;AACf,OAAM,0BAA0B,KAAK;CAErC,MAAM,QAAQ,MAAM,SAAS;CAC7B,MAAM,SAAS,MAAM,UAAU;CAC/B,MAAM,OAAO,MAAM,gBAAgB;CACnC,MAAM,kBAAkB,KAAK,UAAU,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC;AAE1D,OAAM,KAAK,MACT;;;;;;;;;;;aAYA;EAAC;EAAO,MAAM;EAAY;EAAQ;EAAiB;EAAK,CACzD;;;;;AAMH,eAAsB,oBACpB,MACA,OAAmC,EAAE,EACP;AAC9B,OAAM,0BAA0B,KAAK;CAErC,MAAM,aAAuB,EAAE;CAC/B,MAAM,SAAoB,EAAE;CAC5B,IAAI,MAAM;AAEV,YAAW,KAAK,YAAY,QAAQ;AACpC,QAAO,KAAK,KAAK,SAAS,UAAU;AAEpC,KAAI,KAAK,WAAW,QAAW;AAC7B,aAAW,KAAK,aAAa,QAAQ;AACrC,SAAO,KAAK,KAAK,OAAO;;CAG1B,MAAM,QAAQ,SAAS,WAAW,KAAK,QAAQ;CAC/C,MAAM,QAAQ,KAAK,SAAS;AAC5B,QAAO,KAAK,MAAM;AAclB,SAZe,MAAM,KAAK,MACxB;;;;;OAKG,MAAM;;cAEC,OACV,OACD,EAEa;;;;;;;;;;;;AC9chB,eAAsB,aACpB,QACA,QACkB;CAIlB,MAAM,IAAI;AAEV,SAAQ,QAAR;EACE,KAAK,gBACH,QAAO,iBAAiB,YAAY,gBAAgB,EAA4C;EAElG,KAAK,aACH,QAAO,cAAc,YAAY,EAAyC;EAE5E,KAAK,eACH,QAAO,gBAAgB,YAAY,EAA2C;EAEhF,KAAK,eACH,QAAO,gBAAgB,YAAY,EAA2C;EAEhF,KAAK,eACH,QAAO,gBAAgB,YAAY,EAA2C;EAEhF,KAAK,kBACH,QAAO,mBAAmB,YAAY,EAA8C;EAEtF,KAAK,iBACH,QAAO,kBAAkB,YAAY,EAA6C;EAEpF,KAAK,iBACH,QAAO,kBAAkB,YAAY,EAA6C;EAEpF,KAAK,eACH,QAAO,gBAAgB,YAAY,EAA2C;EAEhF,KAAK,gBACH,QAAO,iBAAiB,YAAY,EAA4C;EAElF,KAAK,kBACH,QAAO,mBAAmB,YAAY,gBAAgB,EAA8C;EAEtG,KAAK,cACH,QAAO,iBACL,YACA,gBACA,EACD;EAEH,KAAK,qBACH,QAAO,iBACL,YACA,gBACA,EACD;EAEH,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,sBAAsB;GACzB,MAAM,EAAE,mBAAmB,kBAAkB,oBAAoB,mBAAmB,oBAAoB,kBAAkB,oBAAoB,0BAA0B,MAAM,OAAO;AAErL,WAAQ,QAAR;IACE,KAAK,iBAAkB,QAAO,kBAAkB,gBAAgB,EAA6C;IAC7G,KAAK,gBAAiB,QAAO,iBAAiB,gBAAgB,EAA4C;IAC1G,KAAK,kBAAmB,QAAO,mBAAmB,gBAAgB,EAA8C;IAChH,KAAK,iBAAkB,QAAO,kBAAkB,gBAAgB,EAA6C;IAC7G,KAAK,kBAAmB,QAAO,mBAAmB,gBAAgB,EAA8C;IAChH,KAAK,gBAAiB,QAAO,iBAAiB,gBAAgB,EAA4C;IAC1G,KAAK,mBAAoB,QAAO,mBAAmB,gBAAgB,EAA8C;IACjH,KAAK,qBAAsB,QAAO,sBAAsB,gBAAgB,EAAiD;;AAE3H;;EAGF,KAAK,kBAAkB;GACrB,MAAM,EAAE,wBAAwB,MAAM,OAAO;AAE7C,UAAO,oBADS,eAA2C,WAAW,IAAI,MACvC,gBAAgB,EAA+C;;EAGpG,KAAK,sBAAsB;GACzB,MAAM,EAAE,4BAA4B,MAAM,OAAO;AAEjD,UAAO,wBADS,eAA2C,WAAW,IAAI,MACnC,gBAAgB,EAAmD;;EAG5G,KAAK,sBAAsB;GACzB,MAAM,EAAE,2BAA2B,MAAM,OAAO;AAEhD,UAAO,uBADS,eAA2C,WAAW,IAAI,MACpC,gBAAgB,EAAkD;;EAG1G,KAAK,eAAe;GAClB,MAAM,EAAE,qBAAqB,MAAM,OAAO;AAC1C,UAAO,iBAAiB,gBAAgB,EAA4C;;EAGtF,KAAK,sBAAsB;GACzB,MAAM,EAAE,2BAA2B,MAAM,OAAO;AAChD,UAAO,uBAAuB,gBAAgB,EAAkD;;EAGlG,KAAK,oBAAoB;GACvB,MAAM,EAAE,0BAA0B,MAAM,OAAO;AAC/C,OAAI,CAAC,aAAa,UAChB,OAAM,IAAI,MAAM,4EAA4E;AAE9F,UAAO,sBACL,GACA,aAAa,UACd;;EAGH,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,qBAAqB;GACxB,MAAM,EAAE,WAAW,aAAa,kBAAkB,yBAAyB,MAAM,OAAO;GACxF,MAAM,SAAU,eAA2C,WAAW,IAAI;AAC1E,OAAI,CAAC,OACH,OAAM,IAAI,MAAM,GAAG,OAAO,sCAAsC;AAElE,WAAQ,QAAR;IACE,KAAK,SAAoB,QAAO,UAAU,QAAQ,EAAqC;IACvF,KAAK,WAAoB,QAAO,YAAY,QAAQ,EAAuC;IAC3F,KAAK,gBAAoB,QAAO,iBAAiB,QAAQ,EAA4C;IACrG,KAAK,oBAAqB,QAAO,qBAAqB,QAAQ,EAAgD;;AAEhH;;EAGF,KAAK,kBAAkB;GACrB,MAAM,EAAE,sBAAsB,MAAM,OAAO;AAC3C,UAAO,kBAAkB,YAAY,gBAAgB,EAA6C;;EAGpG,KAAK,mBAAmB;GAEtB,MAAM,eAAe,gBAAgB;AACrC,OAAI;AACF,WAAO,MAAM,mBAAmB,cAAc,EAA8C;aACpF;AACR,iBAAa,OAAO;;;EAIxB,KAAK,oBAAoB;GAEvB,MAAM,eAAe,gBAAgB;GACrC,MAAM,SAAU,eAA2C,WAAW,IAAI;AAC1E,OAAI,CAAC,QAAQ;AACX,iBAAa,OAAO;AACpB,UAAM,IAAI,MAAM,+EAA+E;;AAEjG,OAAI;AACF,WAAO,MAAM,mBAAmB,cAAc,QAAQ,EAA8C;aAC5F;AACR,iBAAa,OAAO;;;EAIxB,QACE,OAAM,IAAI,MAAM,mBAAmB,SAAS;;;;;;AC1JlD,SAAgB,aAAa,QAAgB,UAA6B;AACxE,KAAI;AACF,SAAO,MAAM,KAAK,UAAU,SAAS,GAAG,KAAK;SACvC;;;;;AAYV,eAAsB,cACpB,SACA,QACe;CACf,MAAM,EAAE,IAAI,QAAQ,WAAW;AAG/B,KAAI,WAAW,UAAU;EACvB,MAAM,UAAU,OAAO,YAAY;AACjC,OAAI;AACF,QAAI,CAAC,eAAgB,QAAO;IAC5B,MAAM,WAAW,MAAM,eAAe,UAAU;IAChD,MAAM,WACJ,WACG,QAAQ,qCAAqC,CAC7C,KAAK,CACR;AACF,WAAO;KAAE,OAAO,SAAS;KAAO,QAAQ,SAAS;KAAQ;KAAU;WAC7D;AACN,WAAO;;MAEP;AAEJ,eAAa,QAAQ;GACnB;GACA,IAAI;GACJ,QAAQ;IACN,QAAQ,KAAK,OAAO,KAAK,KAAK,GAAG,aAAa,IAAK;IACnD;IACA,eAAe,gBAAgB,IAAI,KAAK,cAAc,CAAC,aAAa,GAAG;IACvE,mBAAmB,aAAa;IAChC;IACA,eAAe,gBAAgB,IAAI,KAAK,cAAc,CAAC,aAAa,GAAG;IACvE,mBAAmB,aAAa;IAChC,YAAY,aAAa;IACzB,gBAAgB,gBAAgB,eAAe;IAC/C,IAAI;IACJ;IACA,oBAAoB,qBAAqB,IAAI,KAAK,mBAAmB,CAAC,aAAa,GAAG;IACtF,WAAW,aAAa,aAAa;IACrC,WAAWA,UAAe;IAC3B;GACF,CAAC;AACF,SAAO,KAAK;AACZ;;AAIF,KAAI,WAAW,aAAa;AAC1B,YAAU,CAAC,OAAO,MAAM;AACtB,WAAQ,OAAO,MAAM,iCAAiC,EAAE,IAAI;IAC5D;AACF,eAAa,QAAQ;GAAE;GAAI,IAAI;GAAM,QAAQ,EAAE,WAAW,MAAM;GAAE,CAAC;AACnE,SAAO,KAAK;AACZ;;AAIF,KAAI,WAAW,mBAAmB;AAChC,iBAAe,CAAC,OAAO,MAAM;AAC3B,WAAQ,OAAO,MAAM,uCAAuC,EAAE,IAAI;IAClE;AACF,eAAa,QAAQ;GAAE;GAAI,IAAI;GAAM,QAAQ,EAAE,WAAW,MAAM;GAAE,CAAC;AACnE,SAAO,KAAK;AACZ;;AAIF,KAAI,WAAW,2BAA2B;AACxC,eAAa,QAAQ;GACnB;GACA,IAAI;GACJ,QAAQ;IACN,QAAQ;IACR,gBAAgB,OAAO,QAAQ,mBAAmB,SAAS,CACxD,QAAQ,CAAC,IAAI,SAAS,OAAO,WAAY,IAA6B,QAAQ,CAC9E,KAAK,CAAC,QAAQ,GAAG;IACrB;GACF,CAAC;AACF,SAAO,KAAK;AACZ;;AAIF,KAAI,WAAW,2BAA2B;AACxC,MAAI;GACF,MAAM,IAAI;GAKV,MAAM,UAAU,wBAAwB;IACtC,MAAM,EAAE;IACR,UAAU,EAAE;IACZ,SAAS,EAAE;IACZ,CAAC;AACF,yBAAsB,QAAQ;AAC9B,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAM,QAAQ,EAAE,QAAQ,SAAS;IAAE,CAAC;WAC5D,GAAG;AAEV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;;AAErD,SAAO,KAAK;AACZ;;AAIF,KAAI,WAAW,qBAAqB;EAClC,MAAM,IAAI;AAMV,MAAI,CAAC,EAAE,SAAS;AACd,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAAO;IAA0C,CAAC;AACxF,UAAO,KAAK;AACZ;;AAKF,oBACE;GAAE,OAHW,EAAE,SAA8D;GAGpE,SAAS,EAAE;GAAS,OAAO,EAAE;GAAO,EAC7C,mBACD,CAAC,MAAM,WAAW;AACjB,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAM;IAAQ,CAAC;AAC9C,UAAO,KAAK;IACZ,CAAC,OAAO,MAAM;AAEd,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;AACnD,UAAO,KAAK;IACZ;AACF;;AAKF,KAAI,WAAW,qBAAqB;EAClC,MAAM,OAAQ,eAA2C,WAAW;AACpE,MAAI,CAAC,MAAM;AACT,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAAO;IAAyC,CAAC;AACvF,UAAO,KAAK;AACZ;;AAEF,MAAI;GACF,MAAM,IAAI;GAcV,IAAI,aAA4B;GAChC,IAAI,eAA8B;AAClC,OAAI,EAAE,KAAK;IACT,MAAM,MAAM,WAAW,QACrB,4HACD,CAAC,IAAI,EAAE,IAAI;AACZ,QAAI,KAAK;AACP,kBAAa,IAAI;AACjB,oBAAe,IAAI;;;AAIvB,SAAM,wBAAwB,KAAK;AAenC,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAM,QAAQ;KAAE,IAAI;KAAM,IAdtC,MAAM,iBAAiB,MAAM;MAC9C,YAAY,EAAE;MACd;MACA;MACA,MAAM,EAAE;MACR,OAAO,EAAE;MACT,WAAW,EAAE,aAAa;MAC1B,WAAW,EAAE;MACb,oBAAoB,EAAE,sBAAsB;MAC5C,YAAY,EAAE,cAAc,EAAE;MAC9B,gBAAgB,EAAE,kBAAkB,EAAE;MACtC,UAAU,EAAE,YAAY,EAAE;MAC3B,CAAC;KAEuE;IAAE,CAAC;WACrE,GAAG;AAEV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;;AAErD,SAAO,KAAK;AACZ;;AAKF,KAAI,WAAW,0BAA0B;EACvC,MAAM,OAAQ,eAA2C,WAAW;AACpE,MAAI,CAAC,MAAM;AACT,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAAO;IAA6C,CAAC;AAC3F,UAAO,KAAK;AACZ;;AAEF,MAAI;GACF,MAAM,IAAI;GAOV,IAAI,eAA8B;AAClC,OAAI,EAAE,KAAK;IACT,MAAM,MAAM,WAAW,QACrB,wHACD,CAAC,IAAI,EAAE,IAAI;AACZ,QAAI,IAAK,gBAAe,IAAI;;AAG9B,SAAM,sBAAsB,MAAM;IAChC,YAAY,EAAE;IACd,QAAQ,EAAE;IACV,OAAO,EAAE;IACT;IACD,CAAC;AAEF,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAM,QAAQ,EAAE,IAAI,MAAM;IAAE,CAAC;WACrD,GAAG;AAEV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;;AAErD,SAAO,KAAK;AACZ;;AAGF,KAAI,WAAW,yBAAyB;EACtC,MAAM,OAAQ,eAA2C,WAAW;AACpE,MAAI,CAAC,MAAM;AACT,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAAO;IAA6C,CAAC;AAC3F,UAAO,KAAK;AACZ;;AAEF,MAAI;GACF,MAAM,IAAI;AAMV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAM,QALxB,MAAM,oBAAoB,MAAM;KAC3C,OAAO,EAAE;KACT,QAAQ,EAAE;KACV,OAAO,EAAE;KACV,CAAC;IACiD,CAAC;WAC7C,GAAG;AAEV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;;AAErD,SAAO,KAAK;AACZ;;AAGF,KAAI,WAAW,qBAAqB;EAClC,MAAM,OAAQ,eAA2C,WAAW;AACpE,MAAI,CAAC,MAAM;AACT,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAAO;IAAyC,CAAC;AACvF,UAAO,KAAK;AACZ;;AAEF,MAAI;GACF,MAAM,IAAI;AAeV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAM,QAPxB,MAAM,kBAAkB,MAAM;KACzC,WAAW,EAAE;KACb,WAAW,EAAE;KACb,MAAM,EAAE;KACR,OAAO,EAAE;KACT,QAAQ,EAAE;KACX,CAAC;IACiD,CAAC;WAC7C,GAAG;AAEV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;;AAErD,SAAO,KAAK;AACZ;;AAGF,KAAI,WAAW,sBAAsB;EACnC,MAAM,OAAQ,eAA2C,WAAW;AACpE,MAAI,CAAC,MAAM;AACT,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAAO;IAAyC,CAAC;AACvF,UAAO,KAAK;AACZ;;AAEF,MAAI;GACF,MAAM,IAAI;GACV,MAAM,QAAQ,EAAE,SAAS;GAEzB,IAAI,oBAAoB,EAAE;GAC1B,IAAI;AACJ,OAAI,sBAAsB,UAAa,EAAE,KAAK;IAC5C,MAAM,MAAM,WAAW,QACrB,4HACD,CAAC,IAAI,EAAE,IAAI;AACZ,QAAI,KAAK;AACP,yBAAoB,IAAI;AACxB,2BAAsB,IAAI;;;GAI9B,IAAI;AACJ,OAAI,sBAAsB,OACxB,QAAO,MAAM,wBAAwB,MAAM,mBAAmB,MAAM;OAEpE,QAAO,MAAM,kBAAkB,MAAM,EAAE,OAAO,CAAC;AAEjD,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAM,QAAQ;KAAE;KAAM,cAAc;KAAqB;IAAE,CAAC;WACpF,GAAG;AAEV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;;AAErD,SAAO,KAAK;AACZ;;AAIF,KAAI,WAAW,oBAAoB;EACjC,MAAM,OAAQ,eAA2C,WAAW;AACpE,MAAI,CAAC,MAAM;AACT,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAAO;IAAyC,CAAC;AACvF,UAAO,KAAK;AACZ;;AAEF,MAAI;GACF,MAAM,IAAI;GAQV,IAAI;AACJ,OAAI,EAAE,aAIJ,aAHY,WAAW,QACrB,yCACD,CAAC,IAAI,EAAE,aAAa,EACJ;AAUnB,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAM,QAPxB,MAAM,kBAAkB,MAAM;KACzC;KACA,WAAW,EAAE;KACb,MAAM,EAAE;KACR,OAAO,EAAE,SAAS;KAClB,QAAQ,EAAE,UAAU;KACrB,CAAC;IACiD,CAAC;WAC7C,GAAG;AAEV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;;AAErD,SAAO,KAAK;AACZ;;AAIF,KAAI,WAAW,qBAAqB;EAClC,MAAM,OAAQ,eAA2C,WAAW;AACpE,MAAI,CAAC,MAAM;AACT,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAAO;IAAyC,CAAC;AACvF,UAAO,KAAK;AACZ;;AAEF,MAAI;AACF,SAAM,wBAAwB,KAAK;GACnC,MAAM,CAAC,UAAU,WAAW,cAAc,aAAa,MAAM,QAAQ,IAAI;IACvE,KAAK,MAAyB,iDAAiD;IAC/E,KAAK,MACH,yFACD;IACD,KAAK,MACH,kHACD;IACD,KAAK,MACH,2EACD;IACF,CAAC;AAEF,gBAAa,QAAQ;IACnB;IACA,IAAI;IACJ,QAAQ;KACN,OAAO,SAAS,SAAS,KAAK,IAAI,SAAS,KAAK,GAAG;KACnD,SAAS,UAAU,KAAK,KAAI,OAAM;MAAE,MAAM,EAAE;MAAM,OAAO,SAAS,EAAE,OAAO,GAAG;MAAE,EAAE;KAClF,YAAY,aAAa,KAAK,KAAI,OAAM;MAAE,cAAc,EAAE;MAAc,OAAO,SAAS,EAAE,OAAO,GAAG;MAAE,EAAE;KACxG,aAAa,UAAU,KAAK,IAAI,cAAc;KAC/C;IACF,CAAC;WACK,GAAG;AAEV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;;AAErD,SAAO,KAAK;AACZ;;AAGF,KAAI,WAAW,yBAAyB;EACtC,MAAM,OAAQ,eAA2C,WAAW;AACpE,MAAI,CAAC,MAAM;AACT,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAAO;IAA8C,CAAC;AAC5F,UAAO,KAAK;AACZ;;AAEF,MAAI;GACF,MAAM,IAAI;GAaV,IAAI,oBAAoB,EAAE,cAAc;GACxC,IAAI,sBAAsB,EAAE,gBAAgB;AAC5C,OAAI,sBAAsB,QAAQ,EAAE,KAAK;IACvC,MAAM,MAAM,WAAW,QACrB,4HACD,CAAC,IAAI,EAAE,IAAI;AACZ,QAAI,KAAK;AACP,yBAAoB,IAAI;AACxB,2BAAsB,IAAI;;;AAI9B,SAAM,oBAAoB,MAAM;IAC9B,YAAY,EAAE;IACd,YAAY;IACZ,cAAc;IACd,SAAS,EAAE,WAAW;IACtB,cAAc,EAAE,gBAAgB;IAChC,SAAS,EAAE,WAAW;IACtB,WAAW,EAAE,aAAa;IAC1B,YAAY,EAAE,cAAc;IAC5B,mBAAmB,EAAE,qBAAqB;IAC3C,CAAC;AAEF,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAM,QAAQ,EAAE,IAAI,MAAM;IAAE,CAAC;WACrD,GAAG;AAEV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;;AAErD,SAAO,KAAK;AACZ;;AAIF,KAAI,WAAW,sBAAsB;AACnC,MAAI;GACF,MAAM,IAAI;AAOV,OAAI,CAAC,EAAE,MAAM;AACX,iBAAa,QAAQ;KAAE;KAAI,IAAI;KAAO,OAAO;KAAwC,CAAC;AACtF,WAAO,KAAK;AACZ;;GAGF,MAAM,OAAO,QAAQ;IACnB,MAAM,EAAE;IACR,UAAU,EAAE;IACZ,SAAS,EAAE,WAAW,EAAE;IACxB,aAAa,EAAE;IAChB,CAAC;AAEF,kCAAe;AAEf,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAM,QAAQ;KAAE,IAAI,KAAK;KAAI,QAAQ,KAAK;KAAQ;IAAE,CAAC;WAC7E,GAAG;AAEV,gBAAa,QAAQ;IAAE;IAAI,IAAI;IAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;IACJ,CAAC;;AAErD,SAAO,KAAK;AACZ;;AAIF,KAAI,WAAW,oBAAoB;AACjC,eAAa,QAAQ;GAAE;GAAI,IAAI;GAAM,QAAQA,UAAe;GAAE,CAAC;AAC/D,SAAO,KAAK;AACZ;;AAIF,KAAI;AAEF,eAAa,QAAQ;GAAE;GAAI,IAAI;GAAM,QADtB,MAAM,aAAa,QAAQ,OAAO;GACJ,CAAC;UACvC,GAAG;AAEV,eAAa,QAAQ;GAAE;GAAI,IAAI;GAAO,OAD1B,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;GACJ,CAAC;;AAErD,QAAO,KAAK;;;;;;;;;;;;ACthBd,SAAS,aAAa,MAAgC;AACpD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,SAAS,QAAQ,KAAK;EAC5B,MAAM,QAAQ,iBAAiB;AAAE,UAAO,SAAS;AAAE,WAAQ,MAAM;KAAK,IAAI;AAC1E,SAAO,GAAG,iBAAiB;AAAE,gBAAa,MAAM;AAAE,UAAO,KAAK;AAAE,WAAQ,KAAK;IAAI;AACjF,SAAO,GAAG,eAAe;AAAE,gBAAa,MAAM;AAAE,WAAQ,MAAM;IAAI;GAClE;;;;;AAMJ,eAAe,eAAe,YAAqC;AACjE,KAAI,WAAW,WAAW,EAAE;AAE1B,MADa,MAAM,aAAa,WAAW,CAEzC,OAAM,IAAI,MAAM,wEAAwE;AAE1F,MAAI;AACF,cAAW,WAAW;AACtB,WAAQ,OAAO,MAAM,4CAA4C;UAC3D;;CAKV,MAAM,SAAS,cAAc,WAAmB;EAC9C,IAAI,SAAS;AAEb,SAAO,GAAG,SAAS,UAAkB;AACnC,aAAU,MAAM,UAAU;GAC1B,IAAI;AACJ,WAAQ,KAAK,OAAO,QAAQ,KAAK,MAAM,IAAI;IACzC,MAAM,OAAO,OAAO,MAAM,GAAG,GAAG;AAChC,aAAS,OAAO,MAAM,KAAK,EAAE;AAE7B,QAAI,KAAK,MAAM,KAAK,GAAI;IAExB,IAAI;AACJ,QAAI;AACF,eAAU,KAAK,MAAM,KAAK;YACpB;AACN,kBAAa,QAAQ;MAAE,IAAI;MAAK,IAAI;MAAO,OAAO;MAAgB,CAAC;AACnE,YAAO,SAAS;AAChB;;AAGF,kBAAc,SAAS,OAAO,CAAC,OAAO,MAAe;KACnD,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,kBAAa,QAAQ;MAAE,IAAI,QAAQ;MAAI,IAAI;MAAO,OAAO;MAAK,CAAC;AAC/D,YAAO,SAAS;MAChB;;IAEJ;AAEF,SAAO,GAAG,eAAe,GAEvB;GACF;AAEF,QAAO,GAAG,UAAU,MAAM;AACxB,UAAQ,OAAO,MAAM,kCAAkC,EAAE,IAAI;GAC7D;AAEF,QAAO,OAAO,kBAAkB;AAC9B,UAAQ,OAAO,MACb,wCAAwC,WAAW,IACpD;GACD;AAEF,QAAO;;AAOT,eAAsB,MAAM,QAAwC;AAClE,iBAAgB,OAAO;AACvB,cAAa,KAAK,KAAK,CAAC;AAExB,uBAAsB,wBAAwB,CAAC;AAE/C,SAAQ,OAAO,MAAM,oCAAoC;AACzD,SAAQ,OAAO,MAAM,wBAAwB,OAAO,WAAW,IAAI;AACnE,SAAQ,OAAO,MAAM,iCAAiC,OAAO,eAAe,IAAI;CAChF,MAAM,EAAE,uBAAuB,MAAM,OAAO;AAC5C,SAAQ,OAAO,MACb,mCAAmC,mBAAmB,KAAK,IAC5D;AAGD,KAAI;AAAE,cAAY,QAAQ,KAAK,GAAG;SAAU;AAE5C,yBAAwB,OAAO,eAAe;AAE9C,KAAI;AACF,gBAAc,cAAc,CAAC;AAC7B,UAAQ,OAAO,MAAM,2CAA2C;UACzD,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,OAAO,MAAM,gDAAgD,IAAI,IAAI;AAC7E,UAAQ,KAAK,EAAE;;CAMjB,MAAM,SAAS,MAAM,eAAe,OAAO,WAAW;AAGtD,6BAA4B;AAO5B,CAAK,qBAAqB,QAAQ,EAAE,iBAAiB,MAAM,CAAC,CACzD,MAAM,YAAY;AACjB,oBAAkB,QAAQ;AAC1B,UAAQ,OAAO,MACb,oCAAoC,QAAQ,YAAY,IACzD;AAED,uBAAqB;AAErB,MAAI,QAAQ,gBAAgB,WAC1B,sBAAqB;MAErB,SAAQ,OAAO,MACb,4DACD;AAIH,aAAW;AACX,eAAa;GACb,CACD,OAAO,MAAM;EACZ,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,OAAO,MACb,0DAA0D,IAAI,IAC/D;AACD,UAAQ,KAAK,EAAE;GACf;CAEJ,MAAM,WAAW,OAAO,WAAkC;AACxD,UAAQ,OAAO,MAAM,kBAAkB,OAAO,wBAAwB;AAEtE,uBAAqB,KAAK;AAE1B,MAAI,oBAAqB,eAAc,oBAAoB;AAC3D,MAAI,oBAAqB,eAAc,oBAAoB;AAE3D,cAAY;AAEZ,SAAO,OAAO;EAEd,MAAM,sBAAsB;EAC5B,MAAM,mBAAmB;EACzB,MAAM,WAAW,KAAK,KAAK,GAAG;AAE9B,MAAI,mBAAmB,iBAAiB;AACtC,WAAQ,OAAO,MACb,oEACY,gBAAgB,UAAU,gBAAgB,QACvD;AAED,WAAQ,mBAAmB,oBAAoB,KAAK,KAAK,GAAG,SAC1D,OAAM,IAAI,SAAS,YAAY,WAAW,SAAS,iBAAiB,CAAC;AAGvE,OAAI,mBAAmB,gBACrB,SAAQ,OAAO,MAAM,0DAA0D;OAE/E,SAAQ,OAAO,MAAM,kDAAkD;;AAI3E,MAAI;AACF,SAAM,eAAe,OAAO;UACtB;AAIR,MAAI;AACF,cAAW,OAAO,WAAW;UACvB;AAIR,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,GAAG,gBAAgB;AAAE,WAAS,SAAS,CAAC,YAAY,QAAQ,KAAK,EAAE,CAAC;GAAI;AAChF,SAAQ,GAAG,iBAAiB;AAAE,WAAS,UAAU,CAAC,YAAY,QAAQ,KAAK,EAAE,CAAC;GAAI;AAGlF,OAAM,IAAI,cAAc,GAAG"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as PaiClient } from "../ipc-client-
|
|
3
|
-
import { o as loadConfig } from "../config-
|
|
2
|
+
import { t as PaiClient } from "../ipc-client-CoyUHPod.mjs";
|
|
3
|
+
import { o as loadConfig } from "../config-BuhHWyOK.mjs";
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
|
|
@@ -129,4 +129,4 @@ function openFederation(path = DEFAULT_FEDERATION_PATH) {
|
|
|
129
129
|
|
|
130
130
|
//#endregion
|
|
131
131
|
export { initializeFederationSchema as i, openFederation as n, FEDERATION_SCHEMA_SQL as r, db_exports as t };
|
|
132
|
-
//# sourceMappingURL=db-
|
|
132
|
+
//# sourceMappingURL=db-CmYbAVCD.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db-
|
|
1
|
+
{"version":3,"file":"db-CmYbAVCD.mjs","names":[],"sources":["../src/memory/schema.ts","../src/memory/db.ts"],"sourcesContent":["/**\n * SQLite DDL for the PAI federation database (federation.db).\n *\n * The federation DB is the cross-project search index — a single SQLite file\n * at ~/.pai/federation.db that holds chunked text from every registered\n * project's memory/ and Notes/ directories.\n *\n * Tables:\n * - memory_files — file-level metadata (hash, mtime, size) for change detection\n * - memory_chunks — chunked text with line numbers, tier classification, and optional embedding\n * - memory_fts — FTS5 virtual table backed by memory_chunks text\n *\n * Vault tables (vault_files, vault_aliases, vault_links, vault_name_index, vault_health)\n * have been migrated to Postgres (docker/init.sql) and are no longer created here.\n *\n * Schema version history:\n * v1 — initial schema (BM25 search only)\n * v2 — added embedding BLOB column to memory_chunks (Phase 2.5, vector search)\n * v3 — added vault tables (now removed — vault tables live in Postgres)\n */\n\nimport type { Database } from \"better-sqlite3\";\n\n/** Current schema version. Bump when adding new columns or tables. */\nexport const SCHEMA_VERSION = 5;\n\nexport const FEDERATION_SCHEMA_SQL = `\nPRAGMA journal_mode = WAL;\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS memory_files (\n project_id INTEGER NOT NULL,\n path TEXT NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n hash TEXT NOT NULL,\n mtime INTEGER NOT NULL,\n size INTEGER NOT NULL,\n PRIMARY KEY (project_id, path)\n);\n\nCREATE TABLE IF NOT EXISTS memory_chunks (\n id TEXT PRIMARY KEY,\n project_id INTEGER NOT NULL,\n source TEXT NOT NULL DEFAULT 'memory',\n tier TEXT NOT NULL DEFAULT 'topic',\n path TEXT NOT NULL,\n start_line INTEGER NOT NULL,\n end_line INTEGER NOT NULL,\n hash TEXT NOT NULL,\n text TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n last_accessed_at INTEGER,\n relevance_score REAL DEFAULT 0.5,\n embedding BLOB\n);\n\nCREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(\n text,\n id UNINDEXED,\n project_id UNINDEXED,\n path UNINDEXED,\n source UNINDEXED,\n tier UNINDEXED,\n start_line UNINDEXED,\n end_line UNINDEXED\n);\n\nCREATE INDEX IF NOT EXISTS idx_mc_project ON memory_chunks(project_id);\nCREATE INDEX IF NOT EXISTS idx_mc_source ON memory_chunks(project_id, source);\nCREATE INDEX IF NOT EXISTS idx_mc_tier ON memory_chunks(tier);\nCREATE INDEX IF NOT EXISTS idx_mf_project ON memory_files(project_id);\n\nCREATE TABLE IF NOT EXISTS kg_entities (\n entity_id TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL DEFAULT 'default',\n name TEXT NOT NULL,\n type TEXT NOT NULL DEFAULT 'unknown',\n description TEXT,\n first_seen INTEGER,\n last_seen INTEGER,\n mention_count INTEGER NOT NULL DEFAULT 1,\n feedback_weight REAL NOT NULL DEFAULT 0.5,\n UNIQUE(tenant_id, entity_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_kge_tenant ON kg_entities(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_kge_name ON kg_entities(tenant_id, name);\nCREATE INDEX IF NOT EXISTS idx_kge_type ON kg_entities(tenant_id, type);\n`;\n\n/**\n * Apply the full federation schema to an open database.\n *\n * Idempotent — all statements use IF NOT EXISTS so calling this on an\n * already-initialised database is safe.\n *\n * Also runs any necessary migrations for existing databases (e.g. adding the\n * embedding column to an older schema that was created without it).\n */\nexport function initializeFederationSchema(db: Database): void {\n db.exec(FEDERATION_SCHEMA_SQL);\n runMigrations(db);\n}\n\n// ---------------------------------------------------------------------------\n// Migrations\n// ---------------------------------------------------------------------------\n\n/**\n * Apply incremental migrations to an existing database.\n *\n * Each migration is idempotent — safe to call on a database that has already\n * been migrated.\n */\nfunction runMigrations(db: Database): void {\n const columns = db.prepare(\"PRAGMA table_info(memory_chunks)\").all() as Array<{\n name: string;\n }>;\n\n // Migration v1→v2: add embedding BLOB column (schema v2, Phase 2.5)\n const hasEmbedding = columns.some((c) => c.name === \"embedding\");\n if (!hasEmbedding) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN embedding BLOB\");\n }\n\n // Create the partial index for embedded chunks (safe now that the column exists)\n db.exec(\n \"CREATE INDEX IF NOT EXISTS idx_mc_embedding ON memory_chunks(id) WHERE embedding IS NOT NULL\",\n );\n\n // Migration v4→v5: add last_accessed_at and relevance_score columns (QW2 + MR2)\n const hasLastAccessedAt = columns.some((c) => c.name === \"last_accessed_at\");\n if (!hasLastAccessedAt) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN last_accessed_at INTEGER\");\n }\n\n const hasRelevanceScore = columns.some((c) => c.name === \"relevance_score\");\n if (!hasRelevanceScore) {\n db.exec(\"ALTER TABLE memory_chunks ADD COLUMN relevance_score REAL DEFAULT 0.5\");\n }\n}\n","/**\n * Database connection helper for the PAI federation DB.\n *\n * Uses better-sqlite3 (synchronous API) to open or create federation.db.\n * On first open it runs the full DDL via initializeFederationSchema().\n */\n\nimport { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport BetterSqlite3 from \"better-sqlite3\";\nimport type { Database } from \"better-sqlite3\";\nimport { initializeFederationSchema } from \"./schema.js\";\n\nexport type { Database };\n\n/** Default federation DB path inside the ~/.pai/ directory. */\nconst DEFAULT_FEDERATION_PATH = join(homedir(), \".pai\", \"federation.db\");\n\n/**\n * Open (or create) the PAI federation database.\n *\n * @param path Absolute path to federation.db. Defaults to ~/.pai/federation.db.\n * @returns An open better-sqlite3 Database instance.\n *\n * Side effects on first call:\n * - Creates the parent directory if it does not exist.\n * - Enables WAL journal mode.\n * - Runs initializeFederationSchema() to ensure tables exist.\n */\nexport function openFederation(path: string = DEFAULT_FEDERATION_PATH): Database {\n // Ensure the directory exists before SQLite tries to create the file\n mkdirSync(dirname(path), { recursive: true });\n\n const db = new BetterSqlite3(path);\n\n // WAL gives better concurrent read performance and crash safety\n db.pragma(\"journal_mode = WAL\");\n db.pragma(\"foreign_keys = ON\");\n\n // Apply schema (idempotent — all statements use IF NOT EXISTS)\n initializeFederationSchema(db);\n\n return db;\n}\n"],"mappings":";;;;;;;AA0BA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0ErC,SAAgB,2BAA2B,IAAoB;AAC7D,IAAG,KAAK,sBAAsB;AAC9B,eAAc,GAAG;;;;;;;;AAanB,SAAS,cAAc,IAAoB;CACzC,MAAM,UAAU,GAAG,QAAQ,mCAAmC,CAAC,KAAK;AAMpE,KAAI,CADiB,QAAQ,MAAM,MAAM,EAAE,SAAS,YAAY,CAE9D,IAAG,KAAK,sDAAsD;AAIhE,IAAG,KACD,+FACD;AAID,KAAI,CADsB,QAAQ,MAAM,MAAM,EAAE,SAAS,mBAAmB,CAE1E,IAAG,KAAK,gEAAgE;AAI1E,KAAI,CADsB,QAAQ,MAAM,MAAM,EAAE,SAAS,kBAAkB,CAEzE,IAAG,KAAK,wEAAwE;;;;;;;;;;;;;AC1HpF,MAAM,0BAA0B,KAAK,SAAS,EAAE,QAAQ,gBAAgB;;;;;;;;;;;;AAaxE,SAAgB,eAAe,OAAe,yBAAmC;AAE/E,WAAU,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;CAE7C,MAAM,KAAK,IAAI,cAAc,KAAK;AAGlC,IAAG,OAAO,qBAAqB;AAC/B,IAAG,OAAO,oBAAoB;AAG9B,4BAA2B,GAAG;AAE9B,QAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect-
|
|
1
|
+
{"version":3,"file":"detect-KjycLtXM.mjs","names":[],"sources":["../src/cli/commands/detect.ts"],"sourcesContent":["/**\n * Project detection logic for PAI.\n *\n * detectProject(cwd) — given a filesystem path, returns the best matching\n * project from the registry:\n * 1. Exact path match\n * 2. Longest parent match (project whose root_path is an ancestor of cwd)\n *\n * Exported for use by the CLI `pai project detect` command and the MCP\n * `project_detect` tool.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { resolve } from \"node:path\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface DetectedProject {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n type: string;\n status: string;\n session_count: number;\n last_session_date: string | null;\n match_type: \"exact\" | \"parent\";\n /** Only set when match_type is 'parent' — the portion of cwd below root_path */\n relative_path: string | null;\n}\n\ninterface ProjectRow {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n type: string;\n status: string;\n}\n\n// ---------------------------------------------------------------------------\n// Core detection function\n// ---------------------------------------------------------------------------\n\n/**\n * Detect which registered project a filesystem path belongs to.\n *\n * @param db Open registry database\n * @param cwd Absolute path to detect (defaults to process.cwd())\n * @returns The best matching project, or null if no match\n */\nexport function detectProject(\n db: Database,\n cwd?: string\n): DetectedProject | null {\n const target = resolve(cwd ?? process.cwd());\n\n // Load all active projects ordered by root_path length descending\n // so the longest (most specific) match wins in a linear scan.\n const projects = db\n .prepare(\n `SELECT id, slug, display_name, root_path, encoded_dir, type, status\n FROM projects\n WHERE status != 'archived'\n ORDER BY LENGTH(root_path) DESC`\n )\n .all() as ProjectRow[];\n\n let matched: ProjectRow | null = null;\n let matchType: \"exact\" | \"parent\" = \"exact\";\n\n for (const p of projects) {\n const root = resolve(p.root_path);\n if (target === root) {\n matched = p;\n matchType = \"exact\";\n break;\n }\n if (!matched && target.startsWith(root + \"/\")) {\n matched = p;\n matchType = \"parent\";\n // Keep scanning — a longer root_path match might exist (but shouldn't\n // since we sorted by length desc). Safety break anyway once found.\n break;\n }\n }\n\n if (!matched) return null;\n\n // Enrich with session stats\n const sessionStats = db\n .prepare(\n `SELECT COUNT(*) AS cnt, MAX(date) AS last_date\n FROM sessions WHERE project_id = ?`\n )\n .get(matched.id) as { cnt: number; last_date: string | null };\n\n const relative =\n matchType === \"parent\"\n ? target.slice(resolve(matched.root_path).length + 1)\n : null;\n\n return {\n id: matched.id,\n slug: matched.slug,\n display_name: matched.display_name,\n root_path: matched.root_path,\n encoded_dir: matched.encoded_dir,\n type: matched.type,\n status: matched.status,\n session_count: sessionStats.cnt,\n last_session_date: sessionStats.last_date,\n match_type: matchType,\n relative_path: relative,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format a DetectedProject for human-readable CLI output.\n */\nexport function formatDetection(d: DetectedProject): string {\n const lines: string[] = [\n `slug: ${d.slug}`,\n `display_name: ${d.display_name}`,\n `root_path: ${d.root_path}`,\n `type: ${d.type}`,\n `status: ${d.status}`,\n `match: ${d.match_type}${d.relative_path ? ` (+${d.relative_path})` : \"\"}`,\n `sessions: ${d.session_count}`,\n ];\n if (d.last_session_date) {\n lines.push(`last_session: ${d.last_session_date}`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Format a DetectedProject as JSON for machine consumption.\n */\nexport function formatDetectionJson(d: DetectedProject): string {\n return JSON.stringify(\n {\n slug: d.slug,\n display_name: d.display_name,\n root_path: d.root_path,\n encoded_dir: d.encoded_dir,\n type: d.type,\n status: d.status,\n match_type: d.match_type,\n relative_path: d.relative_path,\n session_count: d.session_count,\n last_session_date: d.last_session_date,\n },\n null,\n 2\n );\n}\n"],"mappings":";;;;;;;;;;AAuDA,SAAgB,cACd,IACA,KACwB;CACxB,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAI5C,MAAM,WAAW,GACd,QACC;;;wCAID,CACA,KAAK;CAER,IAAI,UAA6B;CACjC,IAAI,YAAgC;AAEpC,MAAK,MAAM,KAAK,UAAU;EACxB,MAAM,OAAO,QAAQ,EAAE,UAAU;AACjC,MAAI,WAAW,MAAM;AACnB,aAAU;AACV,eAAY;AACZ;;AAEF,MAAI,CAAC,WAAW,OAAO,WAAW,OAAO,IAAI,EAAE;AAC7C,aAAU;AACV,eAAY;AAGZ;;;AAIJ,KAAI,CAAC,QAAS,QAAO;CAGrB,MAAM,eAAe,GAClB,QACC;2CAED,CACA,IAAI,QAAQ,GAAG;CAElB,MAAM,WACJ,cAAc,WACV,OAAO,MAAM,QAAQ,QAAQ,UAAU,CAAC,SAAS,EAAE,GACnD;AAEN,QAAO;EACL,IAAI,QAAQ;EACZ,MAAM,QAAQ;EACd,cAAc,QAAQ;EACtB,WAAW,QAAQ;EACnB,aAAa,QAAQ;EACrB,MAAM,QAAQ;EACd,QAAQ,QAAQ;EAChB,eAAe,aAAa;EAC5B,mBAAmB,aAAa;EAChC,YAAY;EACZ,eAAe;EAChB;;;;;AAUH,SAAgB,gBAAgB,GAA4B;CAC1D,MAAM,QAAkB;EACtB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE;EACnB,iBAAiB,EAAE,aAAa,EAAE,gBAAgB,MAAM,EAAE,cAAc,KAAK;EAC7E,iBAAiB,EAAE;EACpB;AACD,KAAI,EAAE,kBACJ,OAAM,KAAK,iBAAiB,EAAE,oBAAoB;AAEpD,QAAO,MAAM,KAAK,KAAK;;;;;AAMzB,SAAgB,oBAAoB,GAA4B;AAC9D,QAAO,KAAK,UACV;EACE,MAAM,EAAE;EACR,cAAc,EAAE;EAChB,WAAW,EAAE;EACb,aAAa,EAAE;EACf,MAAM,EAAE;EACR,QAAQ,EAAE;EACV,YAAY,EAAE;EACd,eAAe,EAAE;EACjB,eAAe,EAAE;EACjB,mBAAmB,EAAE;EACtB,EACD,MACA,EACD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
-
import { n as populateSlugs, r as searchMemory } from "./search-
|
|
2
|
+
import { n as populateSlugs, r as searchMemory } from "./search-HcdKtMla.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/topics/detector.ts
|
|
5
5
|
var detector_exports = /* @__PURE__ */ __exportAll({ detectTopicShift: () => detectTopicShift });
|
|
@@ -71,4 +71,4 @@ async function detectTopicShift(registryDb, federation, params) {
|
|
|
71
71
|
|
|
72
72
|
//#endregion
|
|
73
73
|
export { detector_exports as n, detectTopicShift as t };
|
|
74
|
-
//# sourceMappingURL=detector-
|
|
74
|
+
//# sourceMappingURL=detector-ZiOhHszd.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detector-
|
|
1
|
+
{"version":3,"file":"detector-ZiOhHszd.mjs","names":[],"sources":["../src/topics/detector.ts"],"sourcesContent":["/**\n * Topic shift detection engine.\n *\n * Accepts a context summary (recent conversation text) and determines whether\n * the conversation has drifted away from the currently-routed project.\n *\n * Algorithm:\n * 1. Run keyword memory_search against the context text (no project filter)\n * 2. Score results by project — sum of BM25 scores per project\n * 3. Compare the top-scoring project against the current project\n * 4. If a different project dominates by more than the confidence threshold,\n * report a topic shift.\n *\n * Design decisions:\n * - Keyword search only (no semantic) — fast, no embedding requirement\n * - Works with or without an active daemon (direct DB access path)\n * - Stateless: callers supply currentProject; detector has no session memory\n * - Minimal: returns a plain result object, not MCP content arrays\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { searchMemory, populateSlugs } from \"../memory/search.js\";\nimport type { SearchResult } from \"../memory/search.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TopicCheckParams {\n /** Recent conversation context (a few sentences or tool call summaries) */\n context: string;\n /** The project slug the session is currently routed to. May be null/empty. */\n currentProject?: string;\n /**\n * Minimum confidence [0,1] to declare a shift. Default: 0.6.\n * Higher = less sensitive, fewer false positives.\n */\n threshold?: number;\n /**\n * Maximum results to draw from memory search (candidates). Default: 20.\n * More candidates = more accurate scoring, slightly slower.\n */\n candidates?: number;\n}\n\nexport interface TopicCheckResult {\n /** Whether a significant topic shift was detected. */\n shifted: boolean;\n /** The project slug the session is currently routed to (echoed from input). */\n currentProject: string | null;\n /** The project slug that best matches the context, or null if no clear match. */\n suggestedProject: string | null;\n /**\n * Confidence score for the suggested project [0,1].\n * Represents the fraction of total score mass held by the top project.\n * 1.0 = all matching chunks belong to one project.\n * 0.5 = two projects are equally matched.\n */\n confidence: number;\n /** Number of memory chunks that contributed to scoring. */\n chunkCount: number;\n /** Top-3 scoring projects with their normalised scores (for debugging). */\n topProjects: Array<{ slug: string; score: number }>;\n}\n\n// ---------------------------------------------------------------------------\n// Core algorithm\n// ---------------------------------------------------------------------------\n\n/**\n * Detect whether the provided context text best matches a different project\n * than the session's current routing.\n *\n * Works with either a raw SQLite Database or a StorageBackend.\n * For the StorageBackend path, keyword search is used.\n * For the raw Database path (legacy/direct), searchMemory() is called.\n */\nexport async function detectTopicShift(\n registryDb: Database,\n federation: Database | StorageBackend,\n params: TopicCheckParams\n): Promise<TopicCheckResult> {\n const threshold = params.threshold ?? 0.6;\n const candidates = params.candidates ?? 20;\n const currentProject = params.currentProject?.trim() || null;\n\n if (!params.context || params.context.trim().length === 0) {\n return {\n shifted: false,\n currentProject,\n suggestedProject: null,\n confidence: 0,\n chunkCount: 0,\n topProjects: [],\n };\n }\n\n // -------------------------------------------------------------------------\n // Run memory search across ALL projects (no project filter)\n // -------------------------------------------------------------------------\n\n let results: SearchResult[];\n\n const isBackend = (x: Database | StorageBackend): x is StorageBackend =>\n \"backendType\" in x;\n\n if (isBackend(federation)) {\n results = await federation.searchKeyword(params.context, {\n maxResults: candidates,\n });\n } else {\n results = searchMemory(federation, params.context, {\n maxResults: candidates,\n });\n }\n\n if (results.length === 0) {\n return {\n shifted: false,\n currentProject,\n suggestedProject: null,\n confidence: 0,\n chunkCount: 0,\n topProjects: [],\n };\n }\n\n // Populate project slugs from the registry\n const withSlugs = populateSlugs(results, registryDb);\n\n // -------------------------------------------------------------------------\n // Score projects by summing BM25 scores of matching chunks\n // -------------------------------------------------------------------------\n\n const projectScores = new Map<string, number>();\n\n for (const r of withSlugs) {\n const slug = r.projectSlug;\n if (!slug) continue;\n projectScores.set(slug, (projectScores.get(slug) ?? 0) + r.score);\n }\n\n if (projectScores.size === 0) {\n return {\n shifted: false,\n currentProject,\n suggestedProject: null,\n confidence: 0,\n chunkCount: withSlugs.length,\n topProjects: [],\n };\n }\n\n // Sort by total score descending\n const ranked = Array.from(projectScores.entries())\n .sort((a, b) => b[1] - a[1]);\n\n const totalScore = ranked.reduce((sum, [, s]) => sum + s, 0);\n\n // Top-3 for reporting (normalised to [0,1] fraction of total mass)\n const topProjects = ranked.slice(0, 3).map(([slug, score]) => ({\n slug,\n score: totalScore > 0 ? score / totalScore : 0,\n }));\n\n const topSlug = ranked[0][0];\n const topRawScore = ranked[0][1];\n const confidence = totalScore > 0 ? topRawScore / totalScore : 0;\n\n // -------------------------------------------------------------------------\n // Determine if a shift occurred\n // -------------------------------------------------------------------------\n\n // A shift is detected when:\n // 1. confidence >= threshold (the top project dominates)\n // 2. The top project is different from currentProject\n // 3. There is a currentProject to compare against\n // (if no current project, we still return the best match but no \"shift\")\n\n const isDifferent =\n currentProject !== null &&\n topSlug !== currentProject;\n\n const shifted = isDifferent && confidence >= threshold;\n\n return {\n shifted,\n currentProject,\n suggestedProject: topSlug,\n confidence,\n chunkCount: withSlugs.length,\n topProjects,\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA8EA,eAAsB,iBACpB,YACA,YACA,QAC2B;CAC3B,MAAM,YAAY,OAAO,aAAa;CACtC,MAAM,aAAa,OAAO,cAAc;CACxC,MAAM,iBAAiB,OAAO,gBAAgB,MAAM,IAAI;AAExD,KAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,MAAM,CAAC,WAAW,EACtD,QAAO;EACL,SAAS;EACT;EACA,kBAAkB;EAClB,YAAY;EACZ,YAAY;EACZ,aAAa,EAAE;EAChB;CAOH,IAAI;CAEJ,MAAM,aAAa,MACjB,iBAAiB;AAEnB,KAAI,UAAU,WAAW,CACvB,WAAU,MAAM,WAAW,cAAc,OAAO,SAAS,EACvD,YAAY,YACb,CAAC;KAEF,WAAU,aAAa,YAAY,OAAO,SAAS,EACjD,YAAY,YACb,CAAC;AAGJ,KAAI,QAAQ,WAAW,EACrB,QAAO;EACL,SAAS;EACT;EACA,kBAAkB;EAClB,YAAY;EACZ,YAAY;EACZ,aAAa,EAAE;EAChB;CAIH,MAAM,YAAY,cAAc,SAAS,WAAW;CAMpD,MAAM,gCAAgB,IAAI,KAAqB;AAE/C,MAAK,MAAM,KAAK,WAAW;EACzB,MAAM,OAAO,EAAE;AACf,MAAI,CAAC,KAAM;AACX,gBAAc,IAAI,OAAO,cAAc,IAAI,KAAK,IAAI,KAAK,EAAE,MAAM;;AAGnE,KAAI,cAAc,SAAS,EACzB,QAAO;EACL,SAAS;EACT;EACA,kBAAkB;EAClB,YAAY;EACZ,YAAY,UAAU;EACtB,aAAa,EAAE;EAChB;CAIH,MAAM,SAAS,MAAM,KAAK,cAAc,SAAS,CAAC,CAC/C,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG;CAE9B,MAAM,aAAa,OAAO,QAAQ,KAAK,GAAG,OAAO,MAAM,GAAG,EAAE;CAG5D,MAAM,cAAc,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,YAAY;EAC7D;EACA,OAAO,aAAa,IAAI,QAAQ,aAAa;EAC9C,EAAE;CAEH,MAAM,UAAU,OAAO,GAAG;CAC1B,MAAM,cAAc,OAAO,GAAG;CAC9B,MAAM,aAAa,aAAa,IAAI,cAAc,aAAa;AAkB/D,QAAO;EACL,SANA,mBAAmB,QACnB,YAAY,kBAEiB,cAAc;EAI3C;EACA,kBAAkB;EAClB;EACA,YAAY,UAAU;EACtB;EACD"}
|
|
@@ -88,4 +88,4 @@ function cosineSimilarity(a, b) {
|
|
|
88
88
|
|
|
89
89
|
//#endregion
|
|
90
90
|
export { generateEmbedding as a, embeddings_exports as i, cosineSimilarity as n, deserializeEmbedding as r, configureEmbeddingModel as t };
|
|
91
|
-
//# sourceMappingURL=embeddings-
|
|
91
|
+
//# sourceMappingURL=embeddings-BJPOcbik.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embeddings-
|
|
1
|
+
{"version":3,"file":"embeddings-BJPOcbik.mjs","names":[],"sources":["../src/memory/embeddings.ts"],"sourcesContent":["/**\n * Embedding generation for the PAI federation memory engine (Phase 2.5).\n *\n * Uses @huggingface/transformers with the Snowflake/snowflake-arctic-embed-m-v1.5 model\n * (768 dims, q8 quantization, MTEB strong retrieval quality).\n *\n * The model uses CLS pooling (first token) — NOT mean pooling.\n * For retrieval, queries require a prefix: \"Represent this sentence for searching relevant passages: \"\n * Documents should be embedded WITHOUT a prefix.\n *\n * The pipeline is a lazy singleton — loaded on first call, reused thereafter.\n * This avoids loading the heavy ML model on every CLI invocation.\n */\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nexport const EMBEDDING_DIM = 768;\nconst DEFAULT_EMBEDDING_MODEL = \"Snowflake/snowflake-arctic-embed-m-v1.5\";\n\n/** Query prefix required by Snowflake Arctic Embed for retrieval tasks. */\nconst QUERY_PREFIX = \"Represent this sentence for searching relevant passages: \";\n\n// ---------------------------------------------------------------------------\n// Lazy pipeline singleton\n// ---------------------------------------------------------------------------\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet _embeddingPipeline: any = null;\nlet _currentModel: string | null = null;\n\n/**\n * Configure the embedding model to use.\n * Must be called before the first generateEmbedding() call.\n * If the pipeline is already loaded with a different model, it will be reloaded.\n *\n * @param model HuggingFace model ID (e.g. \"Snowflake/snowflake-arctic-embed-m-v1.5\").\n * Pass undefined or empty string to use the default model.\n */\nexport function configureEmbeddingModel(model?: string): void {\n const resolved = model?.trim() || DEFAULT_EMBEDDING_MODEL;\n if (_currentModel !== null && _currentModel !== resolved) {\n // Model changed — force reload on next call\n _embeddingPipeline = null;\n }\n _currentModel = resolved;\n}\n\nasync function getEmbedder() {\n const model = _currentModel ?? DEFAULT_EMBEDDING_MODEL;\n if (!_embeddingPipeline) {\n // Dynamic import to avoid loading the ML runtime on startup\n const { pipeline } = await import(\"@huggingface/transformers\");\n _embeddingPipeline = await pipeline(\n \"feature-extraction\",\n model,\n { dtype: \"q8\" },\n );\n }\n return _embeddingPipeline;\n}\n\n// ---------------------------------------------------------------------------\n// Embedding generation\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a normalized 768-dim embedding for the given text.\n *\n * Uses CLS pooling (first token) and L2 normalization (cosine similarity ready).\n *\n * @param text The text to embed.\n * @param isQuery If true, prepend the Snowflake query prefix. Use for search queries.\n * Documents should be embedded without the prefix (default: false).\n */\nexport async function generateEmbedding(text: string, isQuery: boolean = false): Promise<Float32Array> {\n const prefix = isQuery ? QUERY_PREFIX : \"\";\n const input = prefix + text;\n const extractor = await getEmbedder();\n // Snowflake Arctic Embed uses CLS pooling (first token), not mean pooling\n const output = await extractor(input, { pooling: \"cls\", normalize: true });\n return new Float32Array(output.data);\n}\n\n// ---------------------------------------------------------------------------\n// Serialization helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Serialize a Float32Array to a Buffer for storage in a SQLite BLOB column.\n */\nexport function serializeEmbedding(vec: Float32Array): Buffer {\n return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);\n}\n\n/**\n * Deserialize a Buffer (from a SQLite BLOB column) back into a Float32Array.\n */\nexport function deserializeEmbedding(blob: Buffer): Float32Array {\n return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);\n}\n\n// ---------------------------------------------------------------------------\n// Similarity computation\n// ---------------------------------------------------------------------------\n\n/**\n * Compute cosine similarity between two normalized embedding vectors.\n *\n * Since both vectors are already L2-normalized by the embedding model,\n * cosine similarity reduces to a dot product — but we compute the full\n * formula for correctness when embeddings may not be pre-normalized.\n *\n * Returns a value in [-1, 1] where 1 = identical.\n */\nexport function cosineSimilarity(a: Float32Array, b: Float32Array): number {\n let dot = 0;\n let normA = 0;\n let normB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n normA += a[i] * a[i];\n normB += b[i] * b[i];\n }\n const denom = Math.sqrt(normA) * Math.sqrt(normB);\n if (denom === 0) return 0;\n return dot / denom;\n}\n"],"mappings":";;;;;;;;;;AAmBA,MAAM,0BAA0B;;AAGhC,MAAM,eAAe;AAOrB,IAAI,qBAA0B;AAC9B,IAAI,gBAA+B;;;;;;;;;AAUnC,SAAgB,wBAAwB,OAAsB;CAC5D,MAAM,WAAW,OAAO,MAAM,IAAI;AAClC,KAAI,kBAAkB,QAAQ,kBAAkB,SAE9C,sBAAqB;AAEvB,iBAAgB;;AAGlB,eAAe,cAAc;CAC3B,MAAM,QAAQ,iBAAiB;AAC/B,KAAI,CAAC,oBAAoB;EAEvB,MAAM,EAAE,aAAa,MAAM,OAAO;AAClC,uBAAqB,MAAM,SACzB,sBACA,OACA,EAAE,OAAO,MAAM,CAChB;;AAEH,QAAO;;;;;;;;;;;AAgBT,eAAsB,kBAAkB,MAAc,UAAmB,OAA8B;CAErG,MAAM,SADS,UAAU,eAAe,MACjB;CAGvB,MAAM,SAAS,OAFG,MAAM,aAAa,EAEN,OAAO;EAAE,SAAS;EAAO,WAAW;EAAM,CAAC;AAC1E,QAAO,IAAI,aAAa,OAAO,KAAK;;;;;AAUtC,SAAgB,mBAAmB,KAA2B;AAC5D,QAAO,OAAO,KAAK,IAAI,QAAQ,IAAI,YAAY,IAAI,WAAW;;;;;AAMhE,SAAgB,qBAAqB,MAA4B;AAC/D,QAAO,IAAI,aAAa,KAAK,QAAQ,KAAK,YAAY,KAAK,aAAa,EAAE;;;;;;;;;;;AAgB5E,SAAgB,iBAAiB,GAAiB,GAAyB;CACzE,IAAI,MAAM;CACV,IAAI,QAAQ;CACZ,IAAI,QAAQ;AACZ,MAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,SAAO,EAAE,KAAK,EAAE;AAChB,WAAS,EAAE,KAAK,EAAE;AAClB,WAAS,EAAE,KAAK,EAAE;;CAEpB,MAAM,QAAQ,KAAK,KAAK,MAAM,GAAG,KAAK,KAAK,MAAM;AACjD,KAAI,UAAU,EAAG,QAAO;AACxB,QAAO,MAAM"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//#region src/storage/factory.ts
|
|
2
|
+
/** Backoff schedule (ms) for the bounded CLI retry path. */
|
|
3
|
+
const CLI_RETRY_DELAYS_MS = [
|
|
4
|
+
500,
|
|
5
|
+
1e3,
|
|
6
|
+
2e3
|
|
7
|
+
];
|
|
8
|
+
/** Backoff cap (ms) for the daemon's infinite retry path. */
|
|
9
|
+
const DAEMON_RETRY_CAP_MS = 15e3;
|
|
10
|
+
/**
|
|
11
|
+
* Create and return the configured StorageBackend.
|
|
12
|
+
*
|
|
13
|
+
* Auto-behaviour:
|
|
14
|
+
* - storageBackend = "sqlite" → SQLiteBackend always
|
|
15
|
+
* - storageBackend = "postgres" → PostgresBackend (retried; never falls back)
|
|
16
|
+
*/
|
|
17
|
+
async function createStorageBackend(config, opts = {}) {
|
|
18
|
+
if (config.storageBackend === "postgres") return await connectPostgres(config, opts.waitForPostgres ?? false);
|
|
19
|
+
return createSQLiteBackend();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Attempt a single Postgres connection (ensure DB + test). Returns the live
|
|
23
|
+
* backend on success, or an error string describing why it failed.
|
|
24
|
+
*/
|
|
25
|
+
async function attemptPostgres(config) {
|
|
26
|
+
const { PostgresBackend } = await import("./postgres-B451Hnkm.mjs");
|
|
27
|
+
const pgConfig = config.postgres ?? {};
|
|
28
|
+
let backend = null;
|
|
29
|
+
try {
|
|
30
|
+
await PostgresBackend.ensureDatabase(pgConfig);
|
|
31
|
+
backend = new PostgresBackend(pgConfig);
|
|
32
|
+
const err = await backend.testConnection();
|
|
33
|
+
if (err) {
|
|
34
|
+
await backend.close().catch(() => {});
|
|
35
|
+
return { error: err };
|
|
36
|
+
}
|
|
37
|
+
return { backend };
|
|
38
|
+
} catch (e) {
|
|
39
|
+
if (backend) await backend.close().catch(() => {});
|
|
40
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function connectPostgres(config, waitForever) {
|
|
44
|
+
let attempt = 0;
|
|
45
|
+
let lastError = "unknown error";
|
|
46
|
+
while (true) {
|
|
47
|
+
attempt++;
|
|
48
|
+
const result = await attemptPostgres(config);
|
|
49
|
+
if ("backend" in result) {
|
|
50
|
+
if (attempt > 1) process.stderr.write(`[pai-daemon] Connected to PostgreSQL backend (after ${attempt} attempts).\n`);
|
|
51
|
+
else process.stderr.write("[pai-daemon] Connected to PostgreSQL backend.\n");
|
|
52
|
+
return result.backend;
|
|
53
|
+
}
|
|
54
|
+
lastError = result.error;
|
|
55
|
+
if (!waitForever && attempt > CLI_RETRY_DELAYS_MS.length) throw new Error(`Postgres backend unreachable after ${attempt} attempts: ${lastError}. Is Docker Desktop / Postgres running? Refusing to fall back to SQLite (would split the corpus). Start Postgres and retry.`);
|
|
56
|
+
const delayMs = waitForever ? Math.min(DAEMON_RETRY_CAP_MS, 1e3 * 2 ** Math.min(attempt - 1, 4)) : CLI_RETRY_DELAYS_MS[attempt - 1];
|
|
57
|
+
process.stderr.write(`[pai-daemon] Postgres unavailable (${lastError}). Retry ${attempt}${waitForever ? "" : `/${CLI_RETRY_DELAYS_MS.length + 1}`} in ${delayMs}ms...\n`);
|
|
58
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function createSQLiteBackend() {
|
|
62
|
+
const { openFederation } = await import("./db-CmYbAVCD.mjs").then((n) => n.t);
|
|
63
|
+
const { SQLiteBackend } = await import("./sqlite-DQpY1Esi.mjs");
|
|
64
|
+
return new SQLiteBackend(openFederation());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { createStorageBackend as t };
|
|
69
|
+
//# sourceMappingURL=factory-Q88X1bAN.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory-Q88X1bAN.mjs","names":[],"sources":["../src/storage/factory.ts"],"sourcesContent":["/**\n * Storage backend factory.\n *\n * Reads the daemon config and returns the appropriate StorageBackend.\n *\n * When Postgres is the configured backend we NEVER silently fall back to\n * SQLite — doing so would split the corpus across two databases. Instead:\n * - Daemon (waitForPostgres: true) retries Postgres forever with capped\n * backoff until it comes up (handles the boot race where launchd starts\n * the daemon before Docker Desktop / Postgres is ready).\n * - CLI / one-shot callers (default) retry a few times, then throw a clear\n * error rather than returning a wrong/empty SQLite database.\n */\n\nimport type { PaiDaemonConfig } from \"../daemon/config.js\";\nimport type { StorageBackend } from \"./interface.js\";\n\nexport interface StorageBackendOptions {\n /**\n * When true, retry Postgres indefinitely instead of giving up. Used by the\n * long-lived daemon so a not-yet-ready Postgres at boot is tolerated.\n * Defaults to false (one-shot CLI behaviour: bounded retries, then throw).\n */\n waitForPostgres?: boolean;\n}\n\n/** Backoff schedule (ms) for the bounded CLI retry path. */\nconst CLI_RETRY_DELAYS_MS = [500, 1_000, 2_000];\n\n/** Backoff cap (ms) for the daemon's infinite retry path. */\nconst DAEMON_RETRY_CAP_MS = 15_000;\n\n/**\n * Create and return the configured StorageBackend.\n *\n * Auto-behaviour:\n * - storageBackend = \"sqlite\" → SQLiteBackend always\n * - storageBackend = \"postgres\" → PostgresBackend (retried; never falls back)\n */\nexport async function createStorageBackend(\n config: PaiDaemonConfig,\n opts: StorageBackendOptions = {}\n): Promise<StorageBackend> {\n if (config.storageBackend === \"postgres\") {\n return await connectPostgres(config, opts.waitForPostgres ?? false);\n }\n\n // Default: SQLite\n return createSQLiteBackend();\n}\n\n/**\n * Attempt a single Postgres connection (ensure DB + test). Returns the live\n * backend on success, or an error string describing why it failed.\n */\nasync function attemptPostgres(\n config: PaiDaemonConfig\n): Promise<{ backend: StorageBackend } | { error: string }> {\n const { PostgresBackend } = await import(\"./postgres.js\");\n const pgConfig = config.postgres ?? {};\n\n let backend: InstanceType<typeof PostgresBackend> | null = null;\n try {\n // Ensure the per-user database exists and has the schema applied.\n await PostgresBackend.ensureDatabase(pgConfig);\n\n backend = new PostgresBackend(pgConfig);\n const err = await backend.testConnection();\n if (err) {\n await backend.close().catch(() => {});\n return { error: err };\n }\n return { backend };\n } catch (e) {\n if (backend) await backend.close().catch(() => {});\n return { error: e instanceof Error ? e.message : String(e) };\n }\n}\n\nasync function connectPostgres(\n config: PaiDaemonConfig,\n waitForever: boolean\n): Promise<StorageBackend> {\n let attempt = 0;\n let lastError = \"unknown error\";\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n attempt++;\n const result = await attemptPostgres(config);\n if (\"backend\" in result) {\n if (attempt > 1) {\n process.stderr.write(\n `[pai-daemon] Connected to PostgreSQL backend (after ${attempt} attempts).\\n`\n );\n } else {\n process.stderr.write(\"[pai-daemon] Connected to PostgreSQL backend.\\n\");\n }\n return result.backend;\n }\n\n lastError = result.error;\n\n if (!waitForever && attempt > CLI_RETRY_DELAYS_MS.length) {\n // Bounded CLI path exhausted — fail loudly, never silently use SQLite.\n throw new Error(\n `Postgres backend unreachable after ${attempt} attempts: ${lastError}. ` +\n `Is Docker Desktop / Postgres running? Refusing to fall back to SQLite ` +\n `(would split the corpus). Start Postgres and retry.`\n );\n }\n\n const delayMs = waitForever\n ? Math.min(DAEMON_RETRY_CAP_MS, 1_000 * 2 ** Math.min(attempt - 1, 4))\n : CLI_RETRY_DELAYS_MS[attempt - 1];\n\n process.stderr.write(\n `[pai-daemon] Postgres unavailable (${lastError}). ` +\n `Retry ${attempt}${waitForever ? \"\" : `/${CLI_RETRY_DELAYS_MS.length + 1}`} ` +\n `in ${delayMs}ms...\\n`\n );\n await new Promise((r) => setTimeout(r, delayMs));\n }\n}\n\nasync function createSQLiteBackend(): Promise<StorageBackend> {\n const { openFederation } = await import(\"../memory/db.js\");\n const { SQLiteBackend } = await import(\"./sqlite.js\");\n const db = openFederation();\n return new SQLiteBackend(db);\n}\n"],"mappings":";;AA2BA,MAAM,sBAAsB;CAAC;CAAK;CAAO;CAAM;;AAG/C,MAAM,sBAAsB;;;;;;;;AAS5B,eAAsB,qBACpB,QACA,OAA8B,EAAE,EACP;AACzB,KAAI,OAAO,mBAAmB,WAC5B,QAAO,MAAM,gBAAgB,QAAQ,KAAK,mBAAmB,MAAM;AAIrE,QAAO,qBAAqB;;;;;;AAO9B,eAAe,gBACb,QAC0D;CAC1D,MAAM,EAAE,oBAAoB,MAAM,OAAO;CACzC,MAAM,WAAW,OAAO,YAAY,EAAE;CAEtC,IAAI,UAAuD;AAC3D,KAAI;AAEF,QAAM,gBAAgB,eAAe,SAAS;AAE9C,YAAU,IAAI,gBAAgB,SAAS;EACvC,MAAM,MAAM,MAAM,QAAQ,gBAAgB;AAC1C,MAAI,KAAK;AACP,SAAM,QAAQ,OAAO,CAAC,YAAY,GAAG;AACrC,UAAO,EAAE,OAAO,KAAK;;AAEvB,SAAO,EAAE,SAAS;UACX,GAAG;AACV,MAAI,QAAS,OAAM,QAAQ,OAAO,CAAC,YAAY,GAAG;AAClD,SAAO,EAAE,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,EAAE;;;AAIhE,eAAe,gBACb,QACA,aACyB;CACzB,IAAI,UAAU;CACd,IAAI,YAAY;AAGhB,QAAO,MAAM;AACX;EACA,MAAM,SAAS,MAAM,gBAAgB,OAAO;AAC5C,MAAI,aAAa,QAAQ;AACvB,OAAI,UAAU,EACZ,SAAQ,OAAO,MACb,uDAAuD,QAAQ,eAChE;OAED,SAAQ,OAAO,MAAM,kDAAkD;AAEzE,UAAO,OAAO;;AAGhB,cAAY,OAAO;AAEnB,MAAI,CAAC,eAAe,UAAU,oBAAoB,OAEhD,OAAM,IAAI,MACR,sCAAsC,QAAQ,aAAa,UAAU,6HAGtE;EAGH,MAAM,UAAU,cACZ,KAAK,IAAI,qBAAqB,MAAQ,KAAK,KAAK,IAAI,UAAU,GAAG,EAAE,CAAC,GACpE,oBAAoB,UAAU;AAElC,UAAQ,OAAO,MACb,sCAAsC,UAAU,WACrC,UAAU,cAAc,KAAK,IAAI,oBAAoB,SAAS,IAAI,MACrE,QAAQ,SACjB;AACD,QAAM,IAAI,SAAS,MAAM,WAAW,GAAG,QAAQ,CAAC;;;AAIpD,eAAe,sBAA+C;CAC5D,MAAM,EAAE,mBAAmB,MAAM,OAAO;CACxC,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,QAAO,IAAI,cADA,gBAAgB,CACC"}
|
|
@@ -425,4 +425,4 @@ const INDEX_YIELD_EVERY = 10;
|
|
|
425
425
|
|
|
426
426
|
//#endregion
|
|
427
427
|
export { parseSessionTitleChunk as a, yieldToEventLoop as c, sha256 as d, sha256File as f, isPathTooBroadForContentScan as i, chunkMarkdown as l, chunkId as n, walkContentFiles as o, detectTier as r, walkMdFiles as s, INDEX_YIELD_EVERY as t, estimateTokens as u };
|
|
428
|
-
//# sourceMappingURL=helpers-
|
|
428
|
+
//# sourceMappingURL=helpers-IjZkXBhj.mjs.map
|