@tekmidian/pai 0.3.2 → 0.5.0

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.
Files changed (101) hide show
  1. package/ARCHITECTURE.md +16 -10
  2. package/README.md +46 -6
  3. package/dist/{auto-route-JjW3f7pV.mjs → auto-route-B5MSUJZK.mjs} +3 -3
  4. package/dist/{auto-route-JjW3f7pV.mjs.map → auto-route-B5MSUJZK.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +313 -43
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/{config-DELNqq3Z.mjs → config-B4brrHHE.mjs} +1 -1
  8. package/dist/{config-DELNqq3Z.mjs.map → config-B4brrHHE.mjs.map} +1 -1
  9. package/dist/daemon/index.mjs +7 -7
  10. package/dist/daemon-mcp/index.mjs +11 -4
  11. package/dist/daemon-mcp/index.mjs.map +1 -1
  12. package/dist/{daemon-CeTX4NpF.mjs → daemon-s868Paua.mjs} +12 -12
  13. package/dist/{daemon-CeTX4NpF.mjs.map → daemon-s868Paua.mjs.map} +1 -1
  14. package/dist/{detect-D7gPV3fQ.mjs → detect-CdaA48EI.mjs} +1 -1
  15. package/dist/{detect-D7gPV3fQ.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  16. package/dist/{detector-cYYhK2Mi.mjs → detector-Bp-2SM3x.mjs} +2 -2
  17. package/dist/{detector-cYYhK2Mi.mjs.map → detector-Bp-2SM3x.mjs.map} +1 -1
  18. package/dist/{factory-DZLvRf4m.mjs → factory-CeXQzlwn.mjs} +3 -3
  19. package/dist/{factory-DZLvRf4m.mjs.map → factory-CeXQzlwn.mjs.map} +1 -1
  20. package/dist/hooks/capture-all-events.mjs +238 -0
  21. package/dist/hooks/capture-all-events.mjs.map +7 -0
  22. package/dist/hooks/capture-session-summary.mjs +198 -0
  23. package/dist/hooks/capture-session-summary.mjs.map +7 -0
  24. package/dist/hooks/capture-tool-output.mjs +105 -0
  25. package/dist/hooks/capture-tool-output.mjs.map +7 -0
  26. package/dist/hooks/cleanup-session-files.mjs +129 -0
  27. package/dist/hooks/cleanup-session-files.mjs.map +7 -0
  28. package/dist/hooks/context-compression-hook.mjs +283 -0
  29. package/dist/hooks/context-compression-hook.mjs.map +7 -0
  30. package/dist/hooks/initialize-session.mjs +206 -0
  31. package/dist/hooks/initialize-session.mjs.map +7 -0
  32. package/dist/hooks/load-core-context.mjs +110 -0
  33. package/dist/hooks/load-core-context.mjs.map +7 -0
  34. package/dist/hooks/load-project-context.mjs +548 -0
  35. package/dist/hooks/load-project-context.mjs.map +7 -0
  36. package/dist/hooks/security-validator.mjs +159 -0
  37. package/dist/hooks/security-validator.mjs.map +7 -0
  38. package/dist/hooks/stop-hook.mjs +625 -0
  39. package/dist/hooks/stop-hook.mjs.map +7 -0
  40. package/dist/hooks/subagent-stop-hook.mjs +152 -0
  41. package/dist/hooks/subagent-stop-hook.mjs.map +7 -0
  42. package/dist/hooks/sync-todo-to-md.mjs +322 -0
  43. package/dist/hooks/sync-todo-to-md.mjs.map +7 -0
  44. package/dist/hooks/update-tab-on-action.mjs +90 -0
  45. package/dist/hooks/update-tab-on-action.mjs.map +7 -0
  46. package/dist/hooks/update-tab-titles.mjs +55 -0
  47. package/dist/hooks/update-tab-titles.mjs.map +7 -0
  48. package/dist/index.d.mts +29 -1
  49. package/dist/index.d.mts.map +1 -1
  50. package/dist/index.mjs +4 -3
  51. package/dist/{indexer-backend-BHztlJJg.mjs → indexer-backend-DQO-FqAI.mjs} +1 -1
  52. package/dist/{indexer-backend-BHztlJJg.mjs.map → indexer-backend-DQO-FqAI.mjs.map} +1 -1
  53. package/dist/{ipc-client-CLt2fNlC.mjs → ipc-client-CgSpwHDC.mjs} +1 -1
  54. package/dist/{ipc-client-CLt2fNlC.mjs.map → ipc-client-CgSpwHDC.mjs.map} +1 -1
  55. package/dist/mcp/index.mjs +15 -5
  56. package/dist/mcp/index.mjs.map +1 -1
  57. package/dist/{postgres-CRBe30Ag.mjs → postgres-CIxeqf_n.mjs} +1 -1
  58. package/dist/{postgres-CRBe30Ag.mjs.map → postgres-CIxeqf_n.mjs.map} +1 -1
  59. package/dist/reranker-D7bRAHi6.mjs +71 -0
  60. package/dist/reranker-D7bRAHi6.mjs.map +1 -0
  61. package/dist/{schemas-BY3Pjvje.mjs → schemas-BFIgGntb.mjs} +1 -1
  62. package/dist/{schemas-BY3Pjvje.mjs.map → schemas-BFIgGntb.mjs.map} +1 -1
  63. package/dist/{search-GK0ibTJy.mjs → search-_oHfguA5.mjs} +47 -4
  64. package/dist/search-_oHfguA5.mjs.map +1 -0
  65. package/dist/{sqlite-RyR8Up1v.mjs → sqlite-CymLKiDE.mjs} +2 -2
  66. package/dist/{sqlite-RyR8Up1v.mjs.map → sqlite-CymLKiDE.mjs.map} +1 -1
  67. package/dist/{tools-CUg0Lyg-.mjs → tools-Dx7GjOHd.mjs} +23 -14
  68. package/dist/tools-Dx7GjOHd.mjs.map +1 -0
  69. package/dist/{vault-indexer-Bo2aPSzP.mjs → vault-indexer-DXWs9pDn.mjs} +1 -1
  70. package/dist/{vault-indexer-Bo2aPSzP.mjs.map → vault-indexer-DXWs9pDn.mjs.map} +1 -1
  71. package/dist/{zettelkasten-Co-w0XSZ.mjs → zettelkasten-e-a4rW_6.mjs} +2 -2
  72. package/dist/{zettelkasten-Co-w0XSZ.mjs.map → zettelkasten-e-a4rW_6.mjs.map} +1 -1
  73. package/package.json +4 -2
  74. package/scripts/build-hooks.mjs +51 -0
  75. package/src/hooks/ts/capture-all-events.ts +179 -0
  76. package/src/hooks/ts/lib/detect-environment.ts +53 -0
  77. package/src/hooks/ts/lib/metadata-extraction.ts +144 -0
  78. package/src/hooks/ts/lib/pai-paths.ts +124 -0
  79. package/src/hooks/ts/lib/project-utils.ts +914 -0
  80. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +78 -0
  81. package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +230 -0
  82. package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +145 -0
  83. package/src/hooks/ts/pre-compact/context-compression-hook.ts +155 -0
  84. package/src/hooks/ts/pre-tool-use/security-validator.ts +258 -0
  85. package/src/hooks/ts/session-end/capture-session-summary.ts +185 -0
  86. package/src/hooks/ts/session-start/initialize-session.ts +155 -0
  87. package/src/hooks/ts/session-start/load-core-context.ts +104 -0
  88. package/src/hooks/ts/session-start/load-project-context.ts +394 -0
  89. package/src/hooks/ts/stop/stop-hook.ts +407 -0
  90. package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +212 -0
  91. package/src/hooks/ts/user-prompt/cleanup-session-files.ts +45 -0
  92. package/src/hooks/ts/user-prompt/update-tab-titles.ts +88 -0
  93. package/tab-color-command.sh +24 -0
  94. package/templates/skills/createskill-skill.template.md +78 -0
  95. package/templates/skills/history-system.template.md +371 -0
  96. package/templates/skills/hook-system.template.md +913 -0
  97. package/templates/skills/sessions-skill.template.md +102 -0
  98. package/templates/skills/skill-system.template.md +214 -0
  99. package/templates/skills/terminal-tabs.template.md +120 -0
  100. package/dist/search-GK0ibTJy.mjs.map +0 -1
  101. package/dist/tools-CUg0Lyg-.mjs.map +0 -1
@@ -150,4 +150,4 @@ function ensureConfigDir() {
150
150
 
151
151
  //#endregion
152
152
  export { expandHome as a, ensureConfigDir as i, CONFIG_FILE as n, loadConfig as o, config_exports as r, DEFAULT_NOTIFICATION_CONFIG as s, CONFIG_DIR as t };
153
- //# sourceMappingURL=config-DELNqq3Z.mjs.map
153
+ //# sourceMappingURL=config-B4brrHHE.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"config-DELNqq3Z.mjs","names":[],"sources":["../src/notifications/types.ts","../src/daemon/config.ts"],"sourcesContent":["/**\n * types.ts — Unified Notification Framework type definitions\n *\n * Defines the channel registry, event routing, and configuration schema\n * for PAI's notification subsystem.\n */\n\n// ---------------------------------------------------------------------------\n// Channel identifiers\n// ---------------------------------------------------------------------------\n\nexport type ChannelId = \"ntfy\" | \"whatsapp\" | \"macos\" | \"voice\" | \"cli\";\n\n// ---------------------------------------------------------------------------\n// Notification event types\n// ---------------------------------------------------------------------------\n\n/**\n * The semantic type of a notification event.\n * Used to route events to the appropriate channels.\n */\nexport type NotificationEvent =\n | \"error\"\n | \"progress\"\n | \"completion\"\n | \"info\"\n | \"debug\";\n\n// ---------------------------------------------------------------------------\n// Notification mode\n// ---------------------------------------------------------------------------\n\n/**\n * The current notification mode.\n *\n * - \"auto\" — Use the per-event routing table (default)\n * - \"voice\" — All events go to voice (WhatsApp TTS)\n * - \"whatsapp\" — All events go to WhatsApp text\n * - \"ntfy\" — All events go to ntfy.sh\n * - \"macos\" — All events go to macOS notifications\n * - \"cli\" — All events go to CLI stdout only\n * - \"off\" — Suppress all notifications\n */\nexport type NotificationMode =\n | \"auto\"\n | \"voice\"\n | \"whatsapp\"\n | \"ntfy\"\n | \"macos\"\n | \"cli\"\n | \"off\";\n\n// ---------------------------------------------------------------------------\n// Per-channel configuration\n// ---------------------------------------------------------------------------\n\nexport interface NtfyChannelConfig {\n enabled: boolean;\n /** ntfy.sh topic URL, e.g. \"https://ntfy.sh/my-topic\" */\n url?: string;\n /** ntfy priority: min | low | default | high | urgent */\n priority?: \"min\" | \"low\" | \"default\" | \"high\" | \"urgent\";\n}\n\nexport interface WhatsAppChannelConfig {\n enabled: boolean;\n /** Optional recipient (phone, JID, or contact name). Omit for self-chat. */\n recipient?: string;\n}\n\nexport interface MacOsChannelConfig {\n enabled: boolean;\n}\n\nexport interface VoiceChannelConfig {\n enabled: boolean;\n /** Kokoro voice name, e.g. \"bm_george\", \"af_bella\". Default: \"bm_george\" */\n voiceName?: string;\n}\n\nexport interface CliChannelConfig {\n enabled: boolean;\n}\n\nexport interface ChannelConfigs {\n ntfy: NtfyChannelConfig;\n whatsapp: WhatsAppChannelConfig;\n macos: MacOsChannelConfig;\n voice: VoiceChannelConfig;\n cli: CliChannelConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Routing table\n// ---------------------------------------------------------------------------\n\n/**\n * Maps each event type to the ordered list of channels that should receive it.\n * Only channels that are enabled in `channels` and present in this list are used.\n */\nexport type RoutingTable = {\n [K in NotificationEvent]: ChannelId[];\n};\n\nexport const DEFAULT_ROUTING: RoutingTable = {\n error: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n completion: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n info: [\"cli\"],\n progress: [\"cli\"],\n debug: [],\n};\n\n// ---------------------------------------------------------------------------\n// Top-level notification config (embedded in PaiDaemonConfig)\n// ---------------------------------------------------------------------------\n\nexport interface NotificationConfig {\n /** Current routing mode. Default: \"auto\" */\n mode: NotificationMode;\n /** Per-channel configuration */\n channels: ChannelConfigs;\n /** Event → channel routing (used in \"auto\" mode) */\n routing: RoutingTable;\n}\n\nexport const DEFAULT_CHANNELS: ChannelConfigs = {\n ntfy: {\n enabled: false,\n url: undefined,\n priority: \"default\",\n },\n whatsapp: {\n enabled: true,\n recipient: undefined,\n },\n macos: {\n enabled: true,\n },\n voice: {\n enabled: false,\n voiceName: \"bm_george\",\n },\n cli: {\n enabled: true,\n },\n};\n\nexport const DEFAULT_NOTIFICATION_CONFIG: NotificationConfig = {\n mode: \"auto\",\n channels: DEFAULT_CHANNELS,\n routing: DEFAULT_ROUTING,\n};\n\n// ---------------------------------------------------------------------------\n// Notification payload\n// ---------------------------------------------------------------------------\n\nexport interface NotificationPayload {\n /** Semantic event type — used for routing */\n event: NotificationEvent;\n /** The notification message body */\n message: string;\n /** Optional title (used by macOS, ntfy) */\n title?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider interface\n// ---------------------------------------------------------------------------\n\nexport interface NotificationProvider {\n readonly channelId: ChannelId;\n /**\n * Send a notification.\n * Returns true on success, false on failure (failure is non-fatal).\n */\n send(payload: NotificationPayload, config: NotificationConfig): Promise<boolean>;\n}\n\n// ---------------------------------------------------------------------------\n// Send result\n// ---------------------------------------------------------------------------\n\nexport interface SendResult {\n channelsAttempted: ChannelId[];\n channelsSucceeded: ChannelId[];\n channelsFailed: ChannelId[];\n mode: NotificationMode;\n}\n","/**\n * config.ts — Configuration loader for PAI Daemon\n *\n * Loads config from ~/.config/pai/config.json (XDG convention).\n * Deep-merges with defaults so partial configs work fine.\n * Expands ~ in path values at runtime.\n */\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { NotificationConfig } from \"../notifications/types.js\";\nimport { DEFAULT_NOTIFICATION_CONFIG } from \"../notifications/types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PostgresConfig {\n /** Connection string — if set, overrides individual host/port/etc. fields */\n connectionString?: string;\n /** Postgres host (default: \"localhost\") */\n host?: string;\n /** Postgres port (default: 5432) */\n port?: number;\n /** Postgres database name (default: \"pai\") */\n database?: string;\n /** Postgres user (default: \"pai\") */\n user?: string;\n /** Postgres password (default: \"pai\") */\n password?: string;\n /** Maximum pool connections (default: 5) */\n maxConnections?: number;\n /** Connection timeout in ms (default: 5000) */\n connectionTimeoutMs?: number;\n}\n\nexport interface PaiDaemonConfig {\n /** Unix Domain Socket path for IPC */\n socketPath: string;\n\n /** Index schedule interval in seconds (default: 300 = 5 minutes) */\n indexIntervalSecs: number;\n\n /** Embedding schedule interval in seconds (default: 600 = 10 minutes) */\n embedIntervalSecs: number;\n\n /** Storage backend: \"sqlite\" (default) or \"postgres\" */\n storageBackend: \"sqlite\" | \"postgres\";\n\n /** PostgreSQL connection config (used when storageBackend = \"postgres\") */\n postgres?: PostgresConfig;\n\n /** Embedding model name (used for semantic/hybrid search) */\n embeddingModel: string;\n\n /** Log level */\n logLevel: \"debug\" | \"info\" | \"warn\" | \"error\";\n\n /** Obsidian vault root path for zettelkasten indexing. If set, vault indexing runs alongside project indexing. */\n vaultPath?: string;\n\n /** Registry project_id to use for vault chunks in memory_chunks. Default: auto-detected. */\n vaultProjectId?: number;\n\n /** Notification subsystem configuration */\n notifications: NotificationConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nexport const DEFAULTS: PaiDaemonConfig = {\n socketPath: \"/tmp/pai.sock\",\n indexIntervalSecs: 300,\n embedIntervalSecs: 600,\n storageBackend: \"sqlite\",\n postgres: {\n connectionString: \"postgresql://pai:pai@localhost:5432/pai\",\n maxConnections: 5,\n connectionTimeoutMs: 5000,\n },\n embeddingModel: \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n logLevel: \"info\",\n notifications: DEFAULT_NOTIFICATION_CONFIG,\n};\n\nconst CONFIG_TEMPLATE = `{\n \"socketPath\": \"/tmp/pai.sock\",\n \"indexIntervalSecs\": 300,\n \"embedIntervalSecs\": 600,\n \"storageBackend\": \"sqlite\",\n \"postgres\": {\n \"connectionString\": \"postgresql://pai:pai@localhost:5432/pai\",\n \"maxConnections\": 5,\n \"connectionTimeoutMs\": 5000\n },\n \"embeddingModel\": \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n \"logLevel\": \"info\",\n \"vaultPath\": \"\",\n \"vaultProjectId\": 0\n}\n`;\n\n// ---------------------------------------------------------------------------\n// Path helpers\n// ---------------------------------------------------------------------------\n\n/** Expand a leading ~ to the real home directory */\nexport function expandHome(p: string): string {\n if (p === \"~\" || p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) {\n return join(homedir(), p.slice(1));\n }\n return p;\n}\n\nexport const CONFIG_DIR = join(homedir(), \".config\", \"pai\");\nexport const CONFIG_FILE = join(CONFIG_DIR, \"config.json\");\n\n// ---------------------------------------------------------------------------\n// Deep merge (handles nested objects, not arrays)\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// Config loader\n// ---------------------------------------------------------------------------\n\n/**\n * Load configuration from ~/.config/pai/config.json.\n * Returns defaults merged with any values found in the file.\n */\nexport function loadConfig(): PaiDaemonConfig {\n if (!existsSync(CONFIG_FILE)) {\n return { ...DEFAULTS };\n }\n\n let raw: string;\n try {\n raw = readFileSync(CONFIG_FILE, \"utf-8\");\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not read config file at ${CONFIG_FILE}: ${e}\\n`\n );\n return { ...DEFAULTS };\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Config file is not valid JSON: ${e}\\n`\n );\n return { ...DEFAULTS };\n }\n\n return deepMerge(DEFAULTS, parsed);\n}\n\n/**\n * Ensure ~/.config/pai/ exists and write a default config.json template\n * if none exists yet. Call this only from the `serve` command.\n */\nexport function ensureConfigDir(): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n process.stderr.write(\n `[pai-daemon] Created config directory: ${CONFIG_DIR}\\n`\n );\n }\n\n if (!existsSync(CONFIG_FILE)) {\n try {\n writeFileSync(CONFIG_FILE, CONFIG_TEMPLATE, \"utf-8\");\n process.stderr.write(\n `[pai-daemon] Wrote default config to: ${CONFIG_FILE}\\n`\n );\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not write default config: ${e}\\n`\n );\n }\n }\n}\n"],"mappings":";;;;;;AAwGA,MAAa,kBAAgC;CAC3C,OAAY;EAAC;EAAY;EAAS;EAAQ;EAAM;CAChD,YAAY;EAAC;EAAY;EAAS;EAAQ;EAAM;CAChD,MAAY,CAAC,MAAM;CACnB,UAAY,CAAC,MAAM;CACnB,OAAY,EAAE;CACf;AAeD,MAAa,mBAAmC;CAC9C,MAAM;EACJ,SAAS;EACT,KAAK;EACL,UAAU;EACX;CACD,UAAU;EACR,SAAS;EACT,WAAW;EACZ;CACD,OAAO,EACL,SAAS,MACV;CACD,OAAO;EACL,SAAS;EACT,WAAW;EACZ;CACD,KAAK,EACH,SAAS,MACV;CACF;AAED,MAAa,8BAAkD;CAC7D,MAAM;CACN,UAAU;CACV,SAAS;CACV;;;;;;;;;;;;;;;;;;;AC9ED,MAAa,WAA4B;CACvC,YAAY;CACZ,mBAAmB;CACnB,mBAAmB;CACnB,gBAAgB;CAChB,UAAU;EACR,kBAAkB;EAClB,gBAAgB;EAChB,qBAAqB;EACtB;CACD,gBAAgB;CAChB,UAAU;CACV,eAAe;CAChB;AAED,MAAM,kBAAkB;;;;;;;;;;;;;;;;;AAsBxB,SAAgB,WAAW,GAAmB;AAC5C,KAAI,MAAM,OAAO,EAAE,WAAW,KAAK,IAAI,EAAE,WAAW,MAAM,CACxD,QAAO,KAAK,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC;AAEpC,QAAO;;AAGT,MAAa,aAAa,KAAK,SAAS,EAAE,WAAW,MAAM;AAC3D,MAAa,cAAc,KAAK,YAAY,cAAc;AAM1D,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,aAA8B;AAC5C,KAAI,CAAC,WAAW,YAAY,CAC1B,QAAO,EAAE,GAAG,UAAU;CAGxB,IAAI;AACJ,KAAI;AACF,QAAM,aAAa,aAAa,QAAQ;UACjC,GAAG;AACV,UAAQ,OAAO,MACb,8CAA8C,YAAY,IAAI,EAAE,IACjE;AACD,SAAO,EAAE,GAAG,UAAU;;CAGxB,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;UACjB,GAAG;AACV,UAAQ,OAAO,MACb,+CAA+C,EAAE,IAClD;AACD,SAAO,EAAE,GAAG,UAAU;;AAGxB,QAAO,UAAU,UAAU,OAAO;;;;;;AAOpC,SAAgB,kBAAwB;AACtC,KAAI,CAAC,WAAW,WAAW,EAAE;AAC3B,YAAU,YAAY,EAAE,WAAW,MAAM,CAAC;AAC1C,UAAQ,OAAO,MACb,0CAA0C,WAAW,IACtD;;AAGH,KAAI,CAAC,WAAW,YAAY,CAC1B,KAAI;AACF,gBAAc,aAAa,iBAAiB,QAAQ;AACpD,UAAQ,OAAO,MACb,yCAAyC,YAAY,IACtD;UACM,GAAG;AACV,UAAQ,OAAO,MACb,gDAAgD,EAAE,IACnD"}
1
+ {"version":3,"file":"config-B4brrHHE.mjs","names":[],"sources":["../src/notifications/types.ts","../src/daemon/config.ts"],"sourcesContent":["/**\n * types.ts — Unified Notification Framework type definitions\n *\n * Defines the channel registry, event routing, and configuration schema\n * for PAI's notification subsystem.\n */\n\n// ---------------------------------------------------------------------------\n// Channel identifiers\n// ---------------------------------------------------------------------------\n\nexport type ChannelId = \"ntfy\" | \"whatsapp\" | \"macos\" | \"voice\" | \"cli\";\n\n// ---------------------------------------------------------------------------\n// Notification event types\n// ---------------------------------------------------------------------------\n\n/**\n * The semantic type of a notification event.\n * Used to route events to the appropriate channels.\n */\nexport type NotificationEvent =\n | \"error\"\n | \"progress\"\n | \"completion\"\n | \"info\"\n | \"debug\";\n\n// ---------------------------------------------------------------------------\n// Notification mode\n// ---------------------------------------------------------------------------\n\n/**\n * The current notification mode.\n *\n * - \"auto\" — Use the per-event routing table (default)\n * - \"voice\" — All events go to voice (WhatsApp TTS)\n * - \"whatsapp\" — All events go to WhatsApp text\n * - \"ntfy\" — All events go to ntfy.sh\n * - \"macos\" — All events go to macOS notifications\n * - \"cli\" — All events go to CLI stdout only\n * - \"off\" — Suppress all notifications\n */\nexport type NotificationMode =\n | \"auto\"\n | \"voice\"\n | \"whatsapp\"\n | \"ntfy\"\n | \"macos\"\n | \"cli\"\n | \"off\";\n\n// ---------------------------------------------------------------------------\n// Per-channel configuration\n// ---------------------------------------------------------------------------\n\nexport interface NtfyChannelConfig {\n enabled: boolean;\n /** ntfy.sh topic URL, e.g. \"https://ntfy.sh/my-topic\" */\n url?: string;\n /** ntfy priority: min | low | default | high | urgent */\n priority?: \"min\" | \"low\" | \"default\" | \"high\" | \"urgent\";\n}\n\nexport interface WhatsAppChannelConfig {\n enabled: boolean;\n /** Optional recipient (phone, JID, or contact name). Omit for self-chat. */\n recipient?: string;\n}\n\nexport interface MacOsChannelConfig {\n enabled: boolean;\n}\n\nexport interface VoiceChannelConfig {\n enabled: boolean;\n /** Kokoro voice name, e.g. \"bm_george\", \"af_bella\". Default: \"bm_george\" */\n voiceName?: string;\n}\n\nexport interface CliChannelConfig {\n enabled: boolean;\n}\n\nexport interface ChannelConfigs {\n ntfy: NtfyChannelConfig;\n whatsapp: WhatsAppChannelConfig;\n macos: MacOsChannelConfig;\n voice: VoiceChannelConfig;\n cli: CliChannelConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Routing table\n// ---------------------------------------------------------------------------\n\n/**\n * Maps each event type to the ordered list of channels that should receive it.\n * Only channels that are enabled in `channels` and present in this list are used.\n */\nexport type RoutingTable = {\n [K in NotificationEvent]: ChannelId[];\n};\n\nexport const DEFAULT_ROUTING: RoutingTable = {\n error: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n completion: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n info: [\"cli\"],\n progress: [\"cli\"],\n debug: [],\n};\n\n// ---------------------------------------------------------------------------\n// Top-level notification config (embedded in PaiDaemonConfig)\n// ---------------------------------------------------------------------------\n\nexport interface NotificationConfig {\n /** Current routing mode. Default: \"auto\" */\n mode: NotificationMode;\n /** Per-channel configuration */\n channels: ChannelConfigs;\n /** Event → channel routing (used in \"auto\" mode) */\n routing: RoutingTable;\n}\n\nexport const DEFAULT_CHANNELS: ChannelConfigs = {\n ntfy: {\n enabled: false,\n url: undefined,\n priority: \"default\",\n },\n whatsapp: {\n enabled: true,\n recipient: undefined,\n },\n macos: {\n enabled: true,\n },\n voice: {\n enabled: false,\n voiceName: \"bm_george\",\n },\n cli: {\n enabled: true,\n },\n};\n\nexport const DEFAULT_NOTIFICATION_CONFIG: NotificationConfig = {\n mode: \"auto\",\n channels: DEFAULT_CHANNELS,\n routing: DEFAULT_ROUTING,\n};\n\n// ---------------------------------------------------------------------------\n// Notification payload\n// ---------------------------------------------------------------------------\n\nexport interface NotificationPayload {\n /** Semantic event type — used for routing */\n event: NotificationEvent;\n /** The notification message body */\n message: string;\n /** Optional title (used by macOS, ntfy) */\n title?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider interface\n// ---------------------------------------------------------------------------\n\nexport interface NotificationProvider {\n readonly channelId: ChannelId;\n /**\n * Send a notification.\n * Returns true on success, false on failure (failure is non-fatal).\n */\n send(payload: NotificationPayload, config: NotificationConfig): Promise<boolean>;\n}\n\n// ---------------------------------------------------------------------------\n// Send result\n// ---------------------------------------------------------------------------\n\nexport interface SendResult {\n channelsAttempted: ChannelId[];\n channelsSucceeded: ChannelId[];\n channelsFailed: ChannelId[];\n mode: NotificationMode;\n}\n","/**\n * config.ts — Configuration loader for PAI Daemon\n *\n * Loads config from ~/.config/pai/config.json (XDG convention).\n * Deep-merges with defaults so partial configs work fine.\n * Expands ~ in path values at runtime.\n */\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { NotificationConfig } from \"../notifications/types.js\";\nimport { DEFAULT_NOTIFICATION_CONFIG } from \"../notifications/types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PostgresConfig {\n /** Connection string — if set, overrides individual host/port/etc. fields */\n connectionString?: string;\n /** Postgres host (default: \"localhost\") */\n host?: string;\n /** Postgres port (default: 5432) */\n port?: number;\n /** Postgres database name (default: \"pai\") */\n database?: string;\n /** Postgres user (default: \"pai\") */\n user?: string;\n /** Postgres password (default: \"pai\") */\n password?: string;\n /** Maximum pool connections (default: 5) */\n maxConnections?: number;\n /** Connection timeout in ms (default: 5000) */\n connectionTimeoutMs?: number;\n}\n\nexport interface PaiDaemonConfig {\n /** Unix Domain Socket path for IPC */\n socketPath: string;\n\n /** Index schedule interval in seconds (default: 300 = 5 minutes) */\n indexIntervalSecs: number;\n\n /** Embedding schedule interval in seconds (default: 600 = 10 minutes) */\n embedIntervalSecs: number;\n\n /** Storage backend: \"sqlite\" (default) or \"postgres\" */\n storageBackend: \"sqlite\" | \"postgres\";\n\n /** PostgreSQL connection config (used when storageBackend = \"postgres\") */\n postgres?: PostgresConfig;\n\n /** Embedding model name (used for semantic/hybrid search) */\n embeddingModel: string;\n\n /** Log level */\n logLevel: \"debug\" | \"info\" | \"warn\" | \"error\";\n\n /** Obsidian vault root path for zettelkasten indexing. If set, vault indexing runs alongside project indexing. */\n vaultPath?: string;\n\n /** Registry project_id to use for vault chunks in memory_chunks. Default: auto-detected. */\n vaultProjectId?: number;\n\n /** Notification subsystem configuration */\n notifications: NotificationConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nexport const DEFAULTS: PaiDaemonConfig = {\n socketPath: \"/tmp/pai.sock\",\n indexIntervalSecs: 300,\n embedIntervalSecs: 600,\n storageBackend: \"sqlite\",\n postgres: {\n connectionString: \"postgresql://pai:pai@localhost:5432/pai\",\n maxConnections: 5,\n connectionTimeoutMs: 5000,\n },\n embeddingModel: \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n logLevel: \"info\",\n notifications: DEFAULT_NOTIFICATION_CONFIG,\n};\n\nconst CONFIG_TEMPLATE = `{\n \"socketPath\": \"/tmp/pai.sock\",\n \"indexIntervalSecs\": 300,\n \"embedIntervalSecs\": 600,\n \"storageBackend\": \"sqlite\",\n \"postgres\": {\n \"connectionString\": \"postgresql://pai:pai@localhost:5432/pai\",\n \"maxConnections\": 5,\n \"connectionTimeoutMs\": 5000\n },\n \"embeddingModel\": \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n \"logLevel\": \"info\",\n \"vaultPath\": \"\",\n \"vaultProjectId\": 0\n}\n`;\n\n// ---------------------------------------------------------------------------\n// Path helpers\n// ---------------------------------------------------------------------------\n\n/** Expand a leading ~ to the real home directory */\nexport function expandHome(p: string): string {\n if (p === \"~\" || p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) {\n return join(homedir(), p.slice(1));\n }\n return p;\n}\n\nexport const CONFIG_DIR = join(homedir(), \".config\", \"pai\");\nexport const CONFIG_FILE = join(CONFIG_DIR, \"config.json\");\n\n// ---------------------------------------------------------------------------\n// Deep merge (handles nested objects, not arrays)\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// Config loader\n// ---------------------------------------------------------------------------\n\n/**\n * Load configuration from ~/.config/pai/config.json.\n * Returns defaults merged with any values found in the file.\n */\nexport function loadConfig(): PaiDaemonConfig {\n if (!existsSync(CONFIG_FILE)) {\n return { ...DEFAULTS };\n }\n\n let raw: string;\n try {\n raw = readFileSync(CONFIG_FILE, \"utf-8\");\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not read config file at ${CONFIG_FILE}: ${e}\\n`\n );\n return { ...DEFAULTS };\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Config file is not valid JSON: ${e}\\n`\n );\n return { ...DEFAULTS };\n }\n\n return deepMerge(DEFAULTS, parsed);\n}\n\n/**\n * Ensure ~/.config/pai/ exists and write a default config.json template\n * if none exists yet. Call this only from the `serve` command.\n */\nexport function ensureConfigDir(): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n process.stderr.write(\n `[pai-daemon] Created config directory: ${CONFIG_DIR}\\n`\n );\n }\n\n if (!existsSync(CONFIG_FILE)) {\n try {\n writeFileSync(CONFIG_FILE, CONFIG_TEMPLATE, \"utf-8\");\n process.stderr.write(\n `[pai-daemon] Wrote default config to: ${CONFIG_FILE}\\n`\n );\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not write default config: ${e}\\n`\n );\n }\n }\n}\n"],"mappings":";;;;;;AAwGA,MAAa,kBAAgC;CAC3C,OAAY;EAAC;EAAY;EAAS;EAAQ;EAAM;CAChD,YAAY;EAAC;EAAY;EAAS;EAAQ;EAAM;CAChD,MAAY,CAAC,MAAM;CACnB,UAAY,CAAC,MAAM;CACnB,OAAY,EAAE;CACf;AAeD,MAAa,mBAAmC;CAC9C,MAAM;EACJ,SAAS;EACT,KAAK;EACL,UAAU;EACX;CACD,UAAU;EACR,SAAS;EACT,WAAW;EACZ;CACD,OAAO,EACL,SAAS,MACV;CACD,OAAO;EACL,SAAS;EACT,WAAW;EACZ;CACD,KAAK,EACH,SAAS,MACV;CACF;AAED,MAAa,8BAAkD;CAC7D,MAAM;CACN,UAAU;CACV,SAAS;CACV;;;;;;;;;;;;;;;;;;;AC9ED,MAAa,WAA4B;CACvC,YAAY;CACZ,mBAAmB;CACnB,mBAAmB;CACnB,gBAAgB;CAChB,UAAU;EACR,kBAAkB;EAClB,gBAAgB;EAChB,qBAAqB;EACtB;CACD,gBAAgB;CAChB,UAAU;CACV,eAAe;CAChB;AAED,MAAM,kBAAkB;;;;;;;;;;;;;;;;;AAsBxB,SAAgB,WAAW,GAAmB;AAC5C,KAAI,MAAM,OAAO,EAAE,WAAW,KAAK,IAAI,EAAE,WAAW,MAAM,CACxD,QAAO,KAAK,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC;AAEpC,QAAO;;AAGT,MAAa,aAAa,KAAK,SAAS,EAAE,WAAW,MAAM;AAC3D,MAAa,cAAc,KAAK,YAAY,cAAc;AAM1D,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,aAA8B;AAC5C,KAAI,CAAC,WAAW,YAAY,CAC1B,QAAO,EAAE,GAAG,UAAU;CAGxB,IAAI;AACJ,KAAI;AACF,QAAM,aAAa,aAAa,QAAQ;UACjC,GAAG;AACV,UAAQ,OAAO,MACb,8CAA8C,YAAY,IAAI,EAAE,IACjE;AACD,SAAO,EAAE,GAAG,UAAU;;CAGxB,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;UACjB,GAAG;AACV,UAAQ,OAAO,MACb,+CAA+C,EAAE,IAClD;AACD,SAAO,EAAE,GAAG,UAAU;;AAGxB,QAAO,UAAU,UAAU,OAAO;;;;;;AAOpC,SAAgB,kBAAwB;AACtC,KAAI,CAAC,WAAW,WAAW,EAAE;AAC3B,YAAU,YAAY,EAAE,WAAW,MAAM,CAAC;AAC1C,UAAQ,OAAO,MACb,0CAA0C,WAAW,IACtD;;AAGH,KAAI,CAAC,WAAW,YAAY,CAC1B,KAAI;AACF,gBAAc,aAAa,iBAAiB,QAAQ;AACpD,UAAQ,OAAO,MACb,yCAAyC,YAAY,IACtD;UACM,GAAG;AACV,UAAQ,OAAO,MACb,gDAAgD,EAAE,IACnD"}
@@ -2,13 +2,13 @@
2
2
  import "../db-4lSqLFb8.mjs";
3
3
  import "../indexer-CKQcgKsz.mjs";
4
4
  import "../embeddings-DGRAPAYb.mjs";
5
- import "../search-GK0ibTJy.mjs";
6
- import "../tools-CUg0Lyg-.mjs";
7
- import { t as PaiClient } from "../ipc-client-CLt2fNlC.mjs";
8
- import { i as ensureConfigDir, o as loadConfig } from "../config-DELNqq3Z.mjs";
9
- import "../factory-DZLvRf4m.mjs";
10
- import "../detector-cYYhK2Mi.mjs";
11
- import { n as serve } from "../daemon-CeTX4NpF.mjs";
5
+ import "../search-_oHfguA5.mjs";
6
+ import "../tools-Dx7GjOHd.mjs";
7
+ import { t as PaiClient } from "../ipc-client-CgSpwHDC.mjs";
8
+ import { i as ensureConfigDir, o as loadConfig } from "../config-B4brrHHE.mjs";
9
+ import "../factory-CeXQzlwn.mjs";
10
+ import "../detector-Bp-2SM3x.mjs";
11
+ import { n as serve } from "../daemon-s868Paua.mjs";
12
12
  import { Command } from "commander";
13
13
 
14
14
  //#region src/daemon/index.ts
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { i as number, n as array, o as string, r as boolean, t as _enum } from "../schemas-BY3Pjvje.mjs";
3
- import { t as PaiClient } from "../ipc-client-CLt2fNlC.mjs";
4
- import { o as loadConfig } from "../config-DELNqq3Z.mjs";
2
+ import { i as number, n as array, o as string, r as boolean, t as _enum } from "../schemas-BFIgGntb.mjs";
3
+ import { t as PaiClient } from "../ipc-client-CgSpwHDC.mjs";
4
+ import { o as loadConfig } from "../config-B4brrHHE.mjs";
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
 
@@ -64,6 +64,11 @@ async function startShim() {
64
64
  " semantic — Cosine similarity over vector embeddings (requires prior embed run)",
65
65
  " hybrid — Normalized combination of BM25 + cosine (best quality)",
66
66
  "",
67
+ "Reranking is ON by default — results are re-scored with a cross-encoder model for better relevance.",
68
+ "Set rerank=false to skip reranking (faster but less accurate ordering).",
69
+ "",
70
+ "Recency boost optionally down-weights older results (recency_boost=90 means scores halve every 90 days).",
71
+ "",
67
72
  "Returns ranked snippets with project slug, file path, line range, and score.",
68
73
  "Higher score = more relevant."
69
74
  ].join("\n"), {
@@ -76,7 +81,9 @@ async function startShim() {
76
81
  "keyword",
77
82
  "semantic",
78
83
  "hybrid"
79
- ]).optional().describe("Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined).")
84
+ ]).optional().describe("Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined)."),
85
+ rerank: boolean().optional().describe("Rerank results using a cross-encoder model for better relevance. Default: true."),
86
+ recency_boost: number().int().min(0).max(365).optional().describe("Apply recency boost: score halves every N days. 0 = off (default). Recommended: 90.")
80
87
  }, async (args) => proxyTool("memory_search", args));
81
88
  server.tool("memory_get", [
82
89
  "Read the content of a specific file from a registered PAI project.",
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["z\n .string","z\n .boolean","z\n .array","z.enum","z\n .number","z\n .enum","z.string"],"sources":["../../src/daemon-mcp/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PAI Daemon MCP Shim\n *\n * A thin MCP server that proxies all PAI tool calls to the PAI daemon via IPC.\n * One shim instance runs per Claude Code session (spawned by Claude Code's MCP\n * mechanism). All shims share the single daemon process, which holds the\n * database connections and embedding model singleton.\n *\n * Tool definitions are static (unlike Coogle which discovers tools dynamically).\n * The 9 PAI tools are: memory_search, memory_get, project_info, project_list,\n * session_list, registry_search, project_detect, project_health, project_todo.\n *\n * If the daemon is not running, tool calls return a helpful error message\n * rather than crashing — this allows the legacy direct MCP (dist/mcp/index.mjs)\n * to serve as fallback.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { PaiClient } from \"../daemon/ipc-client.js\";\nimport { loadConfig } from \"../daemon/config.js\";\n\n// ---------------------------------------------------------------------------\n// IPC client singleton\n// ---------------------------------------------------------------------------\n\nlet _client: PaiClient | null = null;\n\nfunction getClient(): PaiClient {\n if (!_client) {\n const config = loadConfig();\n _client = new PaiClient(config.socketPath);\n }\n return _client;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: proxy a tool call to daemon, returning MCP-compatible content\n// ---------------------------------------------------------------------------\n\nasync function proxyTool(\n method: string,\n params: Record<string, unknown>\n): Promise<{ content: Array<{ type: \"text\"; text: string }>; isError?: boolean }> {\n try {\n const result = await getClient().call(method, params);\n // The daemon returns ToolResult objects (content + isError)\n const toolResult = result as {\n content: Array<{ type: string; text: string }>;\n isError?: boolean;\n };\n\n return {\n content: toolResult.content.map((c) => ({\n type: \"text\" as const,\n text: c.text,\n })),\n isError: toolResult.isError,\n };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n return {\n content: [\n {\n type: \"text\" as const,\n text: `PAI daemon error: ${msg}\\n\\nIs the daemon running? Start it with: pai daemon serve`,\n },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// MCP server\n// ---------------------------------------------------------------------------\n\nasync function startShim(): Promise<void> {\n const server = new McpServer({\n name: \"pai\",\n version: \"0.1.0\",\n });\n\n // -------------------------------------------------------------------------\n // Tool: memory_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_search\",\n [\n \"Search PAI federated memory using BM25 full-text ranking, semantic similarity, or a hybrid of both.\",\n \"\",\n \"Use this BEFORE answering questions about past work, decisions, dates, people,\",\n \"preferences, project status, todos, technical choices, or anything that might\",\n \"have been recorded in session notes or memory files.\",\n \"\",\n \"Modes:\",\n \" keyword — BM25 full-text search (default, fast, no embeddings required)\",\n \" semantic — Cosine similarity over vector embeddings (requires prior embed run)\",\n \" hybrid — Normalized combination of BM25 + cosine (best quality)\",\n \"\",\n \"Returns ranked snippets with project slug, file path, line range, and score.\",\n \"Higher score = more relevant.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\"Free-text search query. Multiple words are ORed together — any matching word returns a result, ranked by relevance.\"),\n project: z\n .string()\n .optional()\n .describe(\n \"Scope search to a single project by slug. Omit to search all projects.\"\n ),\n all_projects: z\n .boolean()\n .optional()\n .describe(\n \"Explicitly search all projects (default behaviour when project is omitted).\"\n ),\n sources: z\n .array(z.enum([\"memory\", \"notes\"]))\n .optional()\n .describe(\"Restrict to specific source types: 'memory' or 'notes'.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum results to return. Default: 10.\"),\n mode: z\n .enum([\"keyword\", \"semantic\", \"hybrid\"])\n .optional()\n .describe(\n \"Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined).\"\n ),\n },\n async (args) => proxyTool(\"memory_search\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: memory_get\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_get\",\n [\n \"Read the content of a specific file from a registered PAI project.\",\n \"\",\n \"Use this to read a full memory file, session note, or document after finding\",\n \"it via memory_search. Optionally restrict to a line range.\",\n \"\",\n \"The path must be a relative path within the project root (no ../ traversal).\",\n ].join(\"\\n\"),\n {\n project: z\n .string()\n .describe(\"Project slug identifying which project's files to read from.\"),\n path: z\n .string()\n .describe(\n \"Relative path within the project root (e.g. 'Notes/0001 - 2026-01-01 - Example.md').\"\n ),\n from: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Starting line number (1-based, inclusive). Default: 1.\"),\n lines: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Number of lines to return. Default: entire file.\"),\n },\n async (args) => proxyTool(\"memory_get\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_info\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_info\",\n [\n \"Get detailed information about a PAI registered project.\",\n \"\",\n \"Use this to look up a project's root path, type, status, tags, session count,\",\n \"and last active date. If no slug is provided, attempts to detect the current\",\n \"project from the caller's working directory.\",\n ].join(\"\\n\"),\n {\n slug: z\n .string()\n .optional()\n .describe(\n \"Project slug. Omit to auto-detect from the current working directory.\"\n ),\n },\n async (args) => proxyTool(\"project_info\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_list\",\n [\n \"List registered PAI projects with optional filters.\",\n \"\",\n \"Use this to browse all known projects, find projects by status or tag,\",\n \"or get a quick overview of the PAI registry.\",\n ].join(\"\\n\"),\n {\n status: z\n .enum([\"active\", \"archived\", \"migrating\"])\n .optional()\n .describe(\"Filter by project status. Default: all statuses.\"),\n tag: z\n .string()\n .optional()\n .describe(\"Filter by tag name (exact match).\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum number of projects to return. Default: 50.\"),\n },\n async (args) => proxyTool(\"project_list\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_list\",\n [\n \"List session notes for a PAI project.\",\n \"\",\n \"Use this to find what sessions exist for a project, see their dates and titles,\",\n \"and identify specific session notes to read via memory_get.\",\n ].join(\"\\n\"),\n {\n project: z.string().describe(\"Project slug to list sessions for.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum sessions to return. Default: 10 (most recent first).\"),\n status: z\n .enum([\"open\", \"completed\", \"compacted\"])\n .optional()\n .describe(\"Filter by session status.\"),\n },\n async (args) => proxyTool(\"session_list\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: registry_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"registry_search\",\n [\n \"Search PAI project registry by slug, display name, or path.\",\n \"\",\n \"Use this to find the slug for a project when you know its name or path,\",\n \"or to check if a project is registered. Returns matching project entries.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\n \"Search term matched against project slugs, display names, and root paths (case-insensitive substring match).\"\n ),\n },\n async (args) => proxyTool(\"registry_search\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_detect\",\n [\n \"Detect which registered PAI project a filesystem path belongs to.\",\n \"\",\n \"Use this at session start to auto-identify the current project from the\",\n \"working directory, or to map any path back to its registered project.\",\n \"\",\n \"Returns: slug, display_name, root_path, type, status, match_type (exact|parent),\",\n \"relative_path (if the given path is inside a project), and session stats.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Absolute path to detect project for. Defaults to the MCP server's process.cwd().\"\n ),\n },\n async (args) => proxyTool(\"project_detect\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_health\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_health\",\n [\n \"Audit all registered PAI projects to find moved or deleted directories.\",\n \"\",\n \"Returns a JSON report categorising every project as:\",\n \" active — root_path exists on disk\",\n \" stale — root_path missing, but a directory with the same name was found nearby\",\n \" dead — root_path missing, no candidate found\",\n \"\",\n \"Each active project entry also includes a 'todo' field indicating whether\",\n \"a TODO.md was found and whether it has a ## Continue section.\",\n ].join(\"\\n\"),\n {\n category: z\n .enum([\"active\", \"stale\", \"dead\", \"all\"])\n .optional()\n .describe(\"Filter results to a specific health category. Default: all.\"),\n },\n async (args) => proxyTool(\"project_health\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_todo\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_todo\",\n [\n \"Read a project's TODO.md without needing to know the exact file path.\",\n \"\",\n \"Use this at session start or when resuming work to get the project's current\",\n \"task list and continuation prompt. If a '## Continue' section is present,\",\n \"it will be surfaced at the top of the response for quick context recovery.\",\n \"\",\n \"Searches these locations in order:\",\n \" 1. <project_root>/Notes/TODO.md\",\n \" 2. <project_root>/.claude/Notes/TODO.md\",\n \" 3. <project_root>/tasks/todo.md\",\n \" 4. <project_root>/TODO.md\",\n \"\",\n \"If no project slug is provided, auto-detects from the current working directory.\",\n ].join(\"\\n\"),\n {\n project: z\n .string()\n .optional()\n .describe(\n \"Project slug. Omit to auto-detect from the current working directory.\"\n ),\n },\n async (args) => proxyTool(\"project_todo\", args)\n );\n\n // -------------------------------------------------------------------------\n // Connect transport and start serving\n // -------------------------------------------------------------------------\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nstartShim().catch((e) => {\n process.stderr.write(`PAI MCP shim fatal error: ${String(e)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["z\n .string","z\n .boolean","z\n .array","z.enum","z\n .number","z\n .enum","z.string"],"sources":["../../src/daemon-mcp/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PAI Daemon MCP Shim\n *\n * A thin MCP server that proxies all PAI tool calls to the PAI daemon via IPC.\n * One shim instance runs per Claude Code session (spawned by Claude Code's MCP\n * mechanism). All shims share the single daemon process, which holds the\n * database connections and embedding model singleton.\n *\n * Tool definitions are static (unlike Coogle which discovers tools dynamically).\n * The 9 PAI tools are: memory_search, memory_get, project_info, project_list,\n * session_list, registry_search, project_detect, project_health, project_todo.\n *\n * If the daemon is not running, tool calls return a helpful error message\n * rather than crashing — this allows the legacy direct MCP (dist/mcp/index.mjs)\n * to serve as fallback.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { PaiClient } from \"../daemon/ipc-client.js\";\nimport { loadConfig } from \"../daemon/config.js\";\n\n// ---------------------------------------------------------------------------\n// IPC client singleton\n// ---------------------------------------------------------------------------\n\nlet _client: PaiClient | null = null;\n\nfunction getClient(): PaiClient {\n if (!_client) {\n const config = loadConfig();\n _client = new PaiClient(config.socketPath);\n }\n return _client;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: proxy a tool call to daemon, returning MCP-compatible content\n// ---------------------------------------------------------------------------\n\nasync function proxyTool(\n method: string,\n params: Record<string, unknown>\n): Promise<{ content: Array<{ type: \"text\"; text: string }>; isError?: boolean }> {\n try {\n const result = await getClient().call(method, params);\n // The daemon returns ToolResult objects (content + isError)\n const toolResult = result as {\n content: Array<{ type: string; text: string }>;\n isError?: boolean;\n };\n\n return {\n content: toolResult.content.map((c) => ({\n type: \"text\" as const,\n text: c.text,\n })),\n isError: toolResult.isError,\n };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n return {\n content: [\n {\n type: \"text\" as const,\n text: `PAI daemon error: ${msg}\\n\\nIs the daemon running? Start it with: pai daemon serve`,\n },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// MCP server\n// ---------------------------------------------------------------------------\n\nasync function startShim(): Promise<void> {\n const server = new McpServer({\n name: \"pai\",\n version: \"0.1.0\",\n });\n\n // -------------------------------------------------------------------------\n // Tool: memory_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_search\",\n [\n \"Search PAI federated memory using BM25 full-text ranking, semantic similarity, or a hybrid of both.\",\n \"\",\n \"Use this BEFORE answering questions about past work, decisions, dates, people,\",\n \"preferences, project status, todos, technical choices, or anything that might\",\n \"have been recorded in session notes or memory files.\",\n \"\",\n \"Modes:\",\n \" keyword — BM25 full-text search (default, fast, no embeddings required)\",\n \" semantic — Cosine similarity over vector embeddings (requires prior embed run)\",\n \" hybrid — Normalized combination of BM25 + cosine (best quality)\",\n \"\",\n \"Reranking is ON by default — results are re-scored with a cross-encoder model for better relevance.\",\n \"Set rerank=false to skip reranking (faster but less accurate ordering).\",\n \"\",\n \"Recency boost optionally down-weights older results (recency_boost=90 means scores halve every 90 days).\",\n \"\",\n \"Returns ranked snippets with project slug, file path, line range, and score.\",\n \"Higher score = more relevant.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\"Free-text search query. Multiple words are ORed together — any matching word returns a result, ranked by relevance.\"),\n project: z\n .string()\n .optional()\n .describe(\n \"Scope search to a single project by slug. Omit to search all projects.\"\n ),\n all_projects: z\n .boolean()\n .optional()\n .describe(\n \"Explicitly search all projects (default behaviour when project is omitted).\"\n ),\n sources: z\n .array(z.enum([\"memory\", \"notes\"]))\n .optional()\n .describe(\"Restrict to specific source types: 'memory' or 'notes'.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum results to return. Default: 10.\"),\n mode: z\n .enum([\"keyword\", \"semantic\", \"hybrid\"])\n .optional()\n .describe(\n \"Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined).\"\n ),\n rerank: z\n .boolean()\n .optional()\n .describe(\n \"Rerank results using a cross-encoder model for better relevance. Default: true.\"\n ),\n recency_boost: z\n .number()\n .int()\n .min(0)\n .max(365)\n .optional()\n .describe(\n \"Apply recency boost: score halves every N days. 0 = off (default). Recommended: 90.\"\n ),\n },\n async (args) => proxyTool(\"memory_search\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: memory_get\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_get\",\n [\n \"Read the content of a specific file from a registered PAI project.\",\n \"\",\n \"Use this to read a full memory file, session note, or document after finding\",\n \"it via memory_search. Optionally restrict to a line range.\",\n \"\",\n \"The path must be a relative path within the project root (no ../ traversal).\",\n ].join(\"\\n\"),\n {\n project: z\n .string()\n .describe(\"Project slug identifying which project's files to read from.\"),\n path: z\n .string()\n .describe(\n \"Relative path within the project root (e.g. 'Notes/0001 - 2026-01-01 - Example.md').\"\n ),\n from: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Starting line number (1-based, inclusive). Default: 1.\"),\n lines: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Number of lines to return. Default: entire file.\"),\n },\n async (args) => proxyTool(\"memory_get\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_info\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_info\",\n [\n \"Get detailed information about a PAI registered project.\",\n \"\",\n \"Use this to look up a project's root path, type, status, tags, session count,\",\n \"and last active date. If no slug is provided, attempts to detect the current\",\n \"project from the caller's working directory.\",\n ].join(\"\\n\"),\n {\n slug: z\n .string()\n .optional()\n .describe(\n \"Project slug. Omit to auto-detect from the current working directory.\"\n ),\n },\n async (args) => proxyTool(\"project_info\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_list\",\n [\n \"List registered PAI projects with optional filters.\",\n \"\",\n \"Use this to browse all known projects, find projects by status or tag,\",\n \"or get a quick overview of the PAI registry.\",\n ].join(\"\\n\"),\n {\n status: z\n .enum([\"active\", \"archived\", \"migrating\"])\n .optional()\n .describe(\"Filter by project status. Default: all statuses.\"),\n tag: z\n .string()\n .optional()\n .describe(\"Filter by tag name (exact match).\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum number of projects to return. Default: 50.\"),\n },\n async (args) => proxyTool(\"project_list\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_list\",\n [\n \"List session notes for a PAI project.\",\n \"\",\n \"Use this to find what sessions exist for a project, see their dates and titles,\",\n \"and identify specific session notes to read via memory_get.\",\n ].join(\"\\n\"),\n {\n project: z.string().describe(\"Project slug to list sessions for.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum sessions to return. Default: 10 (most recent first).\"),\n status: z\n .enum([\"open\", \"completed\", \"compacted\"])\n .optional()\n .describe(\"Filter by session status.\"),\n },\n async (args) => proxyTool(\"session_list\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: registry_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"registry_search\",\n [\n \"Search PAI project registry by slug, display name, or path.\",\n \"\",\n \"Use this to find the slug for a project when you know its name or path,\",\n \"or to check if a project is registered. Returns matching project entries.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\n \"Search term matched against project slugs, display names, and root paths (case-insensitive substring match).\"\n ),\n },\n async (args) => proxyTool(\"registry_search\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_detect\",\n [\n \"Detect which registered PAI project a filesystem path belongs to.\",\n \"\",\n \"Use this at session start to auto-identify the current project from the\",\n \"working directory, or to map any path back to its registered project.\",\n \"\",\n \"Returns: slug, display_name, root_path, type, status, match_type (exact|parent),\",\n \"relative_path (if the given path is inside a project), and session stats.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Absolute path to detect project for. Defaults to the MCP server's process.cwd().\"\n ),\n },\n async (args) => proxyTool(\"project_detect\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_health\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_health\",\n [\n \"Audit all registered PAI projects to find moved or deleted directories.\",\n \"\",\n \"Returns a JSON report categorising every project as:\",\n \" active — root_path exists on disk\",\n \" stale — root_path missing, but a directory with the same name was found nearby\",\n \" dead — root_path missing, no candidate found\",\n \"\",\n \"Each active project entry also includes a 'todo' field indicating whether\",\n \"a TODO.md was found and whether it has a ## Continue section.\",\n ].join(\"\\n\"),\n {\n category: z\n .enum([\"active\", \"stale\", \"dead\", \"all\"])\n .optional()\n .describe(\"Filter results to a specific health category. Default: all.\"),\n },\n async (args) => proxyTool(\"project_health\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_todo\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_todo\",\n [\n \"Read a project's TODO.md without needing to know the exact file path.\",\n \"\",\n \"Use this at session start or when resuming work to get the project's current\",\n \"task list and continuation prompt. If a '## Continue' section is present,\",\n \"it will be surfaced at the top of the response for quick context recovery.\",\n \"\",\n \"Searches these locations in order:\",\n \" 1. <project_root>/Notes/TODO.md\",\n \" 2. <project_root>/.claude/Notes/TODO.md\",\n \" 3. <project_root>/tasks/todo.md\",\n \" 4. <project_root>/TODO.md\",\n \"\",\n \"If no project slug is provided, auto-detects from the current working directory.\",\n ].join(\"\\n\"),\n {\n project: z\n .string()\n .optional()\n .describe(\n \"Project slug. Omit to auto-detect from the current working directory.\"\n ),\n },\n async (args) => proxyTool(\"project_todo\", args)\n );\n\n // -------------------------------------------------------------------------\n // Connect transport and start serving\n // -------------------------------------------------------------------------\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nstartShim().catch((e) => {\n process.stderr.write(`PAI MCP shim fatal error: ${String(e)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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"}
@@ -2,10 +2,10 @@ import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
2
2
  import { n as openRegistry } from "./db-4lSqLFb8.mjs";
3
3
  import { r as indexAll } from "./indexer-CKQcgKsz.mjs";
4
4
  import { t as configureEmbeddingModel } from "./embeddings-DGRAPAYb.mjs";
5
- import { a as toolProjectHealth, c as toolProjectTodo, d as toolSessionRoute, i as toolProjectDetect, l as toolRegistrySearch, n as toolMemorySearch, o as toolProjectInfo, s as toolProjectList, t as toolMemoryGet, u as toolSessionList } from "./tools-CUg0Lyg-.mjs";
6
- import { n as CONFIG_FILE, s as DEFAULT_NOTIFICATION_CONFIG, t as CONFIG_DIR } from "./config-DELNqq3Z.mjs";
7
- import { t as createStorageBackend } from "./factory-DZLvRf4m.mjs";
8
- import { t as detectTopicShift } from "./detector-cYYhK2Mi.mjs";
5
+ import { a as toolProjectHealth, c as toolProjectTodo, d as toolSessionRoute, i as toolProjectDetect, l as toolRegistrySearch, n as toolMemorySearch, o as toolProjectInfo, s as toolProjectList, t as toolMemoryGet, u as toolSessionList } from "./tools-Dx7GjOHd.mjs";
6
+ import { n as CONFIG_FILE, s as DEFAULT_NOTIFICATION_CONFIG, t as CONFIG_DIR } from "./config-B4brrHHE.mjs";
7
+ import { t as createStorageBackend } from "./factory-CeXQzlwn.mjs";
8
+ import { t as detectTopicShift } from "./detector-Bp-2SM3x.mjs";
9
9
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
10
10
  import { setPriority } from "node:os";
11
11
  import { randomUUID } from "node:crypto";
@@ -384,7 +384,7 @@ async function runIndex() {
384
384
  try {
385
385
  process.stderr.write("[pai-daemon] Starting scheduled index run...\n");
386
386
  if (storageBackend.backendType === "sqlite") {
387
- const { SQLiteBackend } = await import("./sqlite-RyR8Up1v.mjs");
387
+ const { SQLiteBackend } = await import("./sqlite-CymLKiDE.mjs");
388
388
  if (storageBackend instanceof SQLiteBackend) {
389
389
  const { projects, result } = await indexAll(storageBackend.getRawDb(), registryDb);
390
390
  const elapsed = Date.now() - t0;
@@ -392,7 +392,7 @@ async function runIndex() {
392
392
  process.stderr.write(`[pai-daemon] Index complete: ${projects} projects, ${result.filesProcessed} files, ${result.chunksCreated} chunks (${elapsed}ms)\n`);
393
393
  }
394
394
  } else {
395
- const { indexAllWithBackend } = await import("./indexer-backend-BHztlJJg.mjs");
395
+ const { indexAllWithBackend } = await import("./indexer-backend-DQO-FqAI.mjs");
396
396
  const { projects, result } = await indexAllWithBackend(storageBackend, registryDb);
397
397
  const elapsed = Date.now() - t0;
398
398
  lastIndexTime = Date.now();
@@ -425,7 +425,7 @@ async function runVaultIndex() {
425
425
  try {
426
426
  process.stderr.write("[pai-daemon] Starting vault index run...\n");
427
427
  if (storageBackend.backendType === "sqlite") {
428
- const { SQLiteBackend } = await import("./sqlite-RyR8Up1v.mjs");
428
+ const { SQLiteBackend } = await import("./sqlite-CymLKiDE.mjs");
429
429
  if (storageBackend instanceof SQLiteBackend) {
430
430
  const db = storageBackend.getRawDb();
431
431
  let vaultProjectId = daemonConfig.vaultProjectId;
@@ -434,7 +434,7 @@ async function runVaultIndex() {
434
434
  process.stderr.write("[pai-daemon] Vault project ID not found. Register the vault as a project first.\n");
435
435
  return;
436
436
  }
437
- const { indexVault } = await import("./vault-indexer-Bo2aPSzP.mjs");
437
+ const { indexVault } = await import("./vault-indexer-DXWs9pDn.mjs");
438
438
  const result = await indexVault(db, vaultProjectId, daemonConfig.vaultPath);
439
439
  const elapsed = Date.now() - t0;
440
440
  lastVaultIndexTime = Date.now();
@@ -485,7 +485,7 @@ async function runEmbed() {
485
485
  const t0 = Date.now();
486
486
  try {
487
487
  process.stderr.write("[pai-daemon] Starting scheduled embed pass...\n");
488
- const { embedChunksWithBackend } = await import("./indexer-backend-BHztlJJg.mjs");
488
+ const { embedChunksWithBackend } = await import("./indexer-backend-DQO-FqAI.mjs");
489
489
  const count = await embedChunksWithBackend(storageBackend, () => shutdownRequested);
490
490
  const elapsed = Date.now() - t0;
491
491
  lastEmbedTime = Date.now();
@@ -540,9 +540,9 @@ async function dispatchTool(method, params) {
540
540
  case "zettel_suggest":
541
541
  case "zettel_converse":
542
542
  case "zettel_themes": {
543
- const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes } = await import("./tools-CUg0Lyg-.mjs").then((n) => n.y);
543
+ const { toolZettelExplore, toolZettelHealth, toolZettelSurprise, toolZettelSuggest, toolZettelConverse, toolZettelThemes } = await import("./tools-Dx7GjOHd.mjs").then((n) => n.y);
544
544
  if (storageBackend.backendType !== "sqlite") throw new Error("Zettel tools require SQLite backend");
545
- const { SQLiteBackend } = await import("./sqlite-RyR8Up1v.mjs");
545
+ const { SQLiteBackend } = await import("./sqlite-CymLKiDE.mjs");
546
546
  if (!(storageBackend instanceof SQLiteBackend)) throw new Error("Zettel tools require SQLite backend");
547
547
  const fedDb = storageBackend.getRawDb();
548
548
  switch (method) {
@@ -851,4 +851,4 @@ async function serve(config) {
851
851
 
852
852
  //#endregion
853
853
  export { serve as n, daemon_exports as t };
854
- //# sourceMappingURL=daemon-CeTX4NpF.mjs.map
854
+ //# sourceMappingURL=daemon-s868Paua.mjs.map