@tekmidian/pai 0.5.6 → 0.6.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 (137) hide show
  1. package/ARCHITECTURE.md +72 -1
  2. package/README.md +107 -3
  3. package/dist/{auto-route-BG6I_4B1.mjs → auto-route-C-DrW6BL.mjs} +3 -3
  4. package/dist/{auto-route-BG6I_4B1.mjs.map → auto-route-C-DrW6BL.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +1897 -1569
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/clusters-JIDQW65f.mjs +201 -0
  8. package/dist/clusters-JIDQW65f.mjs.map +1 -0
  9. package/dist/{config-Cf92lGX_.mjs → config-BuhHWyOK.mjs} +21 -6
  10. package/dist/config-BuhHWyOK.mjs.map +1 -0
  11. package/dist/daemon/index.mjs +12 -9
  12. package/dist/daemon/index.mjs.map +1 -1
  13. package/dist/{daemon-D9evGlgR.mjs → daemon-D3hYb5_C.mjs} +670 -219
  14. package/dist/daemon-D3hYb5_C.mjs.map +1 -0
  15. package/dist/daemon-mcp/index.mjs +4597 -4
  16. package/dist/daemon-mcp/index.mjs.map +1 -1
  17. package/dist/{db-4lSqLFb8.mjs → db-BtuN768f.mjs} +9 -2
  18. package/dist/db-BtuN768f.mjs.map +1 -0
  19. package/dist/db-DdUperSl.mjs +110 -0
  20. package/dist/db-DdUperSl.mjs.map +1 -0
  21. package/dist/{detect-BU3Nx_2L.mjs → detect-CdaA48EI.mjs} +1 -1
  22. package/dist/{detect-BU3Nx_2L.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  23. package/dist/{detector-Bp-2SM3x.mjs → detector-jGBuYQJM.mjs} +2 -2
  24. package/dist/{detector-Bp-2SM3x.mjs.map → detector-jGBuYQJM.mjs.map} +1 -1
  25. package/dist/{factory-Bzcy70G9.mjs → factory-Ygqe_bVZ.mjs} +7 -5
  26. package/dist/{factory-Bzcy70G9.mjs.map → factory-Ygqe_bVZ.mjs.map} +1 -1
  27. package/dist/helpers-BEST-4Gx.mjs +420 -0
  28. package/dist/helpers-BEST-4Gx.mjs.map +1 -0
  29. package/dist/hooks/capture-all-events.mjs +19 -4
  30. package/dist/hooks/capture-all-events.mjs.map +4 -4
  31. package/dist/hooks/capture-session-summary.mjs +38 -0
  32. package/dist/hooks/capture-session-summary.mjs.map +3 -3
  33. package/dist/hooks/cleanup-session-files.mjs +6 -12
  34. package/dist/hooks/cleanup-session-files.mjs.map +4 -4
  35. package/dist/hooks/context-compression-hook.mjs +105 -111
  36. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  37. package/dist/hooks/initialize-session.mjs +26 -17
  38. package/dist/hooks/initialize-session.mjs.map +4 -4
  39. package/dist/hooks/inject-observations.mjs +220 -0
  40. package/dist/hooks/inject-observations.mjs.map +7 -0
  41. package/dist/hooks/load-core-context.mjs +18 -2
  42. package/dist/hooks/load-core-context.mjs.map +4 -4
  43. package/dist/hooks/load-project-context.mjs +102 -97
  44. package/dist/hooks/load-project-context.mjs.map +4 -4
  45. package/dist/hooks/observe.mjs +354 -0
  46. package/dist/hooks/observe.mjs.map +7 -0
  47. package/dist/hooks/stop-hook.mjs +174 -90
  48. package/dist/hooks/stop-hook.mjs.map +4 -4
  49. package/dist/hooks/sync-todo-to-md.mjs +31 -33
  50. package/dist/hooks/sync-todo-to-md.mjs.map +4 -4
  51. package/dist/index.d.mts +32 -9
  52. package/dist/index.d.mts.map +1 -1
  53. package/dist/index.mjs +6 -9
  54. package/dist/indexer-D53l5d1U.mjs +1 -0
  55. package/dist/{indexer-backend-CIMXedqk.mjs → indexer-backend-jcJFsmB4.mjs} +37 -127
  56. package/dist/indexer-backend-jcJFsmB4.mjs.map +1 -0
  57. package/dist/{ipc-client-Bjg_a1dc.mjs → ipc-client-CoyUHPod.mjs} +2 -7
  58. package/dist/{ipc-client-Bjg_a1dc.mjs.map → ipc-client-CoyUHPod.mjs.map} +1 -1
  59. package/dist/latent-ideas-bTJo6Omd.mjs +191 -0
  60. package/dist/latent-ideas-bTJo6Omd.mjs.map +1 -0
  61. package/dist/neighborhood-BYYbEkUJ.mjs +135 -0
  62. package/dist/neighborhood-BYYbEkUJ.mjs.map +1 -0
  63. package/dist/note-context-BK24bX8Y.mjs +126 -0
  64. package/dist/note-context-BK24bX8Y.mjs.map +1 -0
  65. package/dist/postgres-CKf-EDtS.mjs +846 -0
  66. package/dist/postgres-CKf-EDtS.mjs.map +1 -0
  67. package/dist/{reranker-D7bRAHi6.mjs → reranker-CMNZcfVx.mjs} +1 -1
  68. package/dist/{reranker-D7bRAHi6.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
  69. package/dist/{search-_oHfguA5.mjs → search-DC1qhkKn.mjs} +2 -58
  70. package/dist/search-DC1qhkKn.mjs.map +1 -0
  71. package/dist/{sqlite-WWBq7_2C.mjs → sqlite-l-s9xPjY.mjs} +160 -3
  72. package/dist/sqlite-l-s9xPjY.mjs.map +1 -0
  73. package/dist/state-C6_vqz7w.mjs +102 -0
  74. package/dist/state-C6_vqz7w.mjs.map +1 -0
  75. package/dist/stop-words-BaMEGVeY.mjs +326 -0
  76. package/dist/stop-words-BaMEGVeY.mjs.map +1 -0
  77. package/dist/{indexer-CMPOiY1r.mjs → sync-BOsnEj2-.mjs} +14 -216
  78. package/dist/sync-BOsnEj2-.mjs.map +1 -0
  79. package/dist/themes-BvYF0W8T.mjs +148 -0
  80. package/dist/themes-BvYF0W8T.mjs.map +1 -0
  81. package/dist/{tools-DV_lsiCc.mjs → tools-DcaJlYDN.mjs} +162 -273
  82. package/dist/tools-DcaJlYDN.mjs.map +1 -0
  83. package/dist/trace-CRx9lPuc.mjs +137 -0
  84. package/dist/trace-CRx9lPuc.mjs.map +1 -0
  85. package/dist/{vault-indexer-DXWs9pDn.mjs → vault-indexer-Bi2cRmn7.mjs} +174 -138
  86. package/dist/vault-indexer-Bi2cRmn7.mjs.map +1 -0
  87. package/dist/zettelkasten-cdajbnPr.mjs +708 -0
  88. package/dist/zettelkasten-cdajbnPr.mjs.map +1 -0
  89. package/package.json +1 -2
  90. package/src/hooks/ts/capture-all-events.ts +6 -0
  91. package/src/hooks/ts/lib/project-utils/index.ts +50 -0
  92. package/src/hooks/ts/lib/project-utils/notify.ts +75 -0
  93. package/src/hooks/ts/lib/project-utils/paths.ts +218 -0
  94. package/src/hooks/ts/lib/project-utils/session-notes.ts +363 -0
  95. package/src/hooks/ts/lib/project-utils/todo.ts +178 -0
  96. package/src/hooks/ts/lib/project-utils/tokens.ts +39 -0
  97. package/src/hooks/ts/lib/project-utils.ts +40 -999
  98. package/src/hooks/ts/post-tool-use/observe.ts +327 -0
  99. package/src/hooks/ts/pre-compact/context-compression-hook.ts +6 -0
  100. package/src/hooks/ts/session-end/capture-session-summary.ts +41 -0
  101. package/src/hooks/ts/session-start/initialize-session.ts +7 -1
  102. package/src/hooks/ts/session-start/inject-observations.ts +254 -0
  103. package/src/hooks/ts/session-start/load-core-context.ts +7 -0
  104. package/src/hooks/ts/session-start/load-project-context.ts +8 -1
  105. package/src/hooks/ts/stop/stop-hook.ts +28 -0
  106. package/templates/claude-md.template.md +7 -74
  107. package/templates/skills/user/.gitkeep +0 -0
  108. package/dist/chunker-CbnBe0s0.mjs +0 -191
  109. package/dist/chunker-CbnBe0s0.mjs.map +0 -1
  110. package/dist/config-Cf92lGX_.mjs.map +0 -1
  111. package/dist/daemon-D9evGlgR.mjs.map +0 -1
  112. package/dist/db-4lSqLFb8.mjs.map +0 -1
  113. package/dist/db-Dp8VXIMR.mjs +0 -212
  114. package/dist/db-Dp8VXIMR.mjs.map +0 -1
  115. package/dist/indexer-CMPOiY1r.mjs.map +0 -1
  116. package/dist/indexer-backend-CIMXedqk.mjs.map +0 -1
  117. package/dist/mcp/index.d.mts +0 -1
  118. package/dist/mcp/index.mjs +0 -500
  119. package/dist/mcp/index.mjs.map +0 -1
  120. package/dist/postgres-FXrHDPcE.mjs +0 -358
  121. package/dist/postgres-FXrHDPcE.mjs.map +0 -1
  122. package/dist/schemas-BFIgGntb.mjs +0 -3405
  123. package/dist/schemas-BFIgGntb.mjs.map +0 -1
  124. package/dist/search-_oHfguA5.mjs.map +0 -1
  125. package/dist/sqlite-WWBq7_2C.mjs.map +0 -1
  126. package/dist/tools-DV_lsiCc.mjs.map +0 -1
  127. package/dist/vault-indexer-DXWs9pDn.mjs.map +0 -1
  128. package/dist/zettelkasten-e-a4rW_6.mjs +0 -901
  129. package/dist/zettelkasten-e-a4rW_6.mjs.map +0 -1
  130. package/templates/README.md +0 -181
  131. package/templates/skills/createskill-skill.template.md +0 -78
  132. package/templates/skills/history-system.template.md +0 -371
  133. package/templates/skills/hook-system.template.md +0 -913
  134. package/templates/skills/sessions-skill.template.md +0 -102
  135. package/templates/skills/skill-system.template.md +0 -214
  136. package/templates/skills/terminal-tabs.template.md +0 -120
  137. package/templates/templates.md +0 -20
@@ -1,500 +0,0 @@
1
- #!/usr/bin/env node
2
- import { n as openRegistry } from "../db-4lSqLFb8.mjs";
3
- import { n as openFederation } from "../db-Dp8VXIMR.mjs";
4
- import "../embeddings-DGRAPAYb.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 { o as loadConfig } from "../config-Cf92lGX_.mjs";
8
- 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-DV_lsiCc.mjs";
9
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
-
12
- //#region src/mcp/server.ts
13
- /**
14
- * PAI Knowledge OS — MCP Server (Phase 3)
15
- *
16
- * Exposes PAI registry and memory as MCP tools callable by Claude Code.
17
- *
18
- * Tools:
19
- * memory_search — BM25 search across indexed memory/notes
20
- * memory_get — Read a specific file or lines from a project
21
- * project_info — Get details for a project (by slug or current dir)
22
- * project_list — List projects with optional filters
23
- * session_list — List sessions for a project
24
- * registry_search — Full-text search over project slugs/names/paths
25
- * project_detect — Detect which project a path belongs to
26
- * project_health — Audit all projects for moved/deleted directories
27
- * session_route — Auto-route session to project (path/marker/topic)
28
- *
29
- * NOTE: All tool logic lives in tools.ts (shared with the daemon).
30
- * This file wires MCP schema definitions to those pure functions.
31
- */
32
- let _registryDb = null;
33
- let _federationDb = null;
34
- function getRegistryDb() {
35
- if (!_registryDb) _registryDb = openRegistry();
36
- return _registryDb;
37
- }
38
- function getFederationDb() {
39
- if (!_federationDb) _federationDb = openFederation();
40
- return _federationDb;
41
- }
42
- async function startMcpServer() {
43
- const server = new McpServer({
44
- name: "pai",
45
- version: "0.1.0"
46
- });
47
- server.tool("memory_search", [
48
- "Search PAI federated memory using BM25 full-text ranking, semantic similarity, or a hybrid of both.",
49
- "",
50
- "Use this BEFORE answering questions about past work, decisions, dates, people,",
51
- "preferences, project status, todos, technical choices, or anything that might",
52
- "have been recorded in session notes or memory files.",
53
- "",
54
- "Modes:",
55
- " keyword — BM25 full-text search (default, fast, no embeddings required)",
56
- " semantic — Cosine similarity over vector embeddings (requires prior embed run)",
57
- " hybrid — Normalized combination of BM25 + cosine (best quality)",
58
- "",
59
- "Reranking is ON by default — results are re-scored with a cross-encoder model for better relevance.",
60
- "Set rerank=false to skip reranking (faster but less accurate ordering).",
61
- "",
62
- "Recency boost optionally down-weights older results (recency_boost=90 means scores halve every 90 days).",
63
- "",
64
- "Defaults come from ~/.config/pai/config.json (search section). Per-call parameters override config defaults.",
65
- "",
66
- "Returns ranked snippets with project slug, file path, line range, and score.",
67
- "Higher score = more relevant."
68
- ].join("\n"), {
69
- query: string().describe("Free-text search query. Multiple words are ORed together — any matching word returns a result, ranked by relevance."),
70
- project: string().optional().describe("Scope search to a single project by slug. Omit to search all projects."),
71
- all_projects: boolean().optional().describe("Explicitly search all projects (default behaviour when project is omitted)."),
72
- sources: array(_enum(["memory", "notes"])).optional().describe("Restrict to specific source types: 'memory' or 'notes'."),
73
- limit: number().int().min(1).max(100).optional().describe("Maximum results to return. Default: 10."),
74
- mode: _enum([
75
- "keyword",
76
- "semantic",
77
- "hybrid"
78
- ]).optional().describe("Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined)."),
79
- 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."),
80
- recency_boost: number().int().min(0).max(365).optional().describe("Apply recency boost: score halves every N days. 0 = off. Default from config (typically 90). Applied after reranking.")
81
- }, async (args) => {
82
- const config = loadConfig();
83
- const result = await toolMemorySearch(getRegistryDb(), getFederationDb(), {
84
- ...args,
85
- recencyBoost: args.recency_boost
86
- }, config.search);
87
- return {
88
- content: result.content.map((c) => ({
89
- type: c.type,
90
- text: c.text
91
- })),
92
- isError: result.isError
93
- };
94
- });
95
- server.tool("memory_get", [
96
- "Read the content of a specific file from a registered PAI project.",
97
- "",
98
- "Use this to read a full memory file, session note, or document after finding",
99
- "it via memory_search. Optionally restrict to a line range.",
100
- "",
101
- "The path must be a relative path within the project root (no ../ traversal)."
102
- ].join("\n"), {
103
- project: string().describe("Project slug identifying which project's files to read from."),
104
- path: string().describe("Relative path within the project root (e.g. 'Notes/0001 - 2026-01-01 - Example.md')."),
105
- from: number().int().min(1).optional().describe("Starting line number (1-based, inclusive). Default: 1."),
106
- lines: number().int().min(1).optional().describe("Number of lines to return. Default: entire file.")
107
- }, async (args) => {
108
- const result = toolMemoryGet(getRegistryDb(), args);
109
- return {
110
- content: result.content.map((c) => ({
111
- type: c.type,
112
- text: c.text
113
- })),
114
- isError: result.isError
115
- };
116
- });
117
- server.tool("project_info", [
118
- "Get detailed information about a PAI registered project.",
119
- "",
120
- "Use this to look up a project's root path, type, status, tags, session count,",
121
- "and last active date. If no slug is provided, attempts to detect the current",
122
- "project from the caller's working directory."
123
- ].join("\n"), { slug: string().optional().describe("Project slug. Omit to auto-detect from the current working directory.") }, async (args) => {
124
- const result = toolProjectInfo(getRegistryDb(), args);
125
- return {
126
- content: result.content.map((c) => ({
127
- type: c.type,
128
- text: c.text
129
- })),
130
- isError: result.isError
131
- };
132
- });
133
- server.tool("project_list", [
134
- "List registered PAI projects with optional filters.",
135
- "",
136
- "Use this to browse all known projects, find projects by status or tag,",
137
- "or get a quick overview of the PAI registry."
138
- ].join("\n"), {
139
- status: _enum([
140
- "active",
141
- "archived",
142
- "migrating"
143
- ]).optional().describe("Filter by project status. Default: all statuses."),
144
- tag: string().optional().describe("Filter by tag name (exact match)."),
145
- limit: number().int().min(1).max(500).optional().describe("Maximum number of projects to return. Default: 50.")
146
- }, async (args) => {
147
- const result = toolProjectList(getRegistryDb(), args);
148
- return {
149
- content: result.content.map((c) => ({
150
- type: c.type,
151
- text: c.text
152
- })),
153
- isError: result.isError
154
- };
155
- });
156
- server.tool("session_list", [
157
- "List session notes for a PAI project.",
158
- "",
159
- "Use this to find what sessions exist for a project, see their dates and titles,",
160
- "and identify specific session notes to read via memory_get."
161
- ].join("\n"), {
162
- project: string().describe("Project slug to list sessions for."),
163
- limit: number().int().min(1).max(500).optional().describe("Maximum sessions to return. Default: 10 (most recent first)."),
164
- status: _enum([
165
- "open",
166
- "completed",
167
- "compacted"
168
- ]).optional().describe("Filter by session status.")
169
- }, async (args) => {
170
- const result = toolSessionList(getRegistryDb(), args);
171
- return {
172
- content: result.content.map((c) => ({
173
- type: c.type,
174
- text: c.text
175
- })),
176
- isError: result.isError
177
- };
178
- });
179
- server.tool("registry_search", [
180
- "Search PAI project registry by slug, display name, or path.",
181
- "",
182
- "Use this to find the slug for a project when you know its name or path,",
183
- "or to check if a project is registered. Returns matching project entries."
184
- ].join("\n"), { query: string().describe("Search term matched against project slugs, display names, and root paths (case-insensitive substring match).") }, async (args) => {
185
- const result = toolRegistrySearch(getRegistryDb(), args);
186
- return {
187
- content: result.content.map((c) => ({
188
- type: c.type,
189
- text: c.text
190
- })),
191
- isError: result.isError
192
- };
193
- });
194
- server.tool("project_detect", [
195
- "Detect which registered PAI project a filesystem path belongs to.",
196
- "",
197
- "Use this at session start to auto-identify the current project from the",
198
- "working directory, or to map any path back to its registered project.",
199
- "",
200
- "Returns: slug, display_name, root_path, type, status, match_type (exact|parent),",
201
- "relative_path (if the given path is inside a project), and session stats.",
202
- "",
203
- "match_type 'exact' means the path IS the project root.",
204
- "match_type 'parent' means the path is a subdirectory of the project root."
205
- ].join("\n"), { cwd: string().optional().describe("Absolute path to detect project for. Defaults to the MCP server's process.cwd().") }, async (args) => {
206
- const result = toolProjectDetect(getRegistryDb(), args);
207
- return {
208
- content: result.content.map((c) => ({
209
- type: c.type,
210
- text: c.text
211
- })),
212
- isError: result.isError
213
- };
214
- });
215
- server.tool("project_health", [
216
- "Audit all registered PAI projects to find moved or deleted directories.",
217
- "",
218
- "Returns a JSON report categorising every project as:",
219
- " active — root_path exists on disk",
220
- " stale — root_path missing, but a directory with the same name was found nearby",
221
- " dead — root_path missing, no candidate found",
222
- "",
223
- "Use this to diagnose orphaned sessions or missing project paths."
224
- ].join("\n"), { category: _enum([
225
- "active",
226
- "stale",
227
- "dead",
228
- "all"
229
- ]).optional().describe("Filter results to a specific health category. Default: all.") }, async (args) => {
230
- const result = await toolProjectHealth(getRegistryDb(), args);
231
- return {
232
- content: result.content.map((c) => ({
233
- type: c.type,
234
- text: c.text
235
- })),
236
- isError: result.isError
237
- };
238
- });
239
- server.tool("notification_config", [
240
- "Query or update the PAI unified notification configuration.",
241
- "",
242
- "Actions:",
243
- " get — Return the current notification mode, active channels, and routing table.",
244
- " set — Change the notification mode or update channel/routing config.",
245
- " send — Send a notification through the configured channels.",
246
- "",
247
- "Notification modes:",
248
- " auto — Use the per-event routing table (default)",
249
- " voice — All events sent as WhatsApp voice (TTS)",
250
- " whatsapp — All events sent as WhatsApp text",
251
- " ntfy — All events sent to ntfy.sh",
252
- " macos — All events sent as macOS notifications",
253
- " cli — All events written to CLI output only",
254
- " off — Suppress all notifications",
255
- "",
256
- "Event types for send: error | progress | completion | info | debug",
257
- "",
258
- "Examples:",
259
- " { \"action\": \"get\" }",
260
- " { \"action\": \"set\", \"mode\": \"voice\" }",
261
- " { \"action\": \"send\", \"event\": \"completion\", \"message\": \"Done!\" }"
262
- ].join("\n"), {
263
- action: _enum([
264
- "get",
265
- "set",
266
- "send"
267
- ]).describe("Action: 'get' (read config), 'set' (update config), 'send' (send notification)."),
268
- mode: _enum([
269
- "auto",
270
- "voice",
271
- "whatsapp",
272
- "ntfy",
273
- "macos",
274
- "cli",
275
- "off"
276
- ]).optional().describe("For action=set: new notification mode."),
277
- channels: record(string(), unknown()).optional().describe("For action=set: partial channel config overrides as a JSON object. E.g. { \"whatsapp\": { \"enabled\": true }, \"macos\": { \"enabled\": false } }"),
278
- routing: record(string(), unknown()).optional().describe("For action=set: partial routing overrides as a JSON object. E.g. { \"error\": [\"whatsapp\", \"macos\"], \"progress\": [\"cli\"] }"),
279
- event: _enum([
280
- "error",
281
- "progress",
282
- "completion",
283
- "info",
284
- "debug"
285
- ]).optional().describe("For action=send: event type. Default: 'info'."),
286
- message: string().optional().describe("For action=send: the notification message body."),
287
- title: string().optional().describe("For action=send: optional notification title (used by macOS and ntfy).")
288
- }, async (args) => {
289
- const result = await toolNotificationConfig(args);
290
- return {
291
- content: result.content.map((c) => ({
292
- type: c.type,
293
- text: c.text
294
- })),
295
- isError: result.isError
296
- };
297
- });
298
- server.tool("topic_detect", [
299
- "Detect whether recent conversation context has shifted to a different project.",
300
- "",
301
- "Call this when the conversation may have drifted away from the initially-routed project.",
302
- "Provide a short summary of the recent context (last few messages or tool call results).",
303
- "",
304
- "Returns:",
305
- " shifted — true if a topic shift was detected",
306
- " current_project — the project the session is currently routed to",
307
- " suggested_project — the project that best matches the context",
308
- " confidence — [0,1] fraction of memory mass held by suggested_project",
309
- " chunks_scored — number of memory chunks that contributed to scoring",
310
- " top_matches — top-3 projects with their confidence percentages",
311
- "",
312
- "A shift is reported when confidence >= threshold (default 0.6) and the",
313
- "best-matching project differs from current_project.",
314
- "",
315
- "Use cases:",
316
- " - Call at session start to confirm routing is correct",
317
- " - Call periodically when working across multiple concerns",
318
- " - Integrate with pre-tool hooks for automatic drift detection"
319
- ].join("\n"), {
320
- context: string().describe("Recent conversation context: a few sentences summarising what the session has been discussing. Can include file paths, feature names, commands run, or any relevant text."),
321
- current_project: string().optional().describe("The project slug this session is currently routed to. If omitted, the tool still returns the best-matching project but shifted will always be false."),
322
- threshold: number().min(0).max(1).optional().describe("Minimum confidence [0,1] to declare a shift. Default: 0.6. Increase to reduce false positives. Decrease to catch subtle drifts.")
323
- }, async (args) => {
324
- const result = await toolTopicDetect(args);
325
- return {
326
- content: result.content.map((c) => ({
327
- type: c.type,
328
- text: c.text
329
- })),
330
- isError: result.isError
331
- };
332
- });
333
- server.tool("session_route", [
334
- "Automatically detect which project this session belongs to.",
335
- "",
336
- "Call this at session start (e.g., from CLAUDE.md or a session-start hook)",
337
- "to route the session to the correct project automatically.",
338
- "",
339
- "Detection strategy (in priority order):",
340
- " 1. path — exact or parent-directory match in the project registry",
341
- " 2. marker — walk up from cwd looking for Notes/PAI.md marker files",
342
- " 3. topic — BM25 keyword search against memory (only if context provided)",
343
- "",
344
- "Returns:",
345
- " slug — the matched project slug",
346
- " display_name — human-readable project name",
347
- " root_path — absolute path to the project root",
348
- " method — how it was detected: 'path', 'marker', or 'topic'",
349
- " confidence — 1.0 for path/marker matches, BM25 fraction for topic",
350
- "",
351
- "If no match is found, returns a message explaining what was tried.",
352
- "Run 'pai project add .' to register the current directory."
353
- ].join("\n"), {
354
- cwd: string().optional().describe("Working directory to detect from. Defaults to process.cwd(). Pass the session's actual working directory for accurate detection."),
355
- context: string().optional().describe("Optional conversation context for topic-based fallback routing. A few sentences summarising what the session will work on. Only used if path and marker detection both fail.")
356
- }, async (args) => {
357
- const result = await toolSessionRoute(getRegistryDb(), getFederationDb(), args);
358
- return {
359
- content: result.content.map((c) => ({
360
- type: c.type,
361
- text: c.text
362
- })),
363
- isError: result.isError
364
- };
365
- });
366
- server.tool("zettel_explore", [
367
- "Explore the vault's knowledge graph using Luhmann's Folgezettel traversal.",
368
- "Follow trains of thought forward, backward, or both from a starting note.",
369
- "Classifies links as sequential (same-folder) or associative (cross-folder)."
370
- ].join("\n"), {
371
- start_note: string().describe("Path or title of the note to start traversal from."),
372
- depth: number().int().min(1).max(10).optional().describe("How many link hops to traverse. Default: 3."),
373
- direction: _enum([
374
- "forward",
375
- "backward",
376
- "both"
377
- ]).optional().describe("Traversal direction: 'forward' (outlinks), 'backward' (backlinks), or 'both'. Default: both."),
378
- mode: _enum([
379
- "sequential",
380
- "associative",
381
- "all"
382
- ]).optional().describe("Link type filter: 'sequential' (same-folder), 'associative' (cross-folder), or 'all'. Default: all.")
383
- }, async (args) => {
384
- const result = await toolZettelExplore(getFederationDb(), args);
385
- return {
386
- content: result.content.map((c) => ({
387
- type: c.type,
388
- text: c.text
389
- })),
390
- isError: result.isError
391
- };
392
- });
393
- server.tool("zettel_health", ["Audit the structural health of the Obsidian vault.", "Reports dead links, orphan notes, disconnected clusters, low-connectivity files, and an overall health score."].join("\n"), {
394
- scope: _enum([
395
- "full",
396
- "recent",
397
- "project"
398
- ]).optional().describe("Audit scope: 'full' (entire vault), 'recent' (recently modified), or 'project' (specific path). Default: full."),
399
- project_path: string().optional().describe("Absolute path to the project/folder to audit when scope='project'."),
400
- recent_days: number().int().optional().describe("Number of days to look back when scope='recent'. Default: 30."),
401
- include: array(_enum([
402
- "dead_links",
403
- "orphans",
404
- "disconnected",
405
- "low_connectivity"
406
- ])).optional().describe("Specific checks to include. Omit to run all checks.")
407
- }, async (args) => {
408
- const result = await toolZettelHealth(getFederationDb(), args);
409
- return {
410
- content: result.content.map((c) => ({
411
- type: c.type,
412
- text: c.text
413
- })),
414
- isError: result.isError
415
- };
416
- });
417
- server.tool("zettel_surprise", ["Find surprising connections — notes that are semantically similar to a reference note but far away in the link graph.", "High surprise = unexpected relevance."].join("\n"), {
418
- reference_path: string().describe("Path to the reference note to find surprising connections for."),
419
- vault_project_id: number().int().describe("Project ID of the vault to search within."),
420
- limit: number().int().optional().describe("Maximum number of surprising notes to return. Default: 10."),
421
- min_similarity: number().optional().describe("Minimum semantic similarity [0,1] for a note to be considered. Default: 0.5."),
422
- min_graph_distance: number().int().optional().describe("Minimum link hops away from the reference note. Default: 3.")
423
- }, async (args) => {
424
- const result = await toolZettelSurprise(getFederationDb(), args);
425
- return {
426
- content: result.content.map((c) => ({
427
- type: c.type,
428
- text: c.text
429
- })),
430
- isError: result.isError
431
- };
432
- });
433
- server.tool("zettel_suggest", ["Suggest new connections for a note using semantic similarity, shared tags, and graph neighborhood (friends-of-friends)."].join("\n"), {
434
- note_path: string().describe("Path to the note to generate link suggestions for."),
435
- vault_project_id: number().int().describe("Project ID of the vault to search within."),
436
- limit: number().int().optional().describe("Maximum number of suggestions to return. Default: 10."),
437
- exclude_linked: boolean().optional().describe("Exclude notes already linked from this note. Default: true.")
438
- }, async (args) => {
439
- const result = await toolZettelSuggest(getFederationDb(), args);
440
- return {
441
- content: result.content.map((c) => ({
442
- type: c.type,
443
- text: c.text
444
- })),
445
- isError: result.isError
446
- };
447
- });
448
- server.tool("zettel_converse", ["Use the vault as a Zettelkasten communication partner.", "Ask a question, get relevant notes with cross-domain connections and a synthesis prompt for generating new insights."].join("\n"), {
449
- question: string().describe("The question or topic to explore in the vault."),
450
- vault_project_id: number().int().describe("Project ID of the vault to query."),
451
- depth: number().int().optional().describe("How many link hops to follow from seed notes. Default: 2."),
452
- limit: number().int().optional().describe("Maximum number of relevant notes to retrieve. Default: 10.")
453
- }, async (args) => {
454
- const result = await toolZettelConverse(getFederationDb(), args);
455
- return {
456
- content: result.content.map((c) => ({
457
- type: c.type,
458
- text: c.text
459
- })),
460
- isError: result.isError
461
- };
462
- });
463
- server.tool("zettel_themes", ["Detect emerging themes by clustering recent notes with similar embeddings.", "Reveals forming idea clusters and suggests index notes for unlinked clusters."].join("\n"), {
464
- vault_project_id: number().int().describe("Project ID of the vault to analyse."),
465
- lookback_days: number().int().optional().describe("Number of days of recent notes to cluster. Default: 30."),
466
- min_cluster_size: number().int().optional().describe("Minimum notes required to form a theme cluster. Default: 3."),
467
- max_themes: number().int().optional().describe("Maximum number of theme clusters to return. Default: 10."),
468
- similarity_threshold: number().optional().describe("Minimum cosine similarity to group notes into a cluster [0,1]. Default: 0.7.")
469
- }, async (args) => {
470
- const result = await toolZettelThemes(getFederationDb(), args);
471
- return {
472
- content: result.content.map((c) => ({
473
- type: c.type,
474
- text: c.text
475
- })),
476
- isError: result.isError
477
- };
478
- });
479
- const transport = new StdioServerTransport();
480
- await server.connect(transport);
481
- }
482
-
483
- //#endregion
484
- //#region src/mcp/index.ts
485
- /**
486
- * PAI Knowledge OS — MCP server entry point
487
- *
488
- * When invoked as `node dist/mcp/index.mjs` (or via the `pai-mcp` bin),
489
- * starts the PAI MCP server on stdio transport so Claude Code can call
490
- * memory_search, memory_get, project_info, project_list, session_list,
491
- * and registry_search tools directly during conversations.
492
- */
493
- startMcpServer().catch((err) => {
494
- process.stderr.write(`PAI MCP server fatal error: ${String(err)}\n`);
495
- process.exit(1);
496
- });
497
-
498
- //#endregion
499
- export { };
500
- //# sourceMappingURL=index.mjs.map
@@ -1 +0,0 @@
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 { loadConfig } from \"../daemon/config.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 \"Defaults come from ~/.config/pai/config.json (search section). Per-call parameters override config defaults.\",\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 from config (typically 90). Applied after reranking.\"\n ),\n },\n async (args) => {\n const config = loadConfig();\n const result = await toolMemorySearch(\n getRegistryDb(),\n getFederationDb(),\n { ...args, recencyBoost: args.recency_boost },\n config.search,\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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"}