@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
package/dist/index.d.mts CHANGED
@@ -252,6 +252,7 @@ interface SearchResult {
252
252
  score: number;
253
253
  tier: string;
254
254
  source: string;
255
+ updatedAt?: number;
255
256
  }
256
257
  interface SearchOptions {
257
258
  /** Restrict search to these project IDs. */
@@ -303,5 +304,32 @@ declare function searchMemory(db: Database, query: string, opts?: SearchOptions)
303
304
  */
304
305
  declare function populateSlugs(results: SearchResult[], registryDb: Database): SearchResult[];
305
306
  //#endregion
306
- export { CREATE_TABLES_SQL, type Chunk, type ChunkOptions, FEDERATION_SCHEMA_SQL, type IndexResult, type MigrationResult, type PaiMarker, SCHEMA_VERSION, type SearchOptions, type SearchResult, buildFtsQuery, chunkMarkdown, decodeEncodedDir, detectTier, discoverPaiMarkers, ensurePaiMarker, estimateTokens, indexAll, indexFile, indexProject, initializeFederationSchema, initializeSchema, migrateFromJson, openFederation, openRegistry, parseSessionFilename, populateSlugs, readPaiMarker, searchMemory, slugify };
307
+ //#region src/memory/reranker.d.ts
308
+ /**
309
+ * Configure the reranker model.
310
+ * Must be called before the first rerank() call if you want a non-default model.
311
+ */
312
+ declare function configureRerankerModel(model?: string): void;
313
+ interface RerankOptions {
314
+ /** Maximum number of results to return after reranking. */
315
+ topK?: number;
316
+ /**
317
+ * Maximum number of candidates to rerank.
318
+ * Cross-encoders are O(n) per candidate, so we cap to keep latency
319
+ * reasonable. Default: 50.
320
+ */
321
+ maxCandidates?: number;
322
+ }
323
+ /**
324
+ * Rerank search results using a cross-encoder model.
325
+ *
326
+ * Takes the top `maxCandidates` results from a first-stage retriever,
327
+ * scores each (query, snippet) pair through the cross-encoder, and
328
+ * returns them sorted by cross-encoder relevance score.
329
+ *
330
+ * The original retrieval score is replaced with the cross-encoder score.
331
+ */
332
+ declare function rerankResults(query: string, results: SearchResult[], opts?: RerankOptions): Promise<SearchResult[]>;
333
+ //#endregion
334
+ export { CREATE_TABLES_SQL, type Chunk, type ChunkOptions, FEDERATION_SCHEMA_SQL, type IndexResult, type MigrationResult, type PaiMarker, type RerankOptions, SCHEMA_VERSION, type SearchOptions, type SearchResult, buildFtsQuery, chunkMarkdown, configureRerankerModel, decodeEncodedDir, detectTier, discoverPaiMarkers, ensurePaiMarker, estimateTokens, indexAll, indexFile, indexProject, initializeFederationSchema, initializeSchema, migrateFromJson, openFederation, openRegistry, parseSessionFilename, populateSlugs, readPaiMarker, rerankResults, searchMemory, slugify };
307
335
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/registry/schema.ts","../src/registry/db.ts","../src/registry/migrate.ts","../src/registry/pai-marker.ts","../src/memory/schema.ts","../src/memory/db.ts","../src/memory/chunker.ts","../src/memory/indexer.ts","../src/memory/search.ts"],"mappings":";;;cAgBa,cAAA;AAAA,cAEA,iBAAA;;;;;;ACYb;;;iBDyGgB,gBAAA,CAAiB,EAAA,EAAI,QAAA;;;AArHrC;;;;;AAqHA;;;;;;AArHA,iBCYgB,YAAA,CAAa,IAAA,YAAuC,UAAA;;;;;;;ACkIpE;;;;;AAUC;;;;;;;;;;;AAyBD;;;;;AAmCA;iBA5GgB,gBAAA,CACd,OAAA,UACA,SAAA,GAAY,GAAA;;;;;;iBAoCE,OAAA,CAAQ,KAAA;AAAA,UAgBd,aAAA;EACR,MAAA;EACA,IAAA;EACA,IAAA;EACA,KAAA;EACA,QAAA;AAAA;;;;;;iBAcc,oBAAA,CACd,QAAA,WACC,aAAA;AAAA,UAiCc,eAAA;EACf,gBAAA;EACA,eAAA;EACA,gBAAA;EACA,MAAA;AAAA;;;;;;AC1DF;;;;;iBDuEgB,eAAA,CACd,EAAA,EAAI,QAAA,EACJ,YAAA,YACC,eAAA;;;;;;AF1OH;;;;;AAEA;UGMiB,SAAA;;EAEf,IAAA;EHR4B;EGU5B,IAAA;EH2G8B;EGzG9B,WAAA;AAAA;;;;;AFAF;;;;;;;iBEkJgB,eAAA,CACd,WAAA,UACA,IAAA,UACA,WAAA;ADzDF;;;;AAAA,iBC0HgB,aAAA,CACd,WAAA;EACG,IAAA;EAAc,UAAA;EAAoB,MAAA;AAAA;ADtFvC;;;;;AAUC;;;;;AAVD,iBCmHgB,kBAAA,CAAmB,UAAA,aAAuB,SAAA;;;cCtP7C,qBAAA;;;AF6Fb;;;;;;;iBEegB,0BAAA,CAA2B,EAAA,EAAI,QAAA;;;AJvH/C;;;;;AAqHA;;;;;;AArHA,iBKYgB,cAAA,CAAe,IAAA,YAAyC,UAAA;;;;;;ALdxE;;;;UMNiB,KAAA;EACf,IAAA;EACA,SAAA;EACA,OAAA;EACA,IAAA;AAAA;AAAA,UAGe,YAAA;ENsHe;EMpH9B,SAAA;ENoHmC;EMlHnC,OAAA;AAAA;;;ALSF;;iBKCgB,cAAA,CAAe,IAAA;;;;;;AJ2F/B;;;;;iBImFgB,aAAA,CAAc,OAAA,UAAiB,IAAA,GAAO,YAAA,GAAe,KAAA;;;UCrLpD,WAAA;EACf,cAAA;EACA,aAAA;EACA,YAAA;AAAA;;;;ANGF;;;;;;iBMagB,UAAA,CACd,YAAA;;AL8EF;;;;iBKXgB,SAAA,CACd,EAAA,EAAI,QAAA,EACJ,SAAA,UACA,QAAA,UACA,YAAA,UACA,MAAA,UACA,IAAA;AAAA,iBA6UoB,YAAA,CACpB,EAAA,EAAI,QAAA,EACJ,SAAA,UACA,QAAA,UACA,cAAA,mBACC,OAAA,CAAQ,WAAA;;;;ALvSX;;;iBK+dsB,QAAA,CACpB,EAAA,EAAI,QAAA,EACJ,UAAA,EAAY,QAAA,GACX,OAAA;EAAU,QAAA;EAAkB,MAAA,EAAQ,WAAA;AAAA;;;UC/mBtB,YAAA;EACf,SAAA;EACA,WAAA;EACA,IAAA;EACA,SAAA;EACA,OAAA;EACA,OAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;AAAA;AAAA,UAGe,aAAA;EPDY;EOG3B,UAAA;;EAEA,OAAA;;EAEA,KAAA;ENqF8B;EMnF9B,UAAA;ENqFe;EMnFf,QAAA;AAAA;;;;ANuHF;;;;;AAUC;;;;;;;;;;iBM3Fe,aAAA,CAAc,KAAA;ANoH9B;;;;;AAmCA;;;;;;;AAnCA,iBMlFgB,YAAA,CACd,EAAA,EAAI,QAAA,EACJ,KAAA,UACA,IAAA,GAAO,aAAA,GACN,YAAA;AL2DH;;;;AAAA,iBKwNgB,aAAA,CACd,OAAA,EAAS,YAAA,IACT,UAAA,EAAY,QAAA,GACX,YAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/registry/schema.ts","../src/registry/db.ts","../src/registry/migrate.ts","../src/registry/pai-marker.ts","../src/memory/schema.ts","../src/memory/db.ts","../src/memory/chunker.ts","../src/memory/indexer.ts","../src/memory/search.ts","../src/memory/reranker.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -6,8 +6,9 @@ import { i as initializeFederationSchema, n as openFederation, r as FEDERATION_S
6
6
  import { n as estimateTokens, t as chunkMarkdown } from "./chunker-CbnBe0s0.mjs";
7
7
  import { a as indexProject, i as indexFile, r as indexAll, t as detectTier } from "./indexer-CKQcgKsz.mjs";
8
8
  import "./embeddings-DGRAPAYb.mjs";
9
- import { n as populateSlugs, r as searchMemory, t as buildFtsQuery } from "./search-GK0ibTJy.mjs";
10
- import "./tools-CUg0Lyg-.mjs";
9
+ import { n as populateSlugs, r as searchMemory, t as buildFtsQuery } from "./search-_oHfguA5.mjs";
10
+ import { n as rerankResults, t as configureRerankerModel } from "./reranker-D7bRAHi6.mjs";
11
+ import "./tools-Dx7GjOHd.mjs";
11
12
  import "./mcp/index.mjs";
12
13
 
13
- export { CREATE_TABLES_SQL, FEDERATION_SCHEMA_SQL, SCHEMA_VERSION, buildFtsQuery, chunkMarkdown, decodeEncodedDir, detectTier, discoverPaiMarkers, ensurePaiMarker, estimateTokens, indexAll, indexFile, indexProject, initializeFederationSchema, initializeSchema, migrateFromJson, openFederation, openRegistry, parseSessionFilename, populateSlugs, readPaiMarker, searchMemory, slugify };
14
+ export { CREATE_TABLES_SQL, FEDERATION_SCHEMA_SQL, SCHEMA_VERSION, buildFtsQuery, chunkMarkdown, configureRerankerModel, decodeEncodedDir, detectTier, discoverPaiMarkers, ensurePaiMarker, estimateTokens, indexAll, indexFile, indexProject, initializeFederationSchema, initializeSchema, migrateFromJson, openFederation, openRegistry, parseSessionFilename, populateSlugs, readPaiMarker, rerankResults, searchMemory, slugify };
@@ -358,4 +358,4 @@ async function indexAllWithBackend(backend, registryDb) {
358
358
 
359
359
  //#endregion
360
360
  export { embedChunksWithBackend, indexAllWithBackend };
361
- //# sourceMappingURL=indexer-backend-BHztlJJg.mjs.map
361
+ //# sourceMappingURL=indexer-backend-DQO-FqAI.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"indexer-backend-BHztlJJg.mjs","names":[],"sources":["../src/memory/indexer-backend.ts"],"sourcesContent":["/**\n * Backend-aware indexer for PAI federation memory.\n *\n * This module provides the same functionality as indexer.ts but writes\n * through the StorageBackend interface instead of directly to better-sqlite3.\n * Used when the daemon is configured with the Postgres backend.\n *\n * The SQLite path still uses indexer.ts directly (which is faster for SQLite\n * due to synchronous transactions).\n */\n\nimport { createHash } from \"node:crypto\";\nimport { readFileSync, statSync, readdirSync, existsSync } from \"node:fs\";\nimport { join, relative, basename, normalize } from \"node:path\";\n\n// ---------------------------------------------------------------------------\n// Session title parsing\n// ---------------------------------------------------------------------------\n\nconst SESSION_TITLE_RE = /^(\\d{4})\\s*-\\s*(\\d{4}-\\d{2}-\\d{2})\\s*-\\s*(.+)\\.md$/;\n\n/**\n * Parse a session title from a Notes filename.\n * Format: \"NNNN - YYYY-MM-DD - Descriptive Title.md\"\n * Returns a synthetic chunk text like \"Session #0086 2026-02-23: Pai Daemon Background Service\"\n * or null if the filename doesn't match the expected pattern.\n */\nexport function parseSessionTitleChunk(fileName: string): string | null {\n const m = SESSION_TITLE_RE.exec(fileName);\n if (!m) return null;\n const [, num, date, title] = m;\n return `Session #${num} ${date}: ${title}`;\n}\nimport { homedir } from \"node:os\";\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend, ChunkRow } from \"../storage/interface.js\";\nimport type { IndexResult } from \"./indexer.js\";\nimport { chunkMarkdown } from \"./chunker.js\";\nimport { detectTier } from \"./indexer.js\";\n\n// ---------------------------------------------------------------------------\n// Constants (mirrored from indexer.ts)\n// ---------------------------------------------------------------------------\n\nconst MAX_FILES_PER_PROJECT = 5_000;\nconst MAX_WALK_DEPTH = 6;\nconst INDEX_YIELD_EVERY = 10;\n\n/**\n * Directories to ALWAYS skip, at any depth, during any directory walk.\n * These are build artifacts, dependency trees, and VCS internals that\n * should never be indexed regardless of where they appear in the tree.\n */\nconst ALWAYS_SKIP_DIRS = new Set([\n // Version control\n \".git\",\n // Dependency directories (any language)\n \"node_modules\",\n \"vendor\",\n \"Pods\", // CocoaPods (iOS/macOS)\n // Build / compile output\n \"dist\",\n \"build\",\n \"out\",\n \"DerivedData\", // Xcode\n \".next\", // Next.js\n // Python virtual environments and caches\n \".venv\",\n \"venv\",\n \"__pycache__\",\n // General caches\n \".cache\",\n \".bun\",\n]);\n\nconst ROOT_SCAN_SKIP_DIRS = new Set([\n \"memory\", \"Notes\", \".claude\", \".DS_Store\",\n ...ALWAYS_SKIP_DIRS,\n]);\n\nconst CONTENT_SCAN_SKIP_DIRS = new Set([\n \"Library\", \"Applications\", \"Music\", \"Movies\", \"Pictures\", \"Desktop\",\n \"Downloads\", \"Public\", \"coverage\",\n ...ALWAYS_SKIP_DIRS,\n]);\n\n// ---------------------------------------------------------------------------\n// Helpers (same logic as indexer.ts)\n// ---------------------------------------------------------------------------\n\nfunction sha256File(content: string): string {\n return createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\nfunction chunkId(\n projectId: number,\n path: string,\n chunkIndex: number,\n startLine: number,\n endLine: number,\n): string {\n return createHash(\"sha256\")\n .update(`${projectId}:${path}:${chunkIndex}:${startLine}:${endLine}`)\n .digest(\"hex\");\n}\n\nfunction walkMdFiles(\n dir: string,\n acc?: string[],\n cap = MAX_FILES_PER_PROJECT,\n depth = 0,\n): string[] {\n const results = acc ?? [];\n if (!existsSync(dir)) return results;\n if (results.length >= cap) return results;\n if (depth > MAX_WALK_DEPTH) return results;\n try {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (results.length >= cap) break;\n if (entry.isSymbolicLink()) continue;\n // Skip known junk directories at every recursion depth\n if (ALWAYS_SKIP_DIRS.has(entry.name)) continue;\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n walkMdFiles(full, results, cap, depth + 1);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n results.push(full);\n }\n }\n } catch { /* skip unreadable */ }\n return results;\n}\n\nfunction walkContentFiles(rootPath: string): string[] {\n if (!existsSync(rootPath)) return [];\n const results: string[] = [];\n try {\n for (const entry of readdirSync(rootPath, { withFileTypes: true })) {\n if (results.length >= MAX_FILES_PER_PROJECT) break;\n if (entry.isSymbolicLink()) continue;\n if (ROOT_SCAN_SKIP_DIRS.has(entry.name)) continue;\n if (CONTENT_SCAN_SKIP_DIRS.has(entry.name)) continue;\n const full = join(rootPath, entry.name);\n if (entry.isDirectory()) {\n walkMdFiles(full, results, MAX_FILES_PER_PROJECT);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n if (entry.name !== \"MEMORY.md\") results.push(full);\n }\n }\n } catch { /* skip */ }\n return results;\n}\n\nfunction isPathTooBroadForContentScan(rootPath: string): boolean {\n const normalized = normalize(rootPath);\n const home = homedir();\n if (home.startsWith(normalized) || normalized === \"/\") return true;\n if (normalized.startsWith(home)) {\n const rel = normalized.slice(home.length).replace(/^\\//, \"\");\n const depth = rel ? rel.split(\"/\").length : 0;\n if (depth === 0) return true;\n }\n if (existsSync(join(normalized, \".git\"))) return true;\n return false;\n}\n\nfunction yieldToEventLoop(): Promise<void> {\n return new Promise((resolve) => setImmediate(resolve));\n}\n\n// ---------------------------------------------------------------------------\n// File indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\n/**\n * Index a single file through the StorageBackend interface.\n * Returns true if the file was re-indexed (changed or new), false if skipped.\n */\nexport async function indexFileWithBackend(\n backend: StorageBackend,\n projectId: number,\n rootPath: string,\n relativePath: string,\n source: string,\n tier: string,\n): Promise<boolean> {\n const absPath = join(rootPath, relativePath);\n\n let content: string;\n let stat: ReturnType<typeof statSync>;\n try {\n content = readFileSync(absPath, \"utf8\");\n stat = statSync(absPath);\n } catch {\n return false;\n }\n\n const hash = sha256File(content);\n const mtime = Math.floor(stat.mtimeMs);\n const size = stat.size;\n\n // Change detection\n const existingHash = await backend.getFileHash(projectId, relativePath);\n if (existingHash === hash) return false;\n\n // Delete old chunks\n await backend.deleteChunksForFile(projectId, relativePath);\n\n // Chunk the content\n const rawChunks = chunkMarkdown(content);\n const updatedAt = Date.now();\n\n const chunks: ChunkRow[] = rawChunks.map((c, i) => ({\n id: chunkId(projectId, relativePath, i, c.startLine, c.endLine),\n projectId,\n source,\n tier,\n path: relativePath,\n startLine: c.startLine,\n endLine: c.endLine,\n hash: c.hash,\n text: c.text,\n updatedAt,\n embedding: null,\n }));\n\n // Insert chunks + update file record\n await backend.insertChunks(chunks);\n await backend.upsertFile({ projectId, path: relativePath, source, tier, hash, mtime, size });\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Project-level indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\nexport async function indexProjectWithBackend(\n backend: StorageBackend,\n projectId: number,\n rootPath: string,\n claudeNotesDir?: string | null,\n): Promise<IndexResult> {\n const result: IndexResult = { filesProcessed: 0, chunksCreated: 0, filesSkipped: 0 };\n\n const filesToIndex: Array<{ absPath: string; rootBase: string; source: string; tier: string }> = [];\n\n const rootMemoryMd = join(rootPath, \"MEMORY.md\");\n if (existsSync(rootMemoryMd)) {\n filesToIndex.push({ absPath: rootMemoryMd, rootBase: rootPath, source: \"memory\", tier: \"evergreen\" });\n }\n\n const memoryDir = join(rootPath, \"memory\");\n for (const absPath of walkMdFiles(memoryDir)) {\n const relPath = relative(rootPath, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"memory\", tier });\n }\n\n const notesDir = join(rootPath, \"Notes\");\n for (const absPath of walkMdFiles(notesDir)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic session-title chunks: parse titles from Notes filenames and insert\n // as high-signal chunks so session names are searchable via BM25 and embeddings.\n {\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(notesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(rootPath, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n const titleChunk: import(\"../storage/interface.js\").ChunkRow = {\n id, projectId, source: \"notes\", tier: \"session\",\n path: syntheticPath, startLine: 0, endLine: 0,\n hash, text, updatedAt, embedding: null,\n };\n await backend.insertChunks([titleChunk]);\n }\n }\n\n if (!isPathTooBroadForContentScan(rootPath)) {\n for (const absPath of walkContentFiles(rootPath)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"content\", tier: \"topic\" });\n }\n }\n\n if (claudeNotesDir && claudeNotesDir !== notesDir) {\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n filesToIndex.push({ absPath, rootBase: claudeNotesDir, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic title chunks for claude notes dir\n {\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(claudeNotesDir, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n const titleChunk: import(\"../storage/interface.js\").ChunkRow = {\n id, projectId, source: \"notes\", tier: \"session\",\n path: syntheticPath, startLine: 0, endLine: 0,\n hash, text, updatedAt, embedding: null,\n };\n await backend.insertChunks([titleChunk]);\n }\n }\n\n if (claudeNotesDir.endsWith(\"/Notes\")) {\n const claudeProjectDir = claudeNotesDir.slice(0, -\"/Notes\".length);\n const claudeMemoryMd = join(claudeProjectDir, \"MEMORY.md\");\n if (existsSync(claudeMemoryMd)) {\n filesToIndex.push({ absPath: claudeMemoryMd, rootBase: claudeProjectDir, source: \"memory\", tier: \"evergreen\" });\n }\n const claudeMemoryDir = join(claudeProjectDir, \"memory\");\n for (const absPath of walkMdFiles(claudeMemoryDir)) {\n const relPath = relative(claudeProjectDir, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: claudeProjectDir, source: \"memory\", tier });\n }\n }\n }\n\n await yieldToEventLoop();\n\n let filesSinceYield = 0;\n\n for (const { absPath, rootBase, source, tier } of filesToIndex) {\n if (filesSinceYield >= INDEX_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const relPath = relative(rootBase, absPath);\n const changed = await indexFileWithBackend(backend, projectId, rootBase, relPath, source, tier);\n\n if (changed) {\n // Count chunks — we know we just inserted them, count from the chunk IDs\n const ids = await backend.getChunkIds(projectId, relPath);\n result.filesProcessed++;\n result.chunksCreated += ids.length;\n } else {\n result.filesSkipped++;\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Embedding generation via StorageBackend\n// ---------------------------------------------------------------------------\n\nconst EMBED_BATCH_SIZE = 50;\nconst EMBED_YIELD_EVERY = 10;\n\n/**\n * Generate and store embeddings for all unembedded chunks via the StorageBackend.\n *\n * Processes chunks in batches of EMBED_BATCH_SIZE, yielding to the event loop\n * every EMBED_YIELD_EVERY chunks to avoid blocking IPC calls from MCP shims.\n *\n * The optional `shouldStop` callback is checked between every batch. When it\n * returns true the embed loop exits early so the caller (e.g. the daemon\n * shutdown handler) can close the pool without racing against active queries.\n *\n * Returns the number of newly embedded chunks.\n */\nexport async function embedChunksWithBackend(\n backend: StorageBackend,\n shouldStop?: () => boolean,\n): Promise<number> {\n const { generateEmbedding, serializeEmbedding } = await import(\"./embeddings.js\");\n\n const rows = await backend.getUnembeddedChunkIds();\n if (rows.length === 0) return 0;\n\n const total = rows.length;\n let embedded = 0;\n\n for (let i = 0; i < rows.length; i += EMBED_BATCH_SIZE) {\n // Check cancellation between every batch before touching the pool again\n if (shouldStop?.()) {\n process.stderr.write(\n `[pai-daemon] Embed pass cancelled after ${embedded}/${total} chunks (shutdown requested)\\n`\n );\n break;\n }\n\n const batch = rows.slice(i, i + EMBED_BATCH_SIZE);\n\n for (let j = 0; j < batch.length; j++) {\n const { id, text } = batch[j];\n\n // Yield to the event loop periodically to keep IPC responsive\n if ((embedded + j) % EMBED_YIELD_EVERY === 0) {\n await yieldToEventLoop();\n }\n\n const vec = await generateEmbedding(text);\n const blob = serializeEmbedding(vec);\n await backend.updateEmbedding(id, blob);\n }\n\n embedded += batch.length;\n process.stderr.write(\n `[pai-daemon] Embedded ${embedded}/${total} chunks\\n`\n );\n }\n\n return embedded;\n}\n\n// ---------------------------------------------------------------------------\n// Global indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\nexport async function indexAllWithBackend(\n backend: StorageBackend,\n registryDb: Database,\n): Promise<{ projects: number; result: IndexResult }> {\n const projects = registryDb\n .prepare(\"SELECT id, root_path, claude_notes_dir FROM projects WHERE status = 'active'\")\n .all() as Array<{ id: number; root_path: string; claude_notes_dir: string | null }>;\n\n const totals: IndexResult = { filesProcessed: 0, chunksCreated: 0, filesSkipped: 0 };\n\n for (const project of projects) {\n await yieldToEventLoop();\n const r = await indexProjectWithBackend(backend, project.id, project.root_path, project.claude_notes_dir);\n totals.filesProcessed += r.filesProcessed;\n totals.chunksCreated += r.chunksCreated;\n totals.filesSkipped += r.filesSkipped;\n }\n\n return { projects: projects.length, result: totals };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"indexer-backend-DQO-FqAI.mjs","names":[],"sources":["../src/memory/indexer-backend.ts"],"sourcesContent":["/**\n * Backend-aware indexer for PAI federation memory.\n *\n * This module provides the same functionality as indexer.ts but writes\n * through the StorageBackend interface instead of directly to better-sqlite3.\n * Used when the daemon is configured with the Postgres backend.\n *\n * The SQLite path still uses indexer.ts directly (which is faster for SQLite\n * due to synchronous transactions).\n */\n\nimport { createHash } from \"node:crypto\";\nimport { readFileSync, statSync, readdirSync, existsSync } from \"node:fs\";\nimport { join, relative, basename, normalize } from \"node:path\";\n\n// ---------------------------------------------------------------------------\n// Session title parsing\n// ---------------------------------------------------------------------------\n\nconst SESSION_TITLE_RE = /^(\\d{4})\\s*-\\s*(\\d{4}-\\d{2}-\\d{2})\\s*-\\s*(.+)\\.md$/;\n\n/**\n * Parse a session title from a Notes filename.\n * Format: \"NNNN - YYYY-MM-DD - Descriptive Title.md\"\n * Returns a synthetic chunk text like \"Session #0086 2026-02-23: Pai Daemon Background Service\"\n * or null if the filename doesn't match the expected pattern.\n */\nexport function parseSessionTitleChunk(fileName: string): string | null {\n const m = SESSION_TITLE_RE.exec(fileName);\n if (!m) return null;\n const [, num, date, title] = m;\n return `Session #${num} ${date}: ${title}`;\n}\nimport { homedir } from \"node:os\";\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend, ChunkRow } from \"../storage/interface.js\";\nimport type { IndexResult } from \"./indexer.js\";\nimport { chunkMarkdown } from \"./chunker.js\";\nimport { detectTier } from \"./indexer.js\";\n\n// ---------------------------------------------------------------------------\n// Constants (mirrored from indexer.ts)\n// ---------------------------------------------------------------------------\n\nconst MAX_FILES_PER_PROJECT = 5_000;\nconst MAX_WALK_DEPTH = 6;\nconst INDEX_YIELD_EVERY = 10;\n\n/**\n * Directories to ALWAYS skip, at any depth, during any directory walk.\n * These are build artifacts, dependency trees, and VCS internals that\n * should never be indexed regardless of where they appear in the tree.\n */\nconst ALWAYS_SKIP_DIRS = new Set([\n // Version control\n \".git\",\n // Dependency directories (any language)\n \"node_modules\",\n \"vendor\",\n \"Pods\", // CocoaPods (iOS/macOS)\n // Build / compile output\n \"dist\",\n \"build\",\n \"out\",\n \"DerivedData\", // Xcode\n \".next\", // Next.js\n // Python virtual environments and caches\n \".venv\",\n \"venv\",\n \"__pycache__\",\n // General caches\n \".cache\",\n \".bun\",\n]);\n\nconst ROOT_SCAN_SKIP_DIRS = new Set([\n \"memory\", \"Notes\", \".claude\", \".DS_Store\",\n ...ALWAYS_SKIP_DIRS,\n]);\n\nconst CONTENT_SCAN_SKIP_DIRS = new Set([\n \"Library\", \"Applications\", \"Music\", \"Movies\", \"Pictures\", \"Desktop\",\n \"Downloads\", \"Public\", \"coverage\",\n ...ALWAYS_SKIP_DIRS,\n]);\n\n// ---------------------------------------------------------------------------\n// Helpers (same logic as indexer.ts)\n// ---------------------------------------------------------------------------\n\nfunction sha256File(content: string): string {\n return createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\nfunction chunkId(\n projectId: number,\n path: string,\n chunkIndex: number,\n startLine: number,\n endLine: number,\n): string {\n return createHash(\"sha256\")\n .update(`${projectId}:${path}:${chunkIndex}:${startLine}:${endLine}`)\n .digest(\"hex\");\n}\n\nfunction walkMdFiles(\n dir: string,\n acc?: string[],\n cap = MAX_FILES_PER_PROJECT,\n depth = 0,\n): string[] {\n const results = acc ?? [];\n if (!existsSync(dir)) return results;\n if (results.length >= cap) return results;\n if (depth > MAX_WALK_DEPTH) return results;\n try {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (results.length >= cap) break;\n if (entry.isSymbolicLink()) continue;\n // Skip known junk directories at every recursion depth\n if (ALWAYS_SKIP_DIRS.has(entry.name)) continue;\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n walkMdFiles(full, results, cap, depth + 1);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n results.push(full);\n }\n }\n } catch { /* skip unreadable */ }\n return results;\n}\n\nfunction walkContentFiles(rootPath: string): string[] {\n if (!existsSync(rootPath)) return [];\n const results: string[] = [];\n try {\n for (const entry of readdirSync(rootPath, { withFileTypes: true })) {\n if (results.length >= MAX_FILES_PER_PROJECT) break;\n if (entry.isSymbolicLink()) continue;\n if (ROOT_SCAN_SKIP_DIRS.has(entry.name)) continue;\n if (CONTENT_SCAN_SKIP_DIRS.has(entry.name)) continue;\n const full = join(rootPath, entry.name);\n if (entry.isDirectory()) {\n walkMdFiles(full, results, MAX_FILES_PER_PROJECT);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n if (entry.name !== \"MEMORY.md\") results.push(full);\n }\n }\n } catch { /* skip */ }\n return results;\n}\n\nfunction isPathTooBroadForContentScan(rootPath: string): boolean {\n const normalized = normalize(rootPath);\n const home = homedir();\n if (home.startsWith(normalized) || normalized === \"/\") return true;\n if (normalized.startsWith(home)) {\n const rel = normalized.slice(home.length).replace(/^\\//, \"\");\n const depth = rel ? rel.split(\"/\").length : 0;\n if (depth === 0) return true;\n }\n if (existsSync(join(normalized, \".git\"))) return true;\n return false;\n}\n\nfunction yieldToEventLoop(): Promise<void> {\n return new Promise((resolve) => setImmediate(resolve));\n}\n\n// ---------------------------------------------------------------------------\n// File indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\n/**\n * Index a single file through the StorageBackend interface.\n * Returns true if the file was re-indexed (changed or new), false if skipped.\n */\nexport async function indexFileWithBackend(\n backend: StorageBackend,\n projectId: number,\n rootPath: string,\n relativePath: string,\n source: string,\n tier: string,\n): Promise<boolean> {\n const absPath = join(rootPath, relativePath);\n\n let content: string;\n let stat: ReturnType<typeof statSync>;\n try {\n content = readFileSync(absPath, \"utf8\");\n stat = statSync(absPath);\n } catch {\n return false;\n }\n\n const hash = sha256File(content);\n const mtime = Math.floor(stat.mtimeMs);\n const size = stat.size;\n\n // Change detection\n const existingHash = await backend.getFileHash(projectId, relativePath);\n if (existingHash === hash) return false;\n\n // Delete old chunks\n await backend.deleteChunksForFile(projectId, relativePath);\n\n // Chunk the content\n const rawChunks = chunkMarkdown(content);\n const updatedAt = Date.now();\n\n const chunks: ChunkRow[] = rawChunks.map((c, i) => ({\n id: chunkId(projectId, relativePath, i, c.startLine, c.endLine),\n projectId,\n source,\n tier,\n path: relativePath,\n startLine: c.startLine,\n endLine: c.endLine,\n hash: c.hash,\n text: c.text,\n updatedAt,\n embedding: null,\n }));\n\n // Insert chunks + update file record\n await backend.insertChunks(chunks);\n await backend.upsertFile({ projectId, path: relativePath, source, tier, hash, mtime, size });\n\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Project-level indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\nexport async function indexProjectWithBackend(\n backend: StorageBackend,\n projectId: number,\n rootPath: string,\n claudeNotesDir?: string | null,\n): Promise<IndexResult> {\n const result: IndexResult = { filesProcessed: 0, chunksCreated: 0, filesSkipped: 0 };\n\n const filesToIndex: Array<{ absPath: string; rootBase: string; source: string; tier: string }> = [];\n\n const rootMemoryMd = join(rootPath, \"MEMORY.md\");\n if (existsSync(rootMemoryMd)) {\n filesToIndex.push({ absPath: rootMemoryMd, rootBase: rootPath, source: \"memory\", tier: \"evergreen\" });\n }\n\n const memoryDir = join(rootPath, \"memory\");\n for (const absPath of walkMdFiles(memoryDir)) {\n const relPath = relative(rootPath, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"memory\", tier });\n }\n\n const notesDir = join(rootPath, \"Notes\");\n for (const absPath of walkMdFiles(notesDir)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic session-title chunks: parse titles from Notes filenames and insert\n // as high-signal chunks so session names are searchable via BM25 and embeddings.\n {\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(notesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(rootPath, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n const titleChunk: import(\"../storage/interface.js\").ChunkRow = {\n id, projectId, source: \"notes\", tier: \"session\",\n path: syntheticPath, startLine: 0, endLine: 0,\n hash, text, updatedAt, embedding: null,\n };\n await backend.insertChunks([titleChunk]);\n }\n }\n\n if (!isPathTooBroadForContentScan(rootPath)) {\n for (const absPath of walkContentFiles(rootPath)) {\n filesToIndex.push({ absPath, rootBase: rootPath, source: \"content\", tier: \"topic\" });\n }\n }\n\n if (claudeNotesDir && claudeNotesDir !== notesDir) {\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n filesToIndex.push({ absPath, rootBase: claudeNotesDir, source: \"notes\", tier: \"session\" });\n }\n\n // Synthetic title chunks for claude notes dir\n {\n const updatedAt = Date.now();\n for (const absPath of walkMdFiles(claudeNotesDir)) {\n const fileName = basename(absPath);\n const text = parseSessionTitleChunk(fileName);\n if (!text) continue;\n const relPath = relative(claudeNotesDir, absPath);\n const syntheticPath = `${relPath}::title`;\n const id = chunkId(projectId, syntheticPath, 0, 0, 0);\n const hash = sha256File(text);\n const titleChunk: import(\"../storage/interface.js\").ChunkRow = {\n id, projectId, source: \"notes\", tier: \"session\",\n path: syntheticPath, startLine: 0, endLine: 0,\n hash, text, updatedAt, embedding: null,\n };\n await backend.insertChunks([titleChunk]);\n }\n }\n\n if (claudeNotesDir.endsWith(\"/Notes\")) {\n const claudeProjectDir = claudeNotesDir.slice(0, -\"/Notes\".length);\n const claudeMemoryMd = join(claudeProjectDir, \"MEMORY.md\");\n if (existsSync(claudeMemoryMd)) {\n filesToIndex.push({ absPath: claudeMemoryMd, rootBase: claudeProjectDir, source: \"memory\", tier: \"evergreen\" });\n }\n const claudeMemoryDir = join(claudeProjectDir, \"memory\");\n for (const absPath of walkMdFiles(claudeMemoryDir)) {\n const relPath = relative(claudeProjectDir, absPath);\n const tier = detectTier(relPath);\n filesToIndex.push({ absPath, rootBase: claudeProjectDir, source: \"memory\", tier });\n }\n }\n }\n\n await yieldToEventLoop();\n\n let filesSinceYield = 0;\n\n for (const { absPath, rootBase, source, tier } of filesToIndex) {\n if (filesSinceYield >= INDEX_YIELD_EVERY) {\n await yieldToEventLoop();\n filesSinceYield = 0;\n }\n filesSinceYield++;\n\n const relPath = relative(rootBase, absPath);\n const changed = await indexFileWithBackend(backend, projectId, rootBase, relPath, source, tier);\n\n if (changed) {\n // Count chunks — we know we just inserted them, count from the chunk IDs\n const ids = await backend.getChunkIds(projectId, relPath);\n result.filesProcessed++;\n result.chunksCreated += ids.length;\n } else {\n result.filesSkipped++;\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Embedding generation via StorageBackend\n// ---------------------------------------------------------------------------\n\nconst EMBED_BATCH_SIZE = 50;\nconst EMBED_YIELD_EVERY = 10;\n\n/**\n * Generate and store embeddings for all unembedded chunks via the StorageBackend.\n *\n * Processes chunks in batches of EMBED_BATCH_SIZE, yielding to the event loop\n * every EMBED_YIELD_EVERY chunks to avoid blocking IPC calls from MCP shims.\n *\n * The optional `shouldStop` callback is checked between every batch. When it\n * returns true the embed loop exits early so the caller (e.g. the daemon\n * shutdown handler) can close the pool without racing against active queries.\n *\n * Returns the number of newly embedded chunks.\n */\nexport async function embedChunksWithBackend(\n backend: StorageBackend,\n shouldStop?: () => boolean,\n): Promise<number> {\n const { generateEmbedding, serializeEmbedding } = await import(\"./embeddings.js\");\n\n const rows = await backend.getUnembeddedChunkIds();\n if (rows.length === 0) return 0;\n\n const total = rows.length;\n let embedded = 0;\n\n for (let i = 0; i < rows.length; i += EMBED_BATCH_SIZE) {\n // Check cancellation between every batch before touching the pool again\n if (shouldStop?.()) {\n process.stderr.write(\n `[pai-daemon] Embed pass cancelled after ${embedded}/${total} chunks (shutdown requested)\\n`\n );\n break;\n }\n\n const batch = rows.slice(i, i + EMBED_BATCH_SIZE);\n\n for (let j = 0; j < batch.length; j++) {\n const { id, text } = batch[j];\n\n // Yield to the event loop periodically to keep IPC responsive\n if ((embedded + j) % EMBED_YIELD_EVERY === 0) {\n await yieldToEventLoop();\n }\n\n const vec = await generateEmbedding(text);\n const blob = serializeEmbedding(vec);\n await backend.updateEmbedding(id, blob);\n }\n\n embedded += batch.length;\n process.stderr.write(\n `[pai-daemon] Embedded ${embedded}/${total} chunks\\n`\n );\n }\n\n return embedded;\n}\n\n// ---------------------------------------------------------------------------\n// Global indexing via StorageBackend\n// ---------------------------------------------------------------------------\n\nexport async function indexAllWithBackend(\n backend: StorageBackend,\n registryDb: Database,\n): Promise<{ projects: number; result: IndexResult }> {\n const projects = registryDb\n .prepare(\"SELECT id, root_path, claude_notes_dir FROM projects WHERE status = 'active'\")\n .all() as Array<{ id: number; root_path: string; claude_notes_dir: string | null }>;\n\n const totals: IndexResult = { filesProcessed: 0, chunksCreated: 0, filesSkipped: 0 };\n\n for (const project of projects) {\n await yieldToEventLoop();\n const r = await indexProjectWithBackend(backend, project.id, project.root_path, project.claude_notes_dir);\n totals.filesProcessed += r.filesProcessed;\n totals.chunksCreated += r.chunksCreated;\n totals.filesSkipped += r.filesSkipped;\n }\n\n return { projects: projects.length, result: totals };\n}\n"],"mappings":"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"}
@@ -153,4 +153,4 @@ var PaiClient = class {
153
153
 
154
154
  //#endregion
155
155
  export { ipc_client_exports as n, PaiClient as t };
156
- //# sourceMappingURL=ipc-client-CLt2fNlC.mjs.map
156
+ //# sourceMappingURL=ipc-client-CgSpwHDC.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"ipc-client-CLt2fNlC.mjs","names":[],"sources":["../src/daemon/ipc-client.ts"],"sourcesContent":["/**\n * ipc-client.ts — IPC client for the PAI Daemon MCP shim\n *\n * PaiClient connects to the Unix Domain Socket served by daemon.ts\n * and forwards tool calls to the daemon. Uses a fresh socket connection per\n * call (connect → write JSON + newline → read response line → parse → destroy).\n * This keeps the client stateless and avoids connection management complexity.\n *\n * Adapted from the Coogle ipc-client pattern (which was adapted from Whazaa).\n */\n\nimport { connect, Socket } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n NotificationConfig,\n NotificationMode,\n NotificationEvent,\n SendResult,\n} from \"../notifications/types.js\";\nimport type { TopicCheckParams, TopicCheckResult } from \"../topics/detector.js\";\nimport type { AutoRouteResult } from \"../session/auto-route.js\";\n\n// ---------------------------------------------------------------------------\n// Protocol types\n// ---------------------------------------------------------------------------\n\n/** Default socket path */\nexport const IPC_SOCKET_PATH = \"/tmp/pai.sock\";\n\n/** Timeout for IPC calls (60 seconds) */\nconst IPC_TIMEOUT_MS = 60_000;\n\ninterface IpcRequest {\n id: string;\n method: string;\n params: Record<string, unknown>;\n}\n\ninterface IpcResponse {\n id: string;\n ok: boolean;\n result?: unknown;\n error?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Client\n// ---------------------------------------------------------------------------\n\n/**\n * Thin IPC proxy that forwards tool calls to pai-daemon over a Unix\n * Domain Socket. Each call opens a fresh connection, sends one NDJSON request,\n * reads the response, and closes. Stateless and simple.\n */\nexport class PaiClient {\n private readonly socketPath: string;\n\n constructor(socketPath?: string) {\n this.socketPath = socketPath ?? IPC_SOCKET_PATH;\n }\n\n /**\n * Call a PAI tool by name with the given params.\n * Returns the tool result or throws on error.\n */\n async call(method: string, params: Record<string, unknown>): Promise<unknown> {\n return this.send(method, params);\n }\n\n /**\n * Check daemon status.\n */\n async status(): Promise<Record<string, unknown>> {\n const result = await this.send(\"status\", {});\n return result as Record<string, unknown>;\n }\n\n /**\n * Trigger an immediate index run.\n */\n async triggerIndex(): Promise<void> {\n await this.send(\"index_now\", {});\n }\n\n // -------------------------------------------------------------------------\n // Notification methods\n // -------------------------------------------------------------------------\n\n /**\n * Get the current notification config from the daemon.\n */\n async getNotificationConfig(): Promise<{\n config: NotificationConfig;\n activeChannels: string[];\n }> {\n const result = await this.send(\"notification_get_config\", {});\n return result as { config: NotificationConfig; activeChannels: string[] };\n }\n\n /**\n * Patch the notification config on the daemon (and persist to disk).\n */\n async setNotificationConfig(patch: {\n mode?: NotificationMode;\n channels?: Partial<NotificationConfig[\"channels\"]>;\n routing?: Partial<NotificationConfig[\"routing\"]>;\n }): Promise<{ config: NotificationConfig }> {\n const result = await this.send(\"notification_set_config\", patch as Record<string, unknown>);\n return result as { config: NotificationConfig };\n }\n\n /**\n * Send a notification via the daemon (routes to configured channels).\n */\n async sendNotification(payload: {\n event: NotificationEvent;\n message: string;\n title?: string;\n }): Promise<SendResult> {\n const result = await this.send(\"notification_send\", payload as Record<string, unknown>);\n return result as SendResult;\n }\n\n // -------------------------------------------------------------------------\n // Topic detection methods\n // -------------------------------------------------------------------------\n\n /**\n * Check whether the provided context text has drifted to a different project\n * than the session's current routing.\n */\n async topicCheck(params: TopicCheckParams): Promise<TopicCheckResult> {\n const result = await this.send(\"topic_check\", params as unknown as Record<string, unknown>);\n return result as TopicCheckResult;\n }\n\n // -------------------------------------------------------------------------\n // Session routing methods\n // -------------------------------------------------------------------------\n\n /**\n * Automatically detect which project a session belongs to.\n * Tries path match, PAI.md marker walk, then topic detection (if context given).\n */\n async sessionAutoRoute(params: {\n cwd?: string;\n context?: string;\n }): Promise<AutoRouteResult | null> {\n // session_auto_route returns a ToolResult (content array). Extract the text\n // and parse JSON from it.\n const result = await this.send(\"session_auto_route\", params as Record<string, unknown>);\n const toolResult = result as { content?: Array<{ text: string }>; isError?: boolean };\n if (toolResult.isError) return null;\n const text = toolResult.content?.[0]?.text ?? \"\";\n // Text is either JSON (on match) or a human-readable \"no match\" message\n try {\n return JSON.parse(text) as AutoRouteResult;\n } catch {\n return null;\n }\n }\n\n // -------------------------------------------------------------------------\n // Internal transport\n // -------------------------------------------------------------------------\n\n /**\n * Send a single IPC request and wait for the response.\n * Opens a new socket connection per call — simple and reliable.\n */\n private send(\n method: string,\n params: Record<string, unknown>\n ): Promise<unknown> {\n const socketPath = this.socketPath;\n\n return new Promise((resolve, reject) => {\n let socket: Socket | null = null;\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(error: Error | null, value?: unknown): void {\n if (done) return;\n done = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n try {\n socket?.destroy();\n } catch {\n // ignore\n }\n if (error) {\n reject(error);\n } else {\n resolve(value);\n }\n }\n\n socket = connect(socketPath, () => {\n const request: IpcRequest = {\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\n const line = buffer.slice(0, nl);\n buffer = buffer.slice(nl + 1);\n\n let response: IpcResponse;\n try {\n response = JSON.parse(line) as IpcResponse;\n } catch {\n finish(new Error(`IPC parse error: ${line}`));\n return;\n }\n\n if (!response.ok) {\n finish(new Error(response.error ?? \"IPC call failed\"));\n } else {\n finish(null, response.result);\n }\n });\n\n socket.on(\"error\", (e: NodeJS.ErrnoException) => {\n if (e.code === \"ENOENT\" || e.code === \"ECONNREFUSED\") {\n finish(\n new Error(\n \"PAI daemon not running. Start it with: pai daemon serve\"\n )\n );\n } else {\n finish(e);\n }\n });\n\n socket.on(\"end\", () => {\n if (!done) {\n finish(new Error(\"IPC connection closed before response\"));\n }\n });\n\n timer = setTimeout(() => {\n finish(new Error(\"IPC call timed out after 60s\"));\n }, IPC_TIMEOUT_MS);\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,kBAAkB;;AAG/B,MAAM,iBAAiB;;;;;;AAwBvB,IAAa,YAAb,MAAuB;CACrB,AAAiB;CAEjB,YAAY,YAAqB;AAC/B,OAAK,aAAa,cAAc;;;;;;CAOlC,MAAM,KAAK,QAAgB,QAAmD;AAC5E,SAAO,KAAK,KAAK,QAAQ,OAAO;;;;;CAMlC,MAAM,SAA2C;AAE/C,SADe,MAAM,KAAK,KAAK,UAAU,EAAE,CAAC;;;;;CAO9C,MAAM,eAA8B;AAClC,QAAM,KAAK,KAAK,aAAa,EAAE,CAAC;;;;;CAUlC,MAAM,wBAGH;AAED,SADe,MAAM,KAAK,KAAK,2BAA2B,EAAE,CAAC;;;;;CAO/D,MAAM,sBAAsB,OAIgB;AAE1C,SADe,MAAM,KAAK,KAAK,2BAA2B,MAAiC;;;;;CAO7F,MAAM,iBAAiB,SAIC;AAEtB,SADe,MAAM,KAAK,KAAK,qBAAqB,QAAmC;;;;;;CAYzF,MAAM,WAAW,QAAqD;AAEpE,SADe,MAAM,KAAK,KAAK,eAAe,OAA6C;;;;;;CAY7F,MAAM,iBAAiB,QAGa;EAIlC,MAAM,aADS,MAAM,KAAK,KAAK,sBAAsB,OAAkC;AAEvF,MAAI,WAAW,QAAS,QAAO;EAC/B,MAAM,OAAO,WAAW,UAAU,IAAI,QAAQ;AAE9C,MAAI;AACF,UAAO,KAAK,MAAM,KAAK;UACjB;AACN,UAAO;;;;;;;CAYX,AAAQ,KACN,QACA,QACkB;EAClB,MAAM,aAAa,KAAK;AAExB,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,IAAI,SAAwB;GAC5B,IAAI,OAAO;GACX,IAAI,SAAS;GACb,IAAI,QAA8C;GAElD,SAAS,OAAO,OAAqB,OAAuB;AAC1D,QAAI,KAAM;AACV,WAAO;AACP,QAAI,UAAU,MAAM;AAClB,kBAAa,MAAM;AACnB,aAAQ;;AAEV,QAAI;AACF,aAAQ,SAAS;YACX;AAGR,QAAI,MACF,QAAO,MAAM;QAEb,SAAQ,MAAM;;AAIlB,YAAS,QAAQ,kBAAkB;IACjC,MAAM,UAAsB;KAC1B,IAAI,YAAY;KAChB;KACA;KACD;AACD,WAAQ,MAAM,KAAK,UAAU,QAAQ,GAAG,KAAK;KAC7C;AAEF,UAAO,GAAG,SAAS,UAAkB;AACnC,cAAU,MAAM,UAAU;IAC1B,MAAM,KAAK,OAAO,QAAQ,KAAK;AAC/B,QAAI,OAAO,GAAI;IAEf,MAAM,OAAO,OAAO,MAAM,GAAG,GAAG;AAChC,aAAS,OAAO,MAAM,KAAK,EAAE;IAE7B,IAAI;AACJ,QAAI;AACF,gBAAW,KAAK,MAAM,KAAK;YACrB;AACN,4BAAO,IAAI,MAAM,oBAAoB,OAAO,CAAC;AAC7C;;AAGF,QAAI,CAAC,SAAS,GACZ,QAAO,IAAI,MAAM,SAAS,SAAS,kBAAkB,CAAC;QAEtD,QAAO,MAAM,SAAS,OAAO;KAE/B;AAEF,UAAO,GAAG,UAAU,MAA6B;AAC/C,QAAI,EAAE,SAAS,YAAY,EAAE,SAAS,eACpC,wBACE,IAAI,MACF,0DACD,CACF;QAED,QAAO,EAAE;KAEX;AAEF,UAAO,GAAG,aAAa;AACrB,QAAI,CAAC,KACH,wBAAO,IAAI,MAAM,wCAAwC,CAAC;KAE5D;AAEF,WAAQ,iBAAiB;AACvB,2BAAO,IAAI,MAAM,+BAA+B,CAAC;MAChD,eAAe;IAClB"}
1
+ {"version":3,"file":"ipc-client-CgSpwHDC.mjs","names":[],"sources":["../src/daemon/ipc-client.ts"],"sourcesContent":["/**\n * ipc-client.ts — IPC client for the PAI Daemon MCP shim\n *\n * PaiClient connects to the Unix Domain Socket served by daemon.ts\n * and forwards tool calls to the daemon. Uses a fresh socket connection per\n * call (connect → write JSON + newline → read response line → parse → destroy).\n * This keeps the client stateless and avoids connection management complexity.\n *\n * Adapted from the Coogle ipc-client pattern (which was adapted from Whazaa).\n */\n\nimport { connect, Socket } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n NotificationConfig,\n NotificationMode,\n NotificationEvent,\n SendResult,\n} from \"../notifications/types.js\";\nimport type { TopicCheckParams, TopicCheckResult } from \"../topics/detector.js\";\nimport type { AutoRouteResult } from \"../session/auto-route.js\";\n\n// ---------------------------------------------------------------------------\n// Protocol types\n// ---------------------------------------------------------------------------\n\n/** Default socket path */\nexport const IPC_SOCKET_PATH = \"/tmp/pai.sock\";\n\n/** Timeout for IPC calls (60 seconds) */\nconst IPC_TIMEOUT_MS = 60_000;\n\ninterface IpcRequest {\n id: string;\n method: string;\n params: Record<string, unknown>;\n}\n\ninterface IpcResponse {\n id: string;\n ok: boolean;\n result?: unknown;\n error?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Client\n// ---------------------------------------------------------------------------\n\n/**\n * Thin IPC proxy that forwards tool calls to pai-daemon over a Unix\n * Domain Socket. Each call opens a fresh connection, sends one NDJSON request,\n * reads the response, and closes. Stateless and simple.\n */\nexport class PaiClient {\n private readonly socketPath: string;\n\n constructor(socketPath?: string) {\n this.socketPath = socketPath ?? IPC_SOCKET_PATH;\n }\n\n /**\n * Call a PAI tool by name with the given params.\n * Returns the tool result or throws on error.\n */\n async call(method: string, params: Record<string, unknown>): Promise<unknown> {\n return this.send(method, params);\n }\n\n /**\n * Check daemon status.\n */\n async status(): Promise<Record<string, unknown>> {\n const result = await this.send(\"status\", {});\n return result as Record<string, unknown>;\n }\n\n /**\n * Trigger an immediate index run.\n */\n async triggerIndex(): Promise<void> {\n await this.send(\"index_now\", {});\n }\n\n // -------------------------------------------------------------------------\n // Notification methods\n // -------------------------------------------------------------------------\n\n /**\n * Get the current notification config from the daemon.\n */\n async getNotificationConfig(): Promise<{\n config: NotificationConfig;\n activeChannels: string[];\n }> {\n const result = await this.send(\"notification_get_config\", {});\n return result as { config: NotificationConfig; activeChannels: string[] };\n }\n\n /**\n * Patch the notification config on the daemon (and persist to disk).\n */\n async setNotificationConfig(patch: {\n mode?: NotificationMode;\n channels?: Partial<NotificationConfig[\"channels\"]>;\n routing?: Partial<NotificationConfig[\"routing\"]>;\n }): Promise<{ config: NotificationConfig }> {\n const result = await this.send(\"notification_set_config\", patch as Record<string, unknown>);\n return result as { config: NotificationConfig };\n }\n\n /**\n * Send a notification via the daemon (routes to configured channels).\n */\n async sendNotification(payload: {\n event: NotificationEvent;\n message: string;\n title?: string;\n }): Promise<SendResult> {\n const result = await this.send(\"notification_send\", payload as Record<string, unknown>);\n return result as SendResult;\n }\n\n // -------------------------------------------------------------------------\n // Topic detection methods\n // -------------------------------------------------------------------------\n\n /**\n * Check whether the provided context text has drifted to a different project\n * than the session's current routing.\n */\n async topicCheck(params: TopicCheckParams): Promise<TopicCheckResult> {\n const result = await this.send(\"topic_check\", params as unknown as Record<string, unknown>);\n return result as TopicCheckResult;\n }\n\n // -------------------------------------------------------------------------\n // Session routing methods\n // -------------------------------------------------------------------------\n\n /**\n * Automatically detect which project a session belongs to.\n * Tries path match, PAI.md marker walk, then topic detection (if context given).\n */\n async sessionAutoRoute(params: {\n cwd?: string;\n context?: string;\n }): Promise<AutoRouteResult | null> {\n // session_auto_route returns a ToolResult (content array). Extract the text\n // and parse JSON from it.\n const result = await this.send(\"session_auto_route\", params as Record<string, unknown>);\n const toolResult = result as { content?: Array<{ text: string }>; isError?: boolean };\n if (toolResult.isError) return null;\n const text = toolResult.content?.[0]?.text ?? \"\";\n // Text is either JSON (on match) or a human-readable \"no match\" message\n try {\n return JSON.parse(text) as AutoRouteResult;\n } catch {\n return null;\n }\n }\n\n // -------------------------------------------------------------------------\n // Internal transport\n // -------------------------------------------------------------------------\n\n /**\n * Send a single IPC request and wait for the response.\n * Opens a new socket connection per call — simple and reliable.\n */\n private send(\n method: string,\n params: Record<string, unknown>\n ): Promise<unknown> {\n const socketPath = this.socketPath;\n\n return new Promise((resolve, reject) => {\n let socket: Socket | null = null;\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(error: Error | null, value?: unknown): void {\n if (done) return;\n done = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n try {\n socket?.destroy();\n } catch {\n // ignore\n }\n if (error) {\n reject(error);\n } else {\n resolve(value);\n }\n }\n\n socket = connect(socketPath, () => {\n const request: IpcRequest = {\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\n const line = buffer.slice(0, nl);\n buffer = buffer.slice(nl + 1);\n\n let response: IpcResponse;\n try {\n response = JSON.parse(line) as IpcResponse;\n } catch {\n finish(new Error(`IPC parse error: ${line}`));\n return;\n }\n\n if (!response.ok) {\n finish(new Error(response.error ?? \"IPC call failed\"));\n } else {\n finish(null, response.result);\n }\n });\n\n socket.on(\"error\", (e: NodeJS.ErrnoException) => {\n if (e.code === \"ENOENT\" || e.code === \"ECONNREFUSED\") {\n finish(\n new Error(\n \"PAI daemon not running. Start it with: pai daemon serve\"\n )\n );\n } else {\n finish(e);\n }\n });\n\n socket.on(\"end\", () => {\n if (!done) {\n finish(new Error(\"IPC connection closed before response\"));\n }\n });\n\n timer = setTimeout(() => {\n finish(new Error(\"IPC call timed out after 60s\"));\n }, IPC_TIMEOUT_MS);\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,kBAAkB;;AAG/B,MAAM,iBAAiB;;;;;;AAwBvB,IAAa,YAAb,MAAuB;CACrB,AAAiB;CAEjB,YAAY,YAAqB;AAC/B,OAAK,aAAa,cAAc;;;;;;CAOlC,MAAM,KAAK,QAAgB,QAAmD;AAC5E,SAAO,KAAK,KAAK,QAAQ,OAAO;;;;;CAMlC,MAAM,SAA2C;AAE/C,SADe,MAAM,KAAK,KAAK,UAAU,EAAE,CAAC;;;;;CAO9C,MAAM,eAA8B;AAClC,QAAM,KAAK,KAAK,aAAa,EAAE,CAAC;;;;;CAUlC,MAAM,wBAGH;AAED,SADe,MAAM,KAAK,KAAK,2BAA2B,EAAE,CAAC;;;;;CAO/D,MAAM,sBAAsB,OAIgB;AAE1C,SADe,MAAM,KAAK,KAAK,2BAA2B,MAAiC;;;;;CAO7F,MAAM,iBAAiB,SAIC;AAEtB,SADe,MAAM,KAAK,KAAK,qBAAqB,QAAmC;;;;;;CAYzF,MAAM,WAAW,QAAqD;AAEpE,SADe,MAAM,KAAK,KAAK,eAAe,OAA6C;;;;;;CAY7F,MAAM,iBAAiB,QAGa;EAIlC,MAAM,aADS,MAAM,KAAK,KAAK,sBAAsB,OAAkC;AAEvF,MAAI,WAAW,QAAS,QAAO;EAC/B,MAAM,OAAO,WAAW,UAAU,IAAI,QAAQ;AAE9C,MAAI;AACF,UAAO,KAAK,MAAM,KAAK;UACjB;AACN,UAAO;;;;;;;CAYX,AAAQ,KACN,QACA,QACkB;EAClB,MAAM,aAAa,KAAK;AAExB,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,IAAI,SAAwB;GAC5B,IAAI,OAAO;GACX,IAAI,SAAS;GACb,IAAI,QAA8C;GAElD,SAAS,OAAO,OAAqB,OAAuB;AAC1D,QAAI,KAAM;AACV,WAAO;AACP,QAAI,UAAU,MAAM;AAClB,kBAAa,MAAM;AACnB,aAAQ;;AAEV,QAAI;AACF,aAAQ,SAAS;YACX;AAGR,QAAI,MACF,QAAO,MAAM;QAEb,SAAQ,MAAM;;AAIlB,YAAS,QAAQ,kBAAkB;IACjC,MAAM,UAAsB;KAC1B,IAAI,YAAY;KAChB;KACA;KACD;AACD,WAAQ,MAAM,KAAK,UAAU,QAAQ,GAAG,KAAK;KAC7C;AAEF,UAAO,GAAG,SAAS,UAAkB;AACnC,cAAU,MAAM,UAAU;IAC1B,MAAM,KAAK,OAAO,QAAQ,KAAK;AAC/B,QAAI,OAAO,GAAI;IAEf,MAAM,OAAO,OAAO,MAAM,GAAG,GAAG;AAChC,aAAS,OAAO,MAAM,KAAK,EAAE;IAE7B,IAAI;AACJ,QAAI;AACF,gBAAW,KAAK,MAAM,KAAK;YACrB;AACN,4BAAO,IAAI,MAAM,oBAAoB,OAAO,CAAC;AAC7C;;AAGF,QAAI,CAAC,SAAS,GACZ,QAAO,IAAI,MAAM,SAAS,SAAS,kBAAkB,CAAC;QAEtD,QAAO,MAAM,SAAS,OAAO;KAE/B;AAEF,UAAO,GAAG,UAAU,MAA6B;AAC/C,QAAI,EAAE,SAAS,YAAY,EAAE,SAAS,eACpC,wBACE,IAAI,MACF,0DACD,CACF;QAED,QAAO,EAAE;KAEX;AAEF,UAAO,GAAG,aAAa;AACrB,QAAI,CAAC,KACH,wBAAO,IAAI,MAAM,wCAAwC,CAAC;KAE5D;AAEF,WAAQ,iBAAiB;AACvB,2BAAO,IAAI,MAAM,+BAA+B,CAAC;MAChD,eAAe;IAClB"}
@@ -2,9 +2,9 @@
2
2
  import { n as openRegistry } from "../db-4lSqLFb8.mjs";
3
3
  import { n as openFederation } from "../db-Dp8VXIMR.mjs";
4
4
  import "../embeddings-DGRAPAYb.mjs";
5
- import "../search-GK0ibTJy.mjs";
6
- import { a as record, i as number, n as array, o as string, r as boolean, s as unknown, t as _enum } from "../schemas-BY3Pjvje.mjs";
7
- import { _ as toolZettelSurprise, a as toolProjectHealth, d as toolSessionRoute, f as toolTopicDetect, g as toolZettelSuggest, h as toolZettelHealth, i as toolProjectDetect, l as toolRegistrySearch, m as toolZettelExplore, n as toolMemorySearch, o as toolProjectInfo, p as toolZettelConverse, r as toolNotificationConfig, s as toolProjectList, t as toolMemoryGet, u as toolSessionList, v as toolZettelThemes } from "../tools-CUg0Lyg-.mjs";
5
+ import "../search-_oHfguA5.mjs";
6
+ import { a as record, i as number, n as array, o as string, r as boolean, s as unknown, t as _enum } from "../schemas-BFIgGntb.mjs";
7
+ import { _ as toolZettelSurprise, a as toolProjectHealth, d as toolSessionRoute, f as toolTopicDetect, g as toolZettelSuggest, h as toolZettelHealth, i as toolProjectDetect, l as toolRegistrySearch, m as toolZettelExplore, n as toolMemorySearch, o as toolProjectInfo, p as toolZettelConverse, r as toolNotificationConfig, s as toolProjectList, t as toolMemoryGet, u as toolSessionList, v as toolZettelThemes } from "../tools-Dx7GjOHd.mjs";
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
10
 
@@ -55,6 +55,11 @@ async function startMcpServer() {
55
55
  " semantic — Cosine similarity over vector embeddings (requires prior embed run)",
56
56
  " hybrid — Normalized combination of BM25 + cosine (best quality)",
57
57
  "",
58
+ "Reranking is ON by default — results are re-scored with a cross-encoder model for better relevance.",
59
+ "Set rerank=false to skip reranking (faster but less accurate ordering).",
60
+ "",
61
+ "Recency boost optionally down-weights older results (recency_boost=90 means scores halve every 90 days).",
62
+ "",
58
63
  "Returns ranked snippets with project slug, file path, line range, and score.",
59
64
  "Higher score = more relevant."
60
65
  ].join("\n"), {
@@ -67,9 +72,14 @@ async function startMcpServer() {
67
72
  "keyword",
68
73
  "semantic",
69
74
  "hybrid"
70
- ]).optional().describe("Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined).")
75
+ ]).optional().describe("Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined)."),
76
+ rerank: boolean().optional().describe("Rerank results using a cross-encoder model for better relevance. Default: true. Set to false to skip reranking for faster but less accurate results."),
77
+ recency_boost: number().int().min(0).max(365).optional().describe("Apply recency boost: score halves every N days. 0 = off (default). Recommended: 90 (3 months). Applied after reranking.")
71
78
  }, async (args) => {
72
- const result = await toolMemorySearch(getRegistryDb(), getFederationDb(), args);
79
+ const result = await toolMemorySearch(getRegistryDb(), getFederationDb(), {
80
+ ...args,
81
+ recencyBoost: args.recency_boost
82
+ });
73
83
  return {
74
84
  content: result.content.map((c) => ({
75
85
  type: c.type,
@@ -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","z\n .record","z.unknown"],"sources":["../../src/mcp/server.ts","../../src/mcp/index.ts"],"sourcesContent":["/**\n * PAI Knowledge OS — MCP Server (Phase 3)\n *\n * Exposes PAI registry and memory as MCP tools callable by Claude Code.\n *\n * Tools:\n * memory_search — BM25 search across indexed memory/notes\n * memory_get — Read a specific file or lines from a project\n * project_info — Get details for a project (by slug or current dir)\n * project_list — List projects with optional filters\n * session_list — List sessions for a project\n * registry_search — Full-text search over project slugs/names/paths\n * project_detect — Detect which project a path belongs to\n * project_health — Audit all projects for moved/deleted directories\n * session_route — Auto-route session to project (path/marker/topic)\n *\n * NOTE: All tool logic lives in tools.ts (shared with the daemon).\n * This file wires MCP schema definitions to those pure functions.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { openRegistry } from \"../registry/db.js\";\nimport { openFederation } from \"../memory/db.js\";\nimport {\n toolMemorySearch,\n toolMemoryGet,\n toolProjectInfo,\n toolProjectList,\n toolSessionList,\n toolRegistrySearch,\n toolProjectDetect,\n toolProjectHealth,\n toolNotificationConfig,\n toolTopicDetect,\n toolSessionRoute,\n toolZettelExplore,\n toolZettelHealth,\n toolZettelSurprise,\n toolZettelSuggest,\n toolZettelConverse,\n toolZettelThemes,\n} from \"./tools.js\";\n\n// ---------------------------------------------------------------------------\n// Database singletons (opened lazily, once per MCP server process)\n// ---------------------------------------------------------------------------\n\nlet _registryDb: ReturnType<typeof openRegistry> | null = null;\nlet _federationDb: ReturnType<typeof openFederation> | null = null;\n\nfunction getRegistryDb() {\n if (!_registryDb) _registryDb = openRegistry();\n return _registryDb;\n}\n\nfunction getFederationDb() {\n if (!_federationDb) _federationDb = openFederation();\n return _federationDb;\n}\n\n// ---------------------------------------------------------------------------\n// MCP server startup\n// ---------------------------------------------------------------------------\n\nexport async function startMcpServer(): 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) => {\n const result = await toolMemorySearch(\n getRegistryDb(),\n getFederationDb(),\n args\n );\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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) => {\n const result = toolMemoryGet(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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) => {\n const result = toolProjectInfo(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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) => {\n const result = toolProjectList(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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) => {\n const result = toolSessionList(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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) => {\n const result = toolRegistrySearch(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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 \"\",\n \"match_type 'exact' means the path IS the project root.\",\n \"match_type 'parent' means the path is a subdirectory of the project root.\",\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) => {\n const result = toolProjectDetect(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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 \"Use this to diagnose orphaned sessions or missing project paths.\",\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) => {\n const result = await toolProjectHealth(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: notification_config\n // -------------------------------------------------------------------------\n\n server.tool(\n \"notification_config\",\n [\n \"Query or update the PAI unified notification configuration.\",\n \"\",\n \"Actions:\",\n \" get — Return the current notification mode, active channels, and routing table.\",\n \" set — Change the notification mode or update channel/routing config.\",\n \" send — Send a notification through the configured channels.\",\n \"\",\n \"Notification modes:\",\n \" auto — Use the per-event routing table (default)\",\n \" voice — All events sent as WhatsApp voice (TTS)\",\n \" whatsapp — All events sent as WhatsApp text\",\n \" ntfy — All events sent to ntfy.sh\",\n \" macos — All events sent as macOS notifications\",\n \" cli — All events written to CLI output only\",\n \" off — Suppress all notifications\",\n \"\",\n \"Event types for send: error | progress | completion | info | debug\",\n \"\",\n \"Examples:\",\n ' { \"action\": \"get\" }',\n ' { \"action\": \"set\", \"mode\": \"voice\" }',\n ' { \"action\": \"send\", \"event\": \"completion\", \"message\": \"Done!\" }',\n ].join(\"\\n\"),\n {\n action: z\n .enum([\"get\", \"set\", \"send\"])\n .describe(\"Action: 'get' (read config), 'set' (update config), 'send' (send notification).\"),\n mode: z\n .enum([\"auto\", \"voice\", \"whatsapp\", \"ntfy\", \"macos\", \"cli\", \"off\"])\n .optional()\n .describe(\"For action=set: new notification mode.\"),\n channels: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"For action=set: partial channel config overrides as a JSON object. \" +\n 'E.g. { \"whatsapp\": { \"enabled\": true }, \"macos\": { \"enabled\": false } }'\n ),\n routing: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"For action=set: partial routing overrides as a JSON object. \" +\n 'E.g. { \"error\": [\"whatsapp\", \"macos\"], \"progress\": [\"cli\"] }'\n ),\n event: z\n .enum([\"error\", \"progress\", \"completion\", \"info\", \"debug\"])\n .optional()\n .describe(\"For action=send: event type. Default: 'info'.\"),\n message: z\n .string()\n .optional()\n .describe(\"For action=send: the notification message body.\"),\n title: z\n .string()\n .optional()\n .describe(\"For action=send: optional notification title (used by macOS and ntfy).\"),\n },\n async (args) => {\n const result = await toolNotificationConfig(args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: topic_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"topic_detect\",\n [\n \"Detect whether recent conversation context has shifted to a different project.\",\n \"\",\n \"Call this when the conversation may have drifted away from the initially-routed project.\",\n \"Provide a short summary of the recent context (last few messages or tool call results).\",\n \"\",\n \"Returns:\",\n \" shifted — true if a topic shift was detected\",\n \" current_project — the project the session is currently routed to\",\n \" suggested_project — the project that best matches the context\",\n \" confidence — [0,1] fraction of memory mass held by suggested_project\",\n \" chunks_scored — number of memory chunks that contributed to scoring\",\n \" top_matches — top-3 projects with their confidence percentages\",\n \"\",\n \"A shift is reported when confidence >= threshold (default 0.6) and the\",\n \"best-matching project differs from current_project.\",\n \"\",\n \"Use cases:\",\n \" - Call at session start to confirm routing is correct\",\n \" - Call periodically when working across multiple concerns\",\n \" - Integrate with pre-tool hooks for automatic drift detection\",\n ].join(\"\\n\"),\n {\n context: z\n .string()\n .describe(\n \"Recent conversation context: a few sentences summarising what the session has been discussing. \" +\n \"Can include file paths, feature names, commands run, or any relevant text.\"\n ),\n current_project: z\n .string()\n .optional()\n .describe(\n \"The project slug this session is currently routed to. \" +\n \"If omitted, the tool still returns the best-matching project but shifted will always be false.\"\n ),\n threshold: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .describe(\n \"Minimum confidence [0,1] to declare a shift. Default: 0.6. \" +\n \"Increase to reduce false positives. Decrease to catch subtle drifts.\"\n ),\n },\n async (args) => {\n const result = await toolTopicDetect(args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_route\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_route\",\n [\n \"Automatically detect which project this session belongs to.\",\n \"\",\n \"Call this at session start (e.g., from CLAUDE.md or a session-start hook)\",\n \"to route the session to the correct project automatically.\",\n \"\",\n \"Detection strategy (in priority order):\",\n \" 1. path — exact or parent-directory match in the project registry\",\n \" 2. marker — walk up from cwd looking for Notes/PAI.md marker files\",\n \" 3. topic — BM25 keyword search against memory (only if context provided)\",\n \"\",\n \"Returns:\",\n \" slug — the matched project slug\",\n \" display_name — human-readable project name\",\n \" root_path — absolute path to the project root\",\n \" method — how it was detected: 'path', 'marker', or 'topic'\",\n \" confidence — 1.0 for path/marker matches, BM25 fraction for topic\",\n \"\",\n \"If no match is found, returns a message explaining what was tried.\",\n \"Run 'pai project add .' to register the current directory.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Working directory to detect from. Defaults to process.cwd(). \" +\n \"Pass the session's actual working directory for accurate detection.\"\n ),\n context: z\n .string()\n .optional()\n .describe(\n \"Optional conversation context for topic-based fallback routing. \" +\n \"A few sentences summarising what the session will work on. \" +\n \"Only used if path and marker detection both fail.\"\n ),\n },\n async (args) => {\n const result = await toolSessionRoute(getRegistryDb(), getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_explore\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_explore\",\n [\n \"Explore the vault's knowledge graph using Luhmann's Folgezettel traversal.\",\n \"Follow trains of thought forward, backward, or both from a starting note.\",\n \"Classifies links as sequential (same-folder) or associative (cross-folder).\",\n ].join(\"\\n\"),\n {\n start_note: z\n .string()\n .describe(\"Path or title of the note to start traversal from.\"),\n depth: z\n .number()\n .int()\n .min(1)\n .max(10)\n .optional()\n .describe(\"How many link hops to traverse. Default: 3.\"),\n direction: z\n .enum([\"forward\", \"backward\", \"both\"])\n .optional()\n .describe(\"Traversal direction: 'forward' (outlinks), 'backward' (backlinks), or 'both'. Default: both.\"),\n mode: z\n .enum([\"sequential\", \"associative\", \"all\"])\n .optional()\n .describe(\"Link type filter: 'sequential' (same-folder), 'associative' (cross-folder), or 'all'. Default: all.\"),\n },\n async (args) => {\n const result = await toolZettelExplore(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_health\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_health\",\n [\n \"Audit the structural health of the Obsidian vault.\",\n \"Reports dead links, orphan notes, disconnected clusters, low-connectivity files, and an overall health score.\",\n ].join(\"\\n\"),\n {\n scope: z\n .enum([\"full\", \"recent\", \"project\"])\n .optional()\n .describe(\"Audit scope: 'full' (entire vault), 'recent' (recently modified), or 'project' (specific path). Default: full.\"),\n project_path: z\n .string()\n .optional()\n .describe(\"Absolute path to the project/folder to audit when scope='project'.\"),\n recent_days: z\n .number()\n .int()\n .optional()\n .describe(\"Number of days to look back when scope='recent'. Default: 30.\"),\n include: z\n .array(z.enum([\"dead_links\", \"orphans\", \"disconnected\", \"low_connectivity\"]))\n .optional()\n .describe(\"Specific checks to include. Omit to run all checks.\"),\n },\n async (args) => {\n const result = await toolZettelHealth(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_surprise\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_surprise\",\n [\n \"Find surprising connections — notes that are semantically similar to a reference note but far away in the link graph.\",\n \"High surprise = unexpected relevance.\",\n ].join(\"\\n\"),\n {\n reference_path: z\n .string()\n .describe(\"Path to the reference note to find surprising connections for.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to search within.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of surprising notes to return. Default: 10.\"),\n min_similarity: z\n .number()\n .optional()\n .describe(\"Minimum semantic similarity [0,1] for a note to be considered. Default: 0.5.\"),\n min_graph_distance: z\n .number()\n .int()\n .optional()\n .describe(\"Minimum link hops away from the reference note. Default: 3.\"),\n },\n async (args) => {\n const result = await toolZettelSurprise(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_suggest\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_suggest\",\n [\n \"Suggest new connections for a note using semantic similarity, shared tags, and graph neighborhood (friends-of-friends).\",\n ].join(\"\\n\"),\n {\n note_path: z\n .string()\n .describe(\"Path to the note to generate link suggestions for.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to search within.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of suggestions to return. Default: 10.\"),\n exclude_linked: z\n .boolean()\n .optional()\n .describe(\"Exclude notes already linked from this note. Default: true.\"),\n },\n async (args) => {\n const result = await toolZettelSuggest(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_converse\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_converse\",\n [\n \"Use the vault as a Zettelkasten communication partner.\",\n \"Ask a question, get relevant notes with cross-domain connections and a synthesis prompt for generating new insights.\",\n ].join(\"\\n\"),\n {\n question: z\n .string()\n .describe(\"The question or topic to explore in the vault.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to query.\"),\n depth: z\n .number()\n .int()\n .optional()\n .describe(\"How many link hops to follow from seed notes. Default: 2.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of relevant notes to retrieve. Default: 10.\"),\n },\n async (args) => {\n const result = await toolZettelConverse(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_themes\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_themes\",\n [\n \"Detect emerging themes by clustering recent notes with similar embeddings.\",\n \"Reveals forming idea clusters and suggests index notes for unlinked clusters.\",\n ].join(\"\\n\"),\n {\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to analyse.\"),\n lookback_days: z\n .number()\n .int()\n .optional()\n .describe(\"Number of days of recent notes to cluster. Default: 30.\"),\n min_cluster_size: z\n .number()\n .int()\n .optional()\n .describe(\"Minimum notes required to form a theme cluster. Default: 3.\"),\n max_themes: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of theme clusters to return. Default: 10.\"),\n similarity_threshold: z\n .number()\n .optional()\n .describe(\"Minimum cosine similarity to group notes into a cluster [0,1]. Default: 0.7.\"),\n },\n async (args) => {\n const result = await toolZettelThemes(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Connect transport and start serving\n // -------------------------------------------------------------------------\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // Keep the process alive — the server runs until stdin closes\n}\n","#!/usr/bin/env node\n/**\n * PAI Knowledge OS — MCP server entry point\n *\n * When invoked as `node dist/mcp/index.mjs` (or via the `pai-mcp` bin),\n * starts the PAI MCP server on stdio transport so Claude Code can call\n * memory_search, memory_get, project_info, project_list, session_list,\n * and registry_search tools directly during conversations.\n */\n\nimport { startMcpServer } from \"./server.js\";\n\nstartMcpServer().catch((err) => {\n // Write errors to stderr only — stdout is reserved for JSON-RPC messages\n process.stderr.write(`PAI MCP server fatal error: ${String(err)}\\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","z\n .record","z.unknown"],"sources":["../../src/mcp/server.ts","../../src/mcp/index.ts"],"sourcesContent":["/**\n * PAI Knowledge OS — MCP Server (Phase 3)\n *\n * Exposes PAI registry and memory as MCP tools callable by Claude Code.\n *\n * Tools:\n * memory_search — BM25 search across indexed memory/notes\n * memory_get — Read a specific file or lines from a project\n * project_info — Get details for a project (by slug or current dir)\n * project_list — List projects with optional filters\n * session_list — List sessions for a project\n * registry_search — Full-text search over project slugs/names/paths\n * project_detect — Detect which project a path belongs to\n * project_health — Audit all projects for moved/deleted directories\n * session_route — Auto-route session to project (path/marker/topic)\n *\n * NOTE: All tool logic lives in tools.ts (shared with the daemon).\n * This file wires MCP schema definitions to those pure functions.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { openRegistry } from \"../registry/db.js\";\nimport { openFederation } from \"../memory/db.js\";\nimport {\n toolMemorySearch,\n toolMemoryGet,\n toolProjectInfo,\n toolProjectList,\n toolSessionList,\n toolRegistrySearch,\n toolProjectDetect,\n toolProjectHealth,\n toolNotificationConfig,\n toolTopicDetect,\n toolSessionRoute,\n toolZettelExplore,\n toolZettelHealth,\n toolZettelSurprise,\n toolZettelSuggest,\n toolZettelConverse,\n toolZettelThemes,\n} from \"./tools.js\";\n\n// ---------------------------------------------------------------------------\n// Database singletons (opened lazily, once per MCP server process)\n// ---------------------------------------------------------------------------\n\nlet _registryDb: ReturnType<typeof openRegistry> | null = null;\nlet _federationDb: ReturnType<typeof openFederation> | null = null;\n\nfunction getRegistryDb() {\n if (!_registryDb) _registryDb = openRegistry();\n return _registryDb;\n}\n\nfunction getFederationDb() {\n if (!_federationDb) _federationDb = openFederation();\n return _federationDb;\n}\n\n// ---------------------------------------------------------------------------\n// MCP server startup\n// ---------------------------------------------------------------------------\n\nexport async function startMcpServer(): 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. Set to false to skip reranking for faster but less accurate results.\"\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 (3 months). Applied after reranking.\"\n ),\n },\n async (args) => {\n const result = await toolMemorySearch(\n getRegistryDb(),\n getFederationDb(),\n { ...args, recencyBoost: args.recency_boost }\n );\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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) => {\n const result = toolMemoryGet(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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) => {\n const result = toolProjectInfo(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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) => {\n const result = toolProjectList(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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) => {\n const result = toolSessionList(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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) => {\n const result = toolRegistrySearch(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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 \"\",\n \"match_type 'exact' means the path IS the project root.\",\n \"match_type 'parent' means the path is a subdirectory of the project root.\",\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) => {\n const result = toolProjectDetect(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\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 \"Use this to diagnose orphaned sessions or missing project paths.\",\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) => {\n const result = await toolProjectHealth(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: notification_config\n // -------------------------------------------------------------------------\n\n server.tool(\n \"notification_config\",\n [\n \"Query or update the PAI unified notification configuration.\",\n \"\",\n \"Actions:\",\n \" get — Return the current notification mode, active channels, and routing table.\",\n \" set — Change the notification mode or update channel/routing config.\",\n \" send — Send a notification through the configured channels.\",\n \"\",\n \"Notification modes:\",\n \" auto — Use the per-event routing table (default)\",\n \" voice — All events sent as WhatsApp voice (TTS)\",\n \" whatsapp — All events sent as WhatsApp text\",\n \" ntfy — All events sent to ntfy.sh\",\n \" macos — All events sent as macOS notifications\",\n \" cli — All events written to CLI output only\",\n \" off — Suppress all notifications\",\n \"\",\n \"Event types for send: error | progress | completion | info | debug\",\n \"\",\n \"Examples:\",\n ' { \"action\": \"get\" }',\n ' { \"action\": \"set\", \"mode\": \"voice\" }',\n ' { \"action\": \"send\", \"event\": \"completion\", \"message\": \"Done!\" }',\n ].join(\"\\n\"),\n {\n action: z\n .enum([\"get\", \"set\", \"send\"])\n .describe(\"Action: 'get' (read config), 'set' (update config), 'send' (send notification).\"),\n mode: z\n .enum([\"auto\", \"voice\", \"whatsapp\", \"ntfy\", \"macos\", \"cli\", \"off\"])\n .optional()\n .describe(\"For action=set: new notification mode.\"),\n channels: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"For action=set: partial channel config overrides as a JSON object. \" +\n 'E.g. { \"whatsapp\": { \"enabled\": true }, \"macos\": { \"enabled\": false } }'\n ),\n routing: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"For action=set: partial routing overrides as a JSON object. \" +\n 'E.g. { \"error\": [\"whatsapp\", \"macos\"], \"progress\": [\"cli\"] }'\n ),\n event: z\n .enum([\"error\", \"progress\", \"completion\", \"info\", \"debug\"])\n .optional()\n .describe(\"For action=send: event type. Default: 'info'.\"),\n message: z\n .string()\n .optional()\n .describe(\"For action=send: the notification message body.\"),\n title: z\n .string()\n .optional()\n .describe(\"For action=send: optional notification title (used by macOS and ntfy).\"),\n },\n async (args) => {\n const result = await toolNotificationConfig(args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: topic_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"topic_detect\",\n [\n \"Detect whether recent conversation context has shifted to a different project.\",\n \"\",\n \"Call this when the conversation may have drifted away from the initially-routed project.\",\n \"Provide a short summary of the recent context (last few messages or tool call results).\",\n \"\",\n \"Returns:\",\n \" shifted — true if a topic shift was detected\",\n \" current_project — the project the session is currently routed to\",\n \" suggested_project — the project that best matches the context\",\n \" confidence — [0,1] fraction of memory mass held by suggested_project\",\n \" chunks_scored — number of memory chunks that contributed to scoring\",\n \" top_matches — top-3 projects with their confidence percentages\",\n \"\",\n \"A shift is reported when confidence >= threshold (default 0.6) and the\",\n \"best-matching project differs from current_project.\",\n \"\",\n \"Use cases:\",\n \" - Call at session start to confirm routing is correct\",\n \" - Call periodically when working across multiple concerns\",\n \" - Integrate with pre-tool hooks for automatic drift detection\",\n ].join(\"\\n\"),\n {\n context: z\n .string()\n .describe(\n \"Recent conversation context: a few sentences summarising what the session has been discussing. \" +\n \"Can include file paths, feature names, commands run, or any relevant text.\"\n ),\n current_project: z\n .string()\n .optional()\n .describe(\n \"The project slug this session is currently routed to. \" +\n \"If omitted, the tool still returns the best-matching project but shifted will always be false.\"\n ),\n threshold: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .describe(\n \"Minimum confidence [0,1] to declare a shift. Default: 0.6. \" +\n \"Increase to reduce false positives. Decrease to catch subtle drifts.\"\n ),\n },\n async (args) => {\n const result = await toolTopicDetect(args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_route\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_route\",\n [\n \"Automatically detect which project this session belongs to.\",\n \"\",\n \"Call this at session start (e.g., from CLAUDE.md or a session-start hook)\",\n \"to route the session to the correct project automatically.\",\n \"\",\n \"Detection strategy (in priority order):\",\n \" 1. path — exact or parent-directory match in the project registry\",\n \" 2. marker — walk up from cwd looking for Notes/PAI.md marker files\",\n \" 3. topic — BM25 keyword search against memory (only if context provided)\",\n \"\",\n \"Returns:\",\n \" slug — the matched project slug\",\n \" display_name — human-readable project name\",\n \" root_path — absolute path to the project root\",\n \" method — how it was detected: 'path', 'marker', or 'topic'\",\n \" confidence — 1.0 for path/marker matches, BM25 fraction for topic\",\n \"\",\n \"If no match is found, returns a message explaining what was tried.\",\n \"Run 'pai project add .' to register the current directory.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Working directory to detect from. Defaults to process.cwd(). \" +\n \"Pass the session's actual working directory for accurate detection.\"\n ),\n context: z\n .string()\n .optional()\n .describe(\n \"Optional conversation context for topic-based fallback routing. \" +\n \"A few sentences summarising what the session will work on. \" +\n \"Only used if path and marker detection both fail.\"\n ),\n },\n async (args) => {\n const result = await toolSessionRoute(getRegistryDb(), getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_explore\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_explore\",\n [\n \"Explore the vault's knowledge graph using Luhmann's Folgezettel traversal.\",\n \"Follow trains of thought forward, backward, or both from a starting note.\",\n \"Classifies links as sequential (same-folder) or associative (cross-folder).\",\n ].join(\"\\n\"),\n {\n start_note: z\n .string()\n .describe(\"Path or title of the note to start traversal from.\"),\n depth: z\n .number()\n .int()\n .min(1)\n .max(10)\n .optional()\n .describe(\"How many link hops to traverse. Default: 3.\"),\n direction: z\n .enum([\"forward\", \"backward\", \"both\"])\n .optional()\n .describe(\"Traversal direction: 'forward' (outlinks), 'backward' (backlinks), or 'both'. Default: both.\"),\n mode: z\n .enum([\"sequential\", \"associative\", \"all\"])\n .optional()\n .describe(\"Link type filter: 'sequential' (same-folder), 'associative' (cross-folder), or 'all'. Default: all.\"),\n },\n async (args) => {\n const result = await toolZettelExplore(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_health\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_health\",\n [\n \"Audit the structural health of the Obsidian vault.\",\n \"Reports dead links, orphan notes, disconnected clusters, low-connectivity files, and an overall health score.\",\n ].join(\"\\n\"),\n {\n scope: z\n .enum([\"full\", \"recent\", \"project\"])\n .optional()\n .describe(\"Audit scope: 'full' (entire vault), 'recent' (recently modified), or 'project' (specific path). Default: full.\"),\n project_path: z\n .string()\n .optional()\n .describe(\"Absolute path to the project/folder to audit when scope='project'.\"),\n recent_days: z\n .number()\n .int()\n .optional()\n .describe(\"Number of days to look back when scope='recent'. Default: 30.\"),\n include: z\n .array(z.enum([\"dead_links\", \"orphans\", \"disconnected\", \"low_connectivity\"]))\n .optional()\n .describe(\"Specific checks to include. Omit to run all checks.\"),\n },\n async (args) => {\n const result = await toolZettelHealth(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_surprise\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_surprise\",\n [\n \"Find surprising connections — notes that are semantically similar to a reference note but far away in the link graph.\",\n \"High surprise = unexpected relevance.\",\n ].join(\"\\n\"),\n {\n reference_path: z\n .string()\n .describe(\"Path to the reference note to find surprising connections for.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to search within.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of surprising notes to return. Default: 10.\"),\n min_similarity: z\n .number()\n .optional()\n .describe(\"Minimum semantic similarity [0,1] for a note to be considered. Default: 0.5.\"),\n min_graph_distance: z\n .number()\n .int()\n .optional()\n .describe(\"Minimum link hops away from the reference note. Default: 3.\"),\n },\n async (args) => {\n const result = await toolZettelSurprise(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_suggest\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_suggest\",\n [\n \"Suggest new connections for a note using semantic similarity, shared tags, and graph neighborhood (friends-of-friends).\",\n ].join(\"\\n\"),\n {\n note_path: z\n .string()\n .describe(\"Path to the note to generate link suggestions for.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to search within.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of suggestions to return. Default: 10.\"),\n exclude_linked: z\n .boolean()\n .optional()\n .describe(\"Exclude notes already linked from this note. Default: true.\"),\n },\n async (args) => {\n const result = await toolZettelSuggest(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_converse\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_converse\",\n [\n \"Use the vault as a Zettelkasten communication partner.\",\n \"Ask a question, get relevant notes with cross-domain connections and a synthesis prompt for generating new insights.\",\n ].join(\"\\n\"),\n {\n question: z\n .string()\n .describe(\"The question or topic to explore in the vault.\"),\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to query.\"),\n depth: z\n .number()\n .int()\n .optional()\n .describe(\"How many link hops to follow from seed notes. Default: 2.\"),\n limit: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of relevant notes to retrieve. Default: 10.\"),\n },\n async (args) => {\n const result = await toolZettelConverse(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: zettel_themes\n // -------------------------------------------------------------------------\n\n server.tool(\n \"zettel_themes\",\n [\n \"Detect emerging themes by clustering recent notes with similar embeddings.\",\n \"Reveals forming idea clusters and suggests index notes for unlinked clusters.\",\n ].join(\"\\n\"),\n {\n vault_project_id: z\n .number()\n .int()\n .describe(\"Project ID of the vault to analyse.\"),\n lookback_days: z\n .number()\n .int()\n .optional()\n .describe(\"Number of days of recent notes to cluster. Default: 30.\"),\n min_cluster_size: z\n .number()\n .int()\n .optional()\n .describe(\"Minimum notes required to form a theme cluster. Default: 3.\"),\n max_themes: z\n .number()\n .int()\n .optional()\n .describe(\"Maximum number of theme clusters to return. Default: 10.\"),\n similarity_threshold: z\n .number()\n .optional()\n .describe(\"Minimum cosine similarity to group notes into a cluster [0,1]. Default: 0.7.\"),\n },\n async (args) => {\n const result = await toolZettelThemes(getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Connect transport and start serving\n // -------------------------------------------------------------------------\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // Keep the process alive — the server runs until stdin closes\n}\n","#!/usr/bin/env node\n/**\n * PAI Knowledge OS — MCP server entry point\n *\n * When invoked as `node dist/mcp/index.mjs` (or via the `pai-mcp` bin),\n * starts the PAI MCP server on stdio transport so Claude Code can call\n * memory_search, memory_get, project_info, project_list, session_list,\n * and registry_search tools directly during conversations.\n */\n\nimport { startMcpServer } from \"./server.js\";\n\nstartMcpServer().catch((err) => {\n // Write errors to stderr only — stdout is reserved for JSON-RPC messages\n process.stderr.write(`PAI MCP server fatal error: ${String(err)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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"}
@@ -332,4 +332,4 @@ function buildPgTsQuery(query) {
332
332
 
333
333
  //#endregion
334
334
  export { PostgresBackend };
335
- //# sourceMappingURL=postgres-CRBe30Ag.mjs.map
335
+ //# sourceMappingURL=postgres-CIxeqf_n.mjs.map