@tekmidian/pai 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -8,7 +8,8 @@ import { a as indexProject, i as indexFile, r as indexAll, t as detectTier } fro
8
8
  import "./embeddings-DGRAPAYb.mjs";
9
9
  import { n as populateSlugs, r as searchMemory, t as buildFtsQuery } from "./search-_oHfguA5.mjs";
10
10
  import { n as rerankResults, t as configureRerankerModel } from "./reranker-D7bRAHi6.mjs";
11
- import "./tools-Dx7GjOHd.mjs";
11
+ import "./config-Cf92lGX_.mjs";
12
+ import "./tools-DV_lsiCc.mjs";
12
13
  import "./mcp/index.mjs";
13
14
 
14
15
  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 };
@@ -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-CgSpwHDC.mjs.map
156
+ //# sourceMappingURL=ipc-client-Bjg_a1dc.mjs.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"ipc-client-Bjg_a1dc.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"}
@@ -4,7 +4,8 @@ import { n as openFederation } from "../db-Dp8VXIMR.mjs";
4
4
  import "../embeddings-DGRAPAYb.mjs";
5
5
  import "../search-_oHfguA5.mjs";
6
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";
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";
8
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
11
 
@@ -60,6 +61,8 @@ async function startMcpServer() {
60
61
  "",
61
62
  "Recency boost optionally down-weights older results (recency_boost=90 means scores halve every 90 days).",
62
63
  "",
64
+ "Defaults come from ~/.config/pai/config.json (search section). Per-call parameters override config defaults.",
65
+ "",
63
66
  "Returns ranked snippets with project slug, file path, line range, and score.",
64
67
  "Higher score = more relevant."
65
68
  ].join("\n"), {
@@ -74,12 +77,13 @@ async function startMcpServer() {
74
77
  "hybrid"
75
78
  ]).optional().describe("Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined)."),
76
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."),
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.")
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.")
78
81
  }, async (args) => {
82
+ const config = loadConfig();
79
83
  const result = await toolMemorySearch(getRegistryDb(), getFederationDb(), {
80
84
  ...args,
81
85
  recencyBoost: args.recency_boost
82
- });
86
+ }, config.search);
83
87
  return {
84
88
  content: result.content.map((c) => ({
85
89
  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 \"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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"}
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,IAAI,cAAsD;AAC1D,IAAI,gBAA0D;AAE9D,SAAS,gBAAgB;AACvB,KAAI,CAAC,YAAa,eAAc,cAAc;AAC9C,QAAO;;AAGT,SAAS,kBAAkB;AACzB,KAAI,CAAC,cAAe,iBAAgB,gBAAgB;AACpD,QAAO;;AAOT,eAAsB,iBAAgC;CACpD,MAAM,SAAS,IAAI,UAAU;EAC3B,MAAM;EACN,SAAS;EACV,CAAC;AAMF,QAAO,KACL,iBACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ;EACE,OAAOA,QACI,CACR,SAAS,sHAAsH;EAClI,SAASA,QACE,CACR,UAAU,CACV,SACC,yEACD;EACH,cAAcC,SACF,CACT,UAAU,CACV,SACC,8EACD;EACH,SAASC,MACAC,MAAO,CAAC,UAAU,QAAQ,CAAC,CAAC,CAClC,UAAU,CACV,SAAS,0DAA0D;EACtE,OAAOC,QACI,CACR,KAAK,CACL,IAAI,EAAE,CACN,IAAI,IAAI,CACR,UAAU,CACV,SAAS,0CAA0C;EACtD,MAAMC,MACE;GAAC;GAAW;GAAY;GAAS,CAAC,CACvC,UAAU,CACV,SACC,mGACD;EACH,QAAQJ,SACI,CACT,UAAU,CACV,SACC,uJACD;EACH,eAAeG,QACJ,CACR,KAAK,CACL,IAAI,EAAE,CACN,IAAI,IAAI,CACR,UAAU,CACV,SACC,wHACD;EACJ,EACD,OAAO,SAAS;EACd,MAAM,SAAS,YAAY;EAC3B,MAAM,SAAS,MAAM,iBACnB,eAAe,EACf,iBAAiB,EACjB;GAAE,GAAG;GAAM,cAAc,KAAK;GAAe,EAC7C,OAAO,OACR;AACD,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,cACA;EACE;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ;EACE,SAASJ,QACE,CACR,SAAS,+DAA+D;EAC3E,MAAMA,QACK,CACR,SACC,uFACD;EACH,MAAMI,QACK,CACR,KAAK,CACL,IAAI,EAAE,CACN,UAAU,CACV,SAAS,yDAAyD;EACrE,OAAOA,QACI,CACR,KAAK,CACL,IAAI,EAAE,CACN,UAAU,CACV,SAAS,mDAAmD;EAChE,EACD,OAAO,SAAS;EACd,MAAM,SAAS,cAAc,eAAe,EAAE,KAAK;AACnD,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,gBACA;EACE;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ,EACE,MAAMJ,QACK,CACR,UAAU,CACV,SACC,wEACD,EACJ,EACD,OAAO,SAAS;EACd,MAAM,SAAS,gBAAgB,eAAe,EAAE,KAAK;AACrD,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,gBACA;EACE;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ;EACE,QAAQK,MACA;GAAC;GAAU;GAAY;GAAY,CAAC,CACzC,UAAU,CACV,SAAS,mDAAmD;EAC/D,KAAKL,QACM,CACR,UAAU,CACV,SAAS,oCAAoC;EAChD,OAAOI,QACI,CACR,KAAK,CACL,IAAI,EAAE,CACN,IAAI,IAAI,CACR,UAAU,CACV,SAAS,qDAAqD;EAClE,EACD,OAAO,SAAS;EACd,MAAM,SAAS,gBAAgB,eAAe,EAAE,KAAK;AACrD,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,gBACA;EACE;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ;EACE,SAASE,QAAU,CAAC,SAAS,qCAAqC;EAClE,OAAOF,QACI,CACR,KAAK,CACL,IAAI,EAAE,CACN,IAAI,IAAI,CACR,UAAU,CACV,SAAS,+DAA+D;EAC3E,QAAQC,MACA;GAAC;GAAQ;GAAa;GAAY,CAAC,CACxC,UAAU,CACV,SAAS,4BAA4B;EACzC,EACD,OAAO,SAAS;EACd,MAAM,SAAS,gBAAgB,eAAe,EAAE,KAAK;AACrD,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,mBACA;EACE;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ,EACE,OAAOL,QACI,CACR,SACC,+GACD,EACJ,EACD,OAAO,SAAS;EACd,MAAM,SAAS,mBAAmB,eAAe,EAAE,KAAK;AACxD,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,kBACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ,EACE,KAAKA,QACM,CACR,UAAU,CACV,SACC,mFACD,EACJ,EACD,OAAO,SAAS;EACd,MAAM,SAAS,kBAAkB,eAAe,EAAE,KAAK;AACvD,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,kBACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ,EACE,UAAUK,MACF;EAAC;EAAU;EAAS;EAAQ;EAAM,CAAC,CACxC,UAAU,CACV,SAAS,8DAA8D,EAC3E,EACD,OAAO,SAAS;EACd,MAAM,SAAS,MAAM,kBAAkB,eAAe,EAAE,KAAK;AAC7D,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,uBACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ;EACE,QAAQA,MACA;GAAC;GAAO;GAAO;GAAO,CAAC,CAC5B,SAAS,kFAAkF;EAC9F,MAAMA,MACE;GAAC;GAAQ;GAAS;GAAY;GAAQ;GAAS;GAAO;GAAM,CAAC,CAClE,UAAU,CACV,SAAS,yCAAyC;EACrD,UAAUE,OACAD,QAAU,EAAEE,SAAW,CAAC,CAC/B,UAAU,CACV,SACC,qJAED;EACH,SAASD,OACCD,QAAU,EAAEE,SAAW,CAAC,CAC/B,UAAU,CACV,SACC,qIAED;EACH,OAAOH,MACC;GAAC;GAAS;GAAY;GAAc;GAAQ;GAAQ,CAAC,CAC1D,UAAU,CACV,SAAS,gDAAgD;EAC5D,SAASL,QACE,CACR,UAAU,CACV,SAAS,kDAAkD;EAC9D,OAAOA,QACI,CACR,UAAU,CACV,SAAS,yEAAyE;EACtF,EACD,OAAO,SAAS;EACd,MAAM,SAAS,MAAM,uBAAuB,KAAK;AACjD,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,gBACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ;EACE,SAASA,QACE,CACR,SACC,4KAED;EACH,iBAAiBA,QACN,CACR,UAAU,CACV,SACC,uJAED;EACH,WAAWI,QACA,CACR,IAAI,EAAE,CACN,IAAI,EAAE,CACN,UAAU,CACV,SACC,kIAED;EACJ,EACD,OAAO,SAAS;EACd,MAAM,SAAS,MAAM,gBAAgB,KAAK;AAC1C,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,iBACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ;EACE,KAAKJ,QACM,CACR,UAAU,CACV,SACC,mIAED;EACH,SAASA,QACE,CACR,UAAU,CACV,SACC,+KAGD;EACJ,EACD,OAAO,SAAS;EACd,MAAM,SAAS,MAAM,iBAAiB,eAAe,EAAE,iBAAiB,EAAE,KAAK;AAC/E,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,kBACA;EACE;EACA;EACA;EACD,CAAC,KAAK,KAAK,EACZ;EACE,YAAYA,QACD,CACR,SAAS,qDAAqD;EACjE,OAAOI,QACI,CACR,KAAK,CACL,IAAI,EAAE,CACN,IAAI,GAAG,CACP,UAAU,CACV,SAAS,8CAA8C;EAC1D,WAAWC,MACH;GAAC;GAAW;GAAY;GAAO,CAAC,CACrC,UAAU,CACV,SAAS,+FAA+F;EAC3G,MAAMA,MACE;GAAC;GAAc;GAAe;GAAM,CAAC,CAC1C,UAAU,CACV,SAAS,sGAAsG;EACnH,EACD,OAAO,SAAS;EACd,MAAM,SAAS,MAAM,kBAAkB,iBAAiB,EAAE,KAAK;AAC/D,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,iBACA,CACE,sDACA,gHACD,CAAC,KAAK,KAAK,EACZ;EACE,OAAOA,MACC;GAAC;GAAQ;GAAU;GAAU,CAAC,CACnC,UAAU,CACV,SAAS,iHAAiH;EAC7H,cAAcL,QACH,CACR,UAAU,CACV,SAAS,qEAAqE;EACjF,aAAaI,QACF,CACR,KAAK,CACL,UAAU,CACV,SAAS,gEAAgE;EAC5E,SAASF,MACAC,MAAO;GAAC;GAAc;GAAW;GAAgB;GAAmB,CAAC,CAAC,CAC5E,UAAU,CACV,SAAS,sDAAsD;EACnE,EACD,OAAO,SAAS;EACd,MAAM,SAAS,MAAM,iBAAiB,iBAAiB,EAAE,KAAK;AAC9D,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,mBACA,CACE,yHACA,wCACD,CAAC,KAAK,KAAK,EACZ;EACE,gBAAgBH,QACL,CACR,SAAS,iEAAiE;EAC7E,kBAAkBI,QACP,CACR,KAAK,CACL,SAAS,4CAA4C;EACxD,OAAOA,QACI,CACR,KAAK,CACL,UAAU,CACV,SAAS,6DAA6D;EACzE,gBAAgBA,QACL,CACR,UAAU,CACV,SAAS,+EAA+E;EAC3F,oBAAoBA,QACT,CACR,KAAK,CACL,UAAU,CACV,SAAS,8DAA8D;EAC3E,EACD,OAAO,SAAS;EACd,MAAM,SAAS,MAAM,mBAAmB,iBAAiB,EAAE,KAAK;AAChE,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,kBACA,CACE,0HACD,CAAC,KAAK,KAAK,EACZ;EACE,WAAWJ,QACA,CACR,SAAS,qDAAqD;EACjE,kBAAkBI,QACP,CACR,KAAK,CACL,SAAS,4CAA4C;EACxD,OAAOA,QACI,CACR,KAAK,CACL,UAAU,CACV,SAAS,wDAAwD;EACpE,gBAAgBH,SACJ,CACT,UAAU,CACV,SAAS,8DAA8D;EAC3E,EACD,OAAO,SAAS;EACd,MAAM,SAAS,MAAM,kBAAkB,iBAAiB,EAAE,KAAK;AAC/D,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,mBACA,CACE,0DACA,uHACD,CAAC,KAAK,KAAK,EACZ;EACE,UAAUD,QACC,CACR,SAAS,iDAAiD;EAC7D,kBAAkBI,QACP,CACR,KAAK,CACL,SAAS,oCAAoC;EAChD,OAAOA,QACI,CACR,KAAK,CACL,UAAU,CACV,SAAS,4DAA4D;EACxE,OAAOA,QACI,CACR,KAAK,CACL,UAAU,CACV,SAAS,6DAA6D;EAC1E,EACD,OAAO,SAAS;EACd,MAAM,SAAS,MAAM,mBAAmB,iBAAiB,EAAE,KAAK;AAChE,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;AAMD,QAAO,KACL,iBACA,CACE,8EACA,gFACD,CAAC,KAAK,KAAK,EACZ;EACE,kBAAkBA,QACP,CACR,KAAK,CACL,SAAS,sCAAsC;EAClD,eAAeA,QACJ,CACR,KAAK,CACL,UAAU,CACV,SAAS,0DAA0D;EACtE,kBAAkBA,QACP,CACR,KAAK,CACL,UAAU,CACV,SAAS,8DAA8D;EAC1E,YAAYA,QACD,CACR,KAAK,CACL,UAAU,CACV,SAAS,2DAA2D;EACvE,sBAAsBA,QACX,CACR,UAAU,CACV,SAAS,+EAA+E;EAC5F,EACD,OAAO,SAAS;EACd,MAAM,SAAS,MAAM,iBAAiB,iBAAiB,EAAE,KAAK;AAC9D,SAAO;GACL,SAAS,OAAO,QAAQ,KAAK,OAAO;IAAE,MAAM,EAAE;IAAgB,MAAM,EAAE;IAAM,EAAE;GAC9E,SAAS,OAAO;GACjB;GAEJ;CAMD,MAAM,YAAY,IAAI,sBAAsB;AAC5C,OAAM,OAAO,QAAQ,UAAU;;;;;;;;;;;;;AC1zBjC,gBAAgB,CAAC,OAAO,QAAQ;AAE9B,SAAQ,OAAO,MAAM,+BAA+B,OAAO,IAAI,CAAC,IAAI;AACpE,SAAQ,KAAK,EAAE;EACf"}
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
2
2
  import { i as searchMemoryHybrid, n as populateSlugs } from "./search-_oHfguA5.mjs";
3
- import { r as formatDetectionJson, t as detectProject } from "./detect-CdaA48EI.mjs";
3
+ import { r as formatDetectionJson, t as detectProject } from "./detect-BU3Nx_2L.mjs";
4
4
  import { existsSync, readFileSync, statSync } from "node:fs";
5
5
  import { isAbsolute, join, resolve } from "node:path";
6
6
 
@@ -75,7 +75,7 @@ function formatProject(registryDb, project) {
75
75
  if (project.archived_at) lines.push(`archived_at: ${new Date(project.archived_at).toISOString().slice(0, 10)}`);
76
76
  return lines.join("\n");
77
77
  }
78
- async function toolMemorySearch(registryDb, federation, params) {
78
+ async function toolMemorySearch(registryDb, federation, params, searchDefaults) {
79
79
  try {
80
80
  const projectIds = params.project ? (() => {
81
81
  const id = lookupProjectId(registryDb, params.project);
@@ -88,12 +88,12 @@ async function toolMemorySearch(registryDb, federation, params) {
88
88
  }],
89
89
  isError: true
90
90
  };
91
- const mode = params.mode ?? "keyword";
92
- const snippetLength = params.snippetLength ?? 200;
91
+ const mode = params.mode ?? searchDefaults?.mode ?? "keyword";
92
+ const snippetLength = params.snippetLength ?? searchDefaults?.snippetLength ?? 200;
93
93
  const searchOpts = {
94
94
  projectIds,
95
95
  sources: params.sources,
96
- maxResults: params.limit ?? 5
96
+ maxResults: params.limit ?? searchDefaults?.defaultLimit ?? 5
97
97
  };
98
98
  let results;
99
99
  const isBackend = (x) => "backendType" in x;
@@ -123,20 +123,22 @@ async function toolMemorySearch(registryDb, federation, params) {
123
123
  else results = searchMemoryHybrid(federation, params.query, queryEmbedding, searchOpts);
124
124
  } else results = searchMemory(federation, params.query, searchOpts);
125
125
  }
126
- if (params.rerank !== false && results.length > 0) {
126
+ const shouldRerank = params.rerank ?? searchDefaults?.rerank ?? true;
127
+ if (shouldRerank && results.length > 0) {
127
128
  const { rerankResults } = await import("./reranker-D7bRAHi6.mjs").then((n) => n.r);
128
129
  results = await rerankResults(params.query, results, { topK: searchOpts.maxResults ?? 5 });
129
130
  }
130
- if (params.recencyBoost && params.recencyBoost > 0 && results.length > 0) {
131
+ const recencyDays = params.recencyBoost ?? searchDefaults?.recencyBoostDays ?? 0;
132
+ if (recencyDays > 0 && results.length > 0) {
131
133
  const { applyRecencyBoost } = await import("./search-_oHfguA5.mjs").then((n) => n.o);
132
- results = applyRecencyBoost(results, params.recencyBoost);
134
+ results = applyRecencyBoost(results, recencyDays);
133
135
  }
134
136
  const withSlugs = populateSlugs(results, registryDb);
135
137
  if (withSlugs.length === 0) return { content: [{
136
138
  type: "text",
137
139
  text: `No results found for query: "${params.query}" (mode: ${mode})`
138
140
  }] };
139
- const rerankLabel = params.rerank !== false ? " +rerank" : "";
141
+ const rerankLabel = shouldRerank ? " +rerank" : "";
140
142
  const formatted = withSlugs.map((r, i) => {
141
143
  const header = `[${i + 1}] ${r.projectSlug ?? `project:${r.projectId}`} — ${r.path} (lines ${r.startLine}-${r.endLine}) score=${r.score.toFixed(4)} tier=${r.tier} source=${r.source}`;
142
144
  const raw = r.snippet.trim();
@@ -634,7 +636,7 @@ function toolProjectTodo(registryDb, params) {
634
636
  */
635
637
  async function toolNotificationConfig(params) {
636
638
  try {
637
- const { PaiClient } = await import("./ipc-client-CgSpwHDC.mjs").then((n) => n.n);
639
+ const { PaiClient } = await import("./ipc-client-Bjg_a1dc.mjs").then((n) => n.n);
638
640
  const client = new PaiClient();
639
641
  if (params.action === "get") {
640
642
  const { config, activeChannels } = await client.getNotificationConfig();
@@ -721,7 +723,7 @@ async function toolNotificationConfig(params) {
721
723
  */
722
724
  async function toolTopicDetect(params) {
723
725
  try {
724
- const { PaiClient } = await import("./ipc-client-CgSpwHDC.mjs").then((n) => n.n);
726
+ const { PaiClient } = await import("./ipc-client-Bjg_a1dc.mjs").then((n) => n.n);
725
727
  const result = await new PaiClient().topicCheck({
726
728
  context: params.context,
727
729
  currentProject: params.current_project,
@@ -770,7 +772,7 @@ async function toolTopicDetect(params) {
770
772
  */
771
773
  async function toolSessionRoute(registryDb, federation, params) {
772
774
  try {
773
- const { autoRoute, formatAutoRouteJson } = await import("./auto-route-B5MSUJZK.mjs");
775
+ const { autoRoute, formatAutoRouteJson } = await import("./auto-route-BG6I_4B1.mjs");
774
776
  const result = await autoRoute(registryDb, federation, params.cwd, params.context);
775
777
  if (!result) return { content: [{
776
778
  type: "text",
@@ -975,4 +977,4 @@ function combineHybridResults(keywordResults, semanticResults, maxResults, keywo
975
977
 
976
978
  //#endregion
977
979
  export { toolZettelSurprise as _, toolProjectHealth as a, toolProjectTodo as c, toolSessionRoute as d, toolTopicDetect as f, toolZettelSuggest as g, toolZettelHealth as h, toolProjectDetect as i, toolRegistrySearch as l, toolZettelExplore as m, toolMemorySearch as n, toolProjectInfo as o, toolZettelConverse as p, toolNotificationConfig as r, toolProjectList as s, toolMemoryGet as t, toolSessionList as u, toolZettelThemes as v, tools_exports as y };
978
- //# sourceMappingURL=tools-Dx7GjOHd.mjs.map
980
+ //# sourceMappingURL=tools-DV_lsiCc.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools-DV_lsiCc.mjs","names":[],"sources":["../src/mcp/tools.ts"],"sourcesContent":["/**\n * PAI Knowledge OS — Pure tool handler functions (shared by daemon + legacy MCP server)\n *\n * Each function accepts pre-opened database handles and raw params, executes\n * the tool logic, and returns an MCP-style content array.\n *\n * This module does NOT import indexAll() — indexing is handled by the daemon\n * on its own schedule. The search hot path is pure DB read.\n */\n\nimport { readFileSync, existsSync, statSync } from \"node:fs\";\nimport { join, resolve, isAbsolute } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\nimport { populateSlugs, searchMemoryHybrid } from \"../memory/search.js\";\nimport { detectProject, formatDetectionJson } from \"../cli/commands/detect.js\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { NotificationMode, NotificationEvent } from \"../notifications/types.js\";\nimport type { SearchConfig } from \"../daemon/config.js\";\n\n// ---------------------------------------------------------------------------\n// Shared types\n// ---------------------------------------------------------------------------\n\nexport interface ToolContent {\n type: \"text\";\n text: string;\n}\n\nexport interface ToolResult {\n content: ToolContent[];\n isError?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: lookup project_id by slug (also checks aliases)\n// ---------------------------------------------------------------------------\n\nexport function lookupProjectId(\n registryDb: Database,\n slug: string\n): number | null {\n const bySlug = registryDb\n .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n .get(slug) as { id: number } | undefined;\n if (bySlug) return bySlug.id;\n\n const byAlias = registryDb\n .prepare(\"SELECT project_id FROM aliases WHERE alias = ?\")\n .get(slug) as { project_id: number } | undefined;\n if (byAlias) return byAlias.project_id;\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: detect project from a filesystem path\n// ---------------------------------------------------------------------------\n\ninterface ProjectRow {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n type: string;\n status: string;\n created_at: number;\n updated_at: number;\n archived_at?: number | null;\n parent_id?: number | null;\n obsidian_link?: string | null;\n}\n\nexport function detectProjectFromPath(\n registryDb: Database,\n fsPath: string\n): ProjectRow | null {\n const resolved = resolve(fsPath);\n\n const exact = registryDb\n .prepare(\n \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at FROM projects WHERE root_path = ?\"\n )\n .get(resolved) as ProjectRow | undefined;\n\n if (exact) return exact;\n\n const all = registryDb\n .prepare(\n \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at FROM projects ORDER BY LENGTH(root_path) DESC\"\n )\n .all() as ProjectRow[];\n\n for (const project of all) {\n if (\n resolved.startsWith(project.root_path + \"/\") ||\n resolved === project.root_path\n ) {\n return project;\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: format project row for tool output\n// ---------------------------------------------------------------------------\n\nexport function formatProject(registryDb: Database, project: ProjectRow): string {\n const sessionCount = (\n registryDb\n .prepare(\"SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?\")\n .get(project.id) as { n: number }\n ).n;\n\n const lastSession = registryDb\n .prepare(\n \"SELECT date FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1\"\n )\n .get(project.id) as { date: string } | undefined;\n\n const tags = (\n registryDb\n .prepare(\n `SELECT t.name FROM tags t\n JOIN project_tags pt ON pt.tag_id = t.id\n WHERE pt.project_id = ?\n ORDER BY t.name`\n )\n .all(project.id) as Array<{ name: string }>\n ).map((r) => r.name);\n\n const aliases = (\n registryDb\n .prepare(\"SELECT alias FROM aliases WHERE project_id = ? ORDER BY alias\")\n .all(project.id) as Array<{ alias: string }>\n ).map((r) => r.alias);\n\n const lines: string[] = [\n `slug: ${project.slug}`,\n `display_name: ${project.display_name}`,\n `root_path: ${project.root_path}`,\n `type: ${project.type}`,\n `status: ${project.status}`,\n `sessions: ${sessionCount}`,\n ];\n\n if (lastSession) lines.push(`last_session: ${lastSession.date}`);\n if (tags.length) lines.push(`tags: ${tags.join(\", \")}`);\n if (aliases.length) lines.push(`aliases: ${aliases.join(\", \")}`);\n if (project.obsidian_link) lines.push(`obsidian_link: ${project.obsidian_link}`);\n if (project.archived_at) {\n lines.push(\n `archived_at: ${new Date(project.archived_at).toISOString().slice(0, 10)}`\n );\n }\n\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Tool: memory_search\n// ---------------------------------------------------------------------------\n\nexport interface MemorySearchParams {\n query: string;\n project?: string;\n all_projects?: boolean;\n sources?: Array<\"memory\" | \"notes\">;\n limit?: number;\n mode?: \"keyword\" | \"semantic\" | \"hybrid\";\n /** Rerank results using cross-encoder model for better relevance ordering. */\n rerank?: boolean;\n /** Apply recency boost — score decays by half every N days. 0 = off (default). */\n recencyBoost?: number;\n /** Maximum characters per result snippet. Default 200.\n * Limit context consumption — MCP results go into Claude's context window. */\n snippetLength?: number;\n}\n\nexport async function toolMemorySearch(\n registryDb: Database,\n federation: Database | StorageBackend,\n params: MemorySearchParams,\n searchDefaults?: SearchConfig,\n): Promise<ToolResult> {\n try {\n const projectIds: number[] | undefined = params.project\n ? (() => {\n const id = lookupProjectId(registryDb, params.project!);\n return id != null ? [id] : [];\n })()\n : undefined;\n\n if (params.project && (!projectIds || projectIds.length === 0)) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n // NOTE: No indexAll() here — indexing is handled by the daemon scheduler.\n // The daemon ensures the index stays fresh; the search hot path is read-only.\n\n const mode = params.mode ?? (searchDefaults?.mode ?? \"keyword\");\n // Limit context consumption — MCP results go into Claude's context window.\n // Default to 5 results and 200-char snippets to keep a single search call\n // within ~1-2K tokens rather than 5K+.\n const snippetLength = params.snippetLength ?? (searchDefaults?.snippetLength ?? 200);\n const searchOpts = {\n projectIds,\n sources: params.sources,\n maxResults: params.limit ?? (searchDefaults?.defaultLimit ?? 5),\n };\n\n let results;\n\n // Determine if federation is a StorageBackend or a raw Database\n const isBackend = (x: Database | StorageBackend): x is StorageBackend =>\n \"backendType\" in x;\n\n if (isBackend(federation)) {\n // Use the storage backend interface (works for both SQLite and Postgres)\n if (mode === \"keyword\") {\n results = await federation.searchKeyword(params.query, searchOpts);\n } else if (mode === \"semantic\" || mode === \"hybrid\") {\n const { generateEmbedding } = await import(\"../memory/embeddings.js\");\n const queryEmbedding = await generateEmbedding(params.query, true);\n\n if (mode === \"semantic\") {\n results = await federation.searchSemantic(queryEmbedding, searchOpts);\n } else {\n // Hybrid: combine keyword + semantic\n const [kwResults, semResults] = await Promise.all([\n federation.searchKeyword(params.query, { ...searchOpts, maxResults: 50 }),\n federation.searchSemantic(queryEmbedding, { ...searchOpts, maxResults: 50 }),\n ]); // 50 candidates is sufficient for min-max normalization\n // Reuse the existing hybrid scoring logic\n results = combineHybridResults(kwResults, semResults, searchOpts.maxResults ?? 10);\n }\n } else {\n results = await federation.searchKeyword(params.query, searchOpts);\n }\n } else {\n // Legacy path: raw better-sqlite3 Database (for direct MCP server usage)\n const { searchMemory, searchMemorySemantic } = await import(\"../memory/search.js\");\n\n if (mode === \"keyword\") {\n results = searchMemory(federation, params.query, searchOpts);\n } else if (mode === \"semantic\" || mode === \"hybrid\") {\n const { generateEmbedding } = await import(\"../memory/embeddings.js\");\n const queryEmbedding = await generateEmbedding(params.query, true);\n\n if (mode === \"semantic\") {\n results = searchMemorySemantic(federation, queryEmbedding, searchOpts);\n } else {\n results = searchMemoryHybrid(\n federation,\n params.query,\n queryEmbedding,\n searchOpts\n );\n }\n } else {\n results = searchMemory(federation, params.query, searchOpts);\n }\n }\n\n // Cross-encoder reranking (on by default)\n const shouldRerank = params.rerank ?? (searchDefaults?.rerank ?? true);\n if (shouldRerank && results.length > 0) {\n const { rerankResults } = await import(\"../memory/reranker.js\");\n results = await rerankResults(params.query, results, {\n topK: searchOpts.maxResults ?? 5,\n });\n }\n\n // Recency boost (off by default, applied after reranking)\n const recencyDays = params.recencyBoost ?? (searchDefaults?.recencyBoostDays ?? 0);\n if (recencyDays > 0 && results.length > 0) {\n const { applyRecencyBoost } = await import(\"../memory/search.js\");\n results = applyRecencyBoost(results, recencyDays);\n }\n\n const withSlugs = populateSlugs(results, registryDb);\n\n if (withSlugs.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No results found for query: \"${params.query}\" (mode: ${mode})`,\n },\n ],\n };\n }\n\n const rerankLabel = shouldRerank ? \" +rerank\" : \"\";\n const formatted = withSlugs\n .map((r, i) => {\n const header = `[${i + 1}] ${r.projectSlug ?? `project:${r.projectId}`} — ${r.path} (lines ${r.startLine}-${r.endLine}) score=${r.score.toFixed(4)} tier=${r.tier} source=${r.source}`;\n // Truncate snippet to snippetLength — limit context consumption.\n // MCP results go into Claude's context window; keep each result tight.\n const raw = r.snippet.trim();\n const snippet = raw.length > snippetLength\n ? raw.slice(0, snippetLength) + \"...\"\n : raw;\n return `${header}\\n${snippet}`;\n })\n .join(\"\\n\\n---\\n\\n\");\n\n return {\n content: [\n {\n type: \"text\",\n text: `Found ${withSlugs.length} result(s) for \"${params.query}\" (mode: ${mode}${rerankLabel}):\\n\\n${formatted}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Search error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: memory_get\n// ---------------------------------------------------------------------------\n\nexport interface MemoryGetParams {\n project: string;\n path: string;\n from?: number;\n lines?: number;\n}\n\nexport function toolMemoryGet(\n registryDb: Database,\n params: MemoryGetParams\n): ToolResult {\n try {\n const projectId = lookupProjectId(registryDb, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const project = registryDb\n .prepare(\"SELECT root_path FROM projects WHERE id = ?\")\n .get(projectId) as { root_path: string } | undefined;\n\n if (!project) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const requestedPath = params.path;\n if (requestedPath.includes(\"..\") || isAbsolute(requestedPath)) {\n return {\n content: [\n {\n type: \"text\",\n text: `Invalid path: ${params.path} (must be a relative path within the project root, no ../ allowed)`,\n },\n ],\n isError: true,\n };\n }\n\n const fullPath = join(project.root_path, requestedPath);\n const resolvedFull = resolve(fullPath);\n const resolvedRoot = resolve(project.root_path);\n\n if (\n !resolvedFull.startsWith(resolvedRoot + \"/\") &&\n resolvedFull !== resolvedRoot\n ) {\n return {\n content: [\n { type: \"text\", text: `Path traversal blocked: ${params.path}` },\n ],\n isError: true,\n };\n }\n\n if (!existsSync(fullPath)) {\n return {\n content: [\n {\n type: \"text\",\n text: `File not found: ${requestedPath} (project: ${params.project})`,\n },\n ],\n isError: true,\n };\n }\n\n const stat = statSync(fullPath);\n if (stat.size > 5 * 1024 * 1024) {\n return {\n content: [\n {\n type: \"text\",\n text: `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Maximum 5 MB.`,\n },\n ],\n };\n }\n\n const content = readFileSync(fullPath, \"utf8\");\n const allLines = content.split(\"\\n\");\n\n const fromLine = (params.from ?? 1) - 1;\n const toLine =\n params.lines != null\n ? Math.min(fromLine + params.lines, allLines.length)\n : allLines.length;\n\n const selectedLines = allLines.slice(fromLine, toLine);\n const text = selectedLines.join(\"\\n\");\n\n const header =\n params.from != null\n ? `${params.project}/${requestedPath} (lines ${fromLine + 1}-${toLine}):`\n : `${params.project}/${requestedPath}:`;\n\n return {\n content: [{ type: \"text\", text: `${header}\\n\\n${text}` }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Read error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_info\n// ---------------------------------------------------------------------------\n\nexport interface ProjectInfoParams {\n slug?: string;\n}\n\nexport function toolProjectInfo(\n registryDb: Database,\n params: ProjectInfoParams\n): ToolResult {\n try {\n let project: ProjectRow | null = null;\n\n if (params.slug) {\n const projectId = lookupProjectId(registryDb, params.slug);\n if (projectId != null) {\n project = registryDb\n .prepare(\n \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at, archived_at, parent_id, obsidian_link FROM projects WHERE id = ?\"\n )\n .get(projectId) as ProjectRow | null;\n }\n } else {\n const cwd = process.cwd();\n project = detectProjectFromPath(registryDb, cwd);\n }\n\n if (!project) {\n const message = params.slug\n ? `Project not found: ${params.slug}`\n : `No PAI project found matching the current directory: ${process.cwd()}`;\n return {\n content: [{ type: \"text\", text: message }],\n isError: !params.slug,\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatProject(registryDb, project) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_info error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_list\n// ---------------------------------------------------------------------------\n\nexport interface ProjectListParams {\n status?: \"active\" | \"archived\" | \"migrating\";\n tag?: string;\n limit?: number;\n}\n\nexport function toolProjectList(\n registryDb: Database,\n params: ProjectListParams\n): ToolResult {\n try {\n const conditions: string[] = [];\n const queryParams: (string | number)[] = [];\n\n if (params.status) {\n conditions.push(\"p.status = ?\");\n queryParams.push(params.status);\n }\n\n if (params.tag) {\n conditions.push(\n \"p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = ?)\"\n );\n queryParams.push(params.tag);\n }\n\n const where =\n conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n const limit = params.limit ?? 50;\n queryParams.push(limit);\n\n const projects = registryDb\n .prepare(\n `SELECT p.id, p.slug, p.display_name, p.root_path, p.type, p.status, p.updated_at\n FROM projects p\n ${where}\n ORDER BY p.updated_at DESC\n LIMIT ?`\n )\n .all(...queryParams) as Array<{\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n type: string;\n status: string;\n updated_at: number;\n }>;\n\n if (projects.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: \"No projects found matching the given filters.\",\n },\n ],\n };\n }\n\n const lines = projects.map(\n (p) =>\n `${p.slug} [${p.status}] ${p.root_path} (updated: ${new Date(p.updated_at).toISOString().slice(0, 10)})`\n );\n\n return {\n content: [\n {\n type: \"text\",\n text: `${projects.length} project(s):\\n\\n${lines.join(\"\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_list error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: session_list\n// ---------------------------------------------------------------------------\n\nexport interface SessionListParams {\n project: string;\n limit?: number;\n status?: \"open\" | \"completed\" | \"compacted\";\n}\n\nexport function toolSessionList(\n registryDb: Database,\n params: SessionListParams\n): ToolResult {\n try {\n const projectId = lookupProjectId(registryDb, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const conditions = [\"project_id = ?\"];\n const queryParams: (string | number)[] = [projectId];\n\n if (params.status) {\n conditions.push(\"status = ?\");\n queryParams.push(params.status);\n }\n\n const limit = params.limit ?? 10;\n queryParams.push(limit);\n\n const sessions = registryDb\n .prepare(\n `SELECT number, date, title, filename, status\n FROM sessions\n WHERE ${conditions.join(\" AND \")}\n ORDER BY number DESC\n LIMIT ?`\n )\n .all(...queryParams) as Array<{\n number: number;\n date: string;\n title: string;\n filename: string;\n status: string;\n }>;\n\n if (sessions.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No sessions found for project: ${params.project}`,\n },\n ],\n };\n }\n\n const lines = sessions.map(\n (s) =>\n `#${String(s.number).padStart(4, \"0\")} ${s.date} [${s.status}] ${s.title}\\n file: Notes/${s.filename}`\n );\n\n return {\n content: [\n {\n type: \"text\",\n text: `${sessions.length} session(s) for ${params.project}:\\n\\n${lines.join(\"\\n\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `session_list error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: registry_search\n// ---------------------------------------------------------------------------\n\nexport interface RegistrySearchParams {\n query: string;\n}\n\nexport function toolRegistrySearch(\n registryDb: Database,\n params: RegistrySearchParams\n): ToolResult {\n try {\n const q = `%${params.query}%`;\n const projects = registryDb\n .prepare(\n `SELECT id, slug, display_name, root_path, type, status, updated_at\n FROM projects\n WHERE slug LIKE ?\n OR display_name LIKE ?\n OR root_path LIKE ?\n ORDER BY updated_at DESC\n LIMIT 20`\n )\n .all(q, q, q) as Array<{\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n type: string;\n status: string;\n updated_at: number;\n }>;\n\n if (projects.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No projects found matching: \"${params.query}\"`,\n },\n ],\n };\n }\n\n const lines = projects.map((p) => `${p.slug} [${p.status}] ${p.root_path}`);\n\n return {\n content: [\n {\n type: \"text\",\n text: `${projects.length} match(es) for \"${params.query}\":\\n\\n${lines.join(\"\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `registry_search error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_detect\n// ---------------------------------------------------------------------------\n\nexport interface ProjectDetectParams {\n cwd?: string;\n}\n\nexport function toolProjectDetect(\n registryDb: Database,\n params: ProjectDetectParams\n): ToolResult {\n try {\n const detection = detectProject(registryDb, params.cwd);\n\n if (!detection) {\n const target = params.cwd ?? process.cwd();\n return {\n content: [\n {\n type: \"text\",\n text: `No registered project found for path: ${target}\\n\\nRun 'pai project add .' to register this directory.`,\n },\n ],\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatDetectionJson(detection) }],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `project_detect error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_health\n// ---------------------------------------------------------------------------\n\nexport interface ProjectHealthParams {\n category?: \"active\" | \"stale\" | \"dead\" | \"all\";\n}\n\nexport async function toolProjectHealth(\n registryDb: Database,\n params: ProjectHealthParams\n): Promise<ToolResult> {\n try {\n const { existsSync: fsExists, readdirSync, statSync } = await import(\n \"node:fs\"\n );\n const {\n join: pathJoin,\n basename: pathBasename,\n } = await import(\"node:path\");\n const { homedir } = await import(\"node:os\");\n const { encodeDir: enc } = await import(\"../cli/utils.js\");\n\n interface HealthRowLocal {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n status: string;\n type: string;\n session_count: number;\n }\n\n const rows = registryDb\n .prepare(\n `SELECT p.id, p.slug, p.display_name, p.root_path, p.encoded_dir, p.status, p.type,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count\n FROM projects p\n ORDER BY p.slug ASC`\n )\n .all() as HealthRowLocal[];\n\n const home = homedir();\n const claudeProjects = pathJoin(home, \".claude\", \"projects\");\n\n function suggestMoved(rootPath: string): string | undefined {\n const name = pathBasename(rootPath);\n const candidates = [\n pathJoin(home, \"dev\", name),\n pathJoin(home, \"dev\", \"ai\", name),\n pathJoin(home, \"Desktop\", name),\n pathJoin(home, \"Projects\", name),\n ];\n return candidates.find((c) => fsExists(c));\n }\n\n function hasClaudeNotes(encodedDir: string): boolean {\n if (!fsExists(claudeProjects)) return false;\n try {\n for (const entry of readdirSync(claudeProjects)) {\n if (entry !== encodedDir && !entry.startsWith(encodedDir)) continue;\n const full = pathJoin(claudeProjects, entry);\n try {\n if (!statSync(full).isDirectory()) continue;\n } catch {\n continue;\n }\n if (fsExists(pathJoin(full, \"Notes\"))) return true;\n }\n } catch {\n /* ignore */\n }\n return false;\n }\n\n interface HealthResult {\n slug: string;\n display_name: string;\n root_path: string;\n status: string;\n type: string;\n session_count: number;\n health: string;\n suggested_path: string | null;\n has_claude_notes: boolean;\n todo: {\n found: boolean;\n path: string | null;\n has_continue: boolean;\n };\n }\n\n function findTodoForProject(rootPath: string): {\n found: boolean;\n path: string | null;\n has_continue: boolean;\n } {\n const locs = [\n \"Notes/TODO.md\",\n \".claude/Notes/TODO.md\",\n \"tasks/todo.md\",\n \"TODO.md\",\n ];\n for (const rel of locs) {\n const full = pathJoin(rootPath, rel);\n if (fsExists(full)) {\n try {\n const raw = readFileSync(full, \"utf8\");\n const hasContinue = /^## Continue$/m.test(raw);\n return { found: true, path: rel, has_continue: hasContinue };\n } catch {\n return { found: true, path: rel, has_continue: false };\n }\n }\n }\n return { found: false, path: null, has_continue: false };\n }\n\n const results: HealthResult[] = rows.map((p) => {\n const pathExists = fsExists(p.root_path);\n let health: string;\n let suggestedPath: string | null = null;\n\n if (pathExists) {\n health = \"active\";\n } else {\n suggestedPath = suggestMoved(p.root_path) ?? null;\n health = suggestedPath ? \"stale\" : \"dead\";\n }\n\n const todo = pathExists\n ? findTodoForProject(p.root_path)\n : { found: false, path: null, has_continue: false };\n\n return {\n slug: p.slug,\n display_name: p.display_name,\n root_path: p.root_path,\n status: p.status,\n type: p.type,\n session_count: p.session_count,\n health,\n suggested_path: suggestedPath,\n has_claude_notes: hasClaudeNotes(p.encoded_dir),\n todo,\n };\n });\n\n const filtered =\n !params.category || params.category === \"all\"\n ? results\n : results.filter((r) => r.health === params.category);\n\n const summary = {\n total: rows.length,\n active: results.filter((r) => r.health === \"active\").length,\n stale: results.filter((r) => r.health === \"stale\").length,\n dead: results.filter((r) => r.health === \"dead\").length,\n };\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify({ summary, projects: filtered }, null, 2),\n },\n ],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `project_health error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_todo\n// ---------------------------------------------------------------------------\n\nexport interface ProjectTodoParams {\n project?: string;\n}\n\n/**\n * TODO candidate locations searched in priority order.\n * Returns the first one that exists, along with its label.\n */\nconst TODO_LOCATIONS = [\n { rel: \"Notes/TODO.md\", label: \"Notes/TODO.md\" },\n { rel: \".claude/Notes/TODO.md\", label: \".claude/Notes/TODO.md\" },\n { rel: \"tasks/todo.md\", label: \"tasks/todo.md\" },\n { rel: \"TODO.md\", label: \"TODO.md\" },\n];\n\n/**\n * Given TODO file content, extract and surface the ## Continue section first,\n * then return the remaining content. Returns an object with:\n * continueSection: string | null\n * fullContent: string\n * hasContinue: boolean\n */\nfunction parseTodoContent(raw: string): {\n continueSection: string | null;\n fullContent: string;\n hasContinue: boolean;\n} {\n const lines = raw.split(\"\\n\");\n\n // Find the ## Continue heading\n const continueIdx = lines.findIndex(\n (l) => l.trim() === \"## Continue\"\n );\n\n if (continueIdx === -1) {\n return { continueSection: null, fullContent: raw, hasContinue: false };\n }\n\n // The section ends at the first `---` separator or next `##` heading after\n // the Continue heading (whichever comes first).\n let endIdx = lines.length;\n for (let i = continueIdx + 1; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (trimmed === \"---\" || (trimmed.startsWith(\"##\") && trimmed !== \"## Continue\")) {\n endIdx = i;\n break;\n }\n }\n\n const continueLines = lines.slice(continueIdx, endIdx);\n const continueSection = continueLines.join(\"\\n\").trim();\n\n return { continueSection, fullContent: raw, hasContinue: true };\n}\n\nexport function toolProjectTodo(\n registryDb: Database,\n params: ProjectTodoParams\n): ToolResult {\n try {\n let rootPath: string;\n let projectSlug: string;\n\n if (params.project) {\n const projectId = lookupProjectId(registryDb, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const row = registryDb\n .prepare(\"SELECT root_path, slug FROM projects WHERE id = ?\")\n .get(projectId) as { root_path: string; slug: string } | undefined;\n\n if (!row) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n rootPath = row.root_path;\n projectSlug = row.slug;\n } else {\n // Auto-detect from cwd\n const project = detectProjectFromPath(registryDb, process.cwd());\n if (!project) {\n return {\n content: [\n {\n type: \"text\",\n text: `No PAI project found matching the current directory: ${process.cwd()}\\n\\nProvide a project slug or run 'pai project add .' to register this directory.`,\n },\n ],\n };\n }\n rootPath = project.root_path;\n projectSlug = project.slug;\n }\n\n // Search for TODO in priority order\n for (const loc of TODO_LOCATIONS) {\n const fullPath = join(rootPath, loc.rel);\n if (existsSync(fullPath)) {\n const raw = readFileSync(fullPath, \"utf8\");\n const { continueSection, fullContent, hasContinue } = parseTodoContent(raw);\n\n let output: string;\n if (hasContinue && continueSection) {\n // Surface the ## Continue section first, then the full content\n output = [\n `TODO found: ${projectSlug}/${loc.label}`,\n \"\",\n \"=== CONTINUE SECTION (surfaced first) ===\",\n continueSection,\n \"\",\n \"=== FULL TODO CONTENT ===\",\n fullContent,\n ].join(\"\\n\");\n } else {\n output = [\n `TODO found: ${projectSlug}/${loc.label}`,\n \"\",\n fullContent,\n ].join(\"\\n\");\n }\n\n return {\n content: [{ type: \"text\", text: output }],\n };\n }\n }\n\n // No TODO found in any location\n const searched = TODO_LOCATIONS.map((l) => ` ${rootPath}/${l.rel}`).join(\"\\n\");\n return {\n content: [\n {\n type: \"text\",\n text: [\n `No TODO.md found for project: ${projectSlug}`,\n \"\",\n \"Searched locations (in order):\",\n searched,\n \"\",\n \"Create a TODO with: echo '## Tasks\\\\n- [ ] First task' > Notes/TODO.md\",\n ].join(\"\\n\"),\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_todo error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: notification_config\n// ---------------------------------------------------------------------------\n\nexport interface NotificationConfigParams {\n /** Action to perform */\n action: \"get\" | \"set\" | \"send\";\n /** For action=\"set\": the notification mode to activate */\n mode?: NotificationMode;\n /** For action=\"set\": partial channel config overrides (JSON object) */\n channels?: Record<string, unknown>;\n /** For action=\"set\": partial routing overrides (JSON object) */\n routing?: Record<string, unknown>;\n /** For action=\"send\": the event type */\n event?: NotificationEvent;\n /** For action=\"send\": the notification message */\n message?: string;\n /** For action=\"send\": optional title */\n title?: string;\n}\n\n/**\n * Handle notification config queries and updates via the daemon IPC.\n * Falls back gracefully if the daemon is not running.\n */\nexport async function toolNotificationConfig(\n params: NotificationConfigParams\n): Promise<ToolResult> {\n try {\n const { PaiClient } = await import(\"../daemon/ipc-client.js\");\n const client = new PaiClient();\n\n if (params.action === \"get\") {\n const { config, activeChannels } = await client.getNotificationConfig();\n const lines = [\n `mode: ${config.mode}`,\n `active_channels: ${activeChannels.join(\", \") || \"(none)\"}`,\n \"\",\n \"channels:\",\n ...Object.entries(config.channels).map(([ch, cfg]) => {\n const c = cfg as { enabled: boolean };\n return ` ${ch}: ${c.enabled ? \"enabled\" : \"disabled\"}`;\n }),\n \"\",\n \"routing:\",\n ...Object.entries(config.routing).map(\n ([event, channels]) => ` ${event}: ${(channels as string[]).join(\", \") || \"(none)\"}`\n ),\n ];\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n };\n }\n\n if (params.action === \"set\") {\n if (!params.mode && !params.channels && !params.routing) {\n return {\n content: [\n {\n type: \"text\",\n text: \"notification_config set: provide at least one of mode, channels, or routing.\",\n },\n ],\n isError: true,\n };\n }\n const result = await client.setNotificationConfig({\n mode: params.mode,\n channels: params.channels as Parameters<typeof client.setNotificationConfig>[0][\"channels\"],\n routing: params.routing as Parameters<typeof client.setNotificationConfig>[0][\"routing\"],\n });\n return {\n content: [\n {\n type: \"text\",\n text: `Notification config updated. Mode: ${result.config.mode}`,\n },\n ],\n };\n }\n\n if (params.action === \"send\") {\n if (!params.message) {\n return {\n content: [\n { type: \"text\", text: \"notification_config send: message is required.\" },\n ],\n isError: true,\n };\n }\n const result = await client.sendNotification({\n event: params.event ?? \"info\",\n message: params.message,\n title: params.title,\n });\n const lines = [\n `mode: ${result.mode}`,\n `attempted: ${result.channelsAttempted.join(\", \") || \"(none)\"}`,\n `succeeded: ${result.channelsSucceeded.join(\", \") || \"(none)\"}`,\n ...(result.channelsFailed.length > 0\n ? [`failed: ${result.channelsFailed.join(\", \")}`]\n : []),\n ];\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n };\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: `Unknown action: ${String(params.action)}. Use \"get\", \"set\", or \"send\".`,\n },\n ],\n isError: true,\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `notification_config error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: topic_detect\n// ---------------------------------------------------------------------------\n\nexport interface TopicDetectParams {\n /** Recent conversation context (a few sentences summarising recent activity) */\n context: string;\n /** The project slug the session is currently routed to. */\n current_project?: string;\n /**\n * Minimum confidence [0,1] to declare a shift. Default: 0.6.\n * Higher = less sensitive (fewer false positives).\n */\n threshold?: number;\n}\n\n/**\n * Detect whether recent conversation context has shifted to a different project.\n * Uses memory_search to find which project best matches the context, then\n * compares against the current project.\n *\n * Calls the daemon via IPC so it has access to the storage backend.\n * Falls back gracefully if the daemon is not running.\n */\nexport async function toolTopicDetect(\n params: TopicDetectParams\n): Promise<ToolResult> {\n try {\n const { PaiClient } = await import(\"../daemon/ipc-client.js\");\n const client = new PaiClient();\n\n const result = await client.topicCheck({\n context: params.context,\n currentProject: params.current_project,\n threshold: params.threshold,\n });\n\n const lines: string[] = [\n `shifted: ${result.shifted}`,\n `current_project: ${result.currentProject ?? \"(none)\"}`,\n `suggested_project: ${result.suggestedProject ?? \"(none)\"}`,\n `confidence: ${result.confidence.toFixed(3)}`,\n `chunks_scored: ${result.chunkCount}`,\n ];\n\n if (result.topProjects.length > 0) {\n lines.push(\"\");\n lines.push(\"top_matches:\");\n for (const p of result.topProjects) {\n lines.push(` ${p.slug}: ${(p.score * 100).toFixed(1)}%`);\n }\n }\n\n if (result.shifted) {\n lines.push(\"\");\n lines.push(\n `TOPIC SHIFT DETECTED: conversation appears to be about \"${result.suggestedProject}\" ` +\n `(confidence: ${(result.confidence * 100).toFixed(0)}%), not \"${result.currentProject}\".`\n );\n }\n\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `topic_detect error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: session_route\n// ---------------------------------------------------------------------------\n\nexport interface SessionRouteParams {\n /** Working directory to route from (defaults to process.cwd()) */\n cwd?: string;\n /** Optional conversation context for topic-based fallback routing */\n context?: string;\n}\n\n/**\n * Automatically suggest which project a session belongs to.\n *\n * 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\n * 3. topic — BM25 keyword search against memory (requires context)\n *\n * Call this at session start (e.g., from CLAUDE.md or a session-start hook)\n * to automatically route the session to the correct project.\n */\nexport async function toolSessionRoute(\n registryDb: Database,\n federation: Database | StorageBackend,\n params: SessionRouteParams\n): Promise<ToolResult> {\n try {\n const { autoRoute, formatAutoRouteJson } = await import(\"../session/auto-route.js\");\n\n const result = await autoRoute(\n registryDb,\n federation,\n params.cwd,\n params.context\n );\n\n if (!result) {\n const target = params.cwd ?? process.cwd();\n return {\n content: [\n {\n type: \"text\",\n text: [\n `No project match found for: ${target}`,\n \"\",\n \"Tried: path match, PAI.md marker walk\" +\n (params.context ? \", topic detection\" : \"\"),\n \"\",\n \"Run 'pai project add .' to register this directory,\",\n \"or provide conversation context for topic-based routing.\",\n ].join(\"\\n\"),\n },\n ],\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatAutoRouteJson(result) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `session_route error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_explore\n// ---------------------------------------------------------------------------\n\nexport interface ZettelExploreParams {\n start_note: string;\n depth?: number;\n direction?: string;\n mode?: string;\n}\n\nexport async function toolZettelExplore(\n federationDb: Database,\n params: ZettelExploreParams\n): Promise<ToolResult> {\n try {\n const { zettelExplore } = await import(\"../zettelkasten/index.js\");\n const result = zettelExplore(federationDb, {\n startNote: params.start_note,\n depth: params.depth,\n direction: params.direction as \"forward\" | \"backward\" | \"both\" | undefined,\n mode: params.mode as \"sequential\" | \"associative\" | \"all\" | undefined,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_explore error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_health\n// ---------------------------------------------------------------------------\n\nexport interface ZettelHealthParams {\n scope?: string;\n project_path?: string;\n recent_days?: number;\n include?: string[];\n}\n\nexport async function toolZettelHealth(\n federationDb: Database,\n params: ZettelHealthParams\n): Promise<ToolResult> {\n try {\n const { zettelHealth } = await import(\"../zettelkasten/index.js\");\n const result = zettelHealth(federationDb, {\n scope: params.scope as \"full\" | \"recent\" | \"project\" | undefined,\n projectPath: params.project_path,\n recentDays: params.recent_days,\n include: params.include as Array<\"dead_links\" | \"orphans\" | \"disconnected\" | \"low_connectivity\"> | undefined,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_health error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_surprise\n// ---------------------------------------------------------------------------\n\nexport interface ZettelSurpriseParams {\n reference_path: string;\n vault_project_id: number;\n limit?: number;\n min_similarity?: number;\n min_graph_distance?: number;\n}\n\nexport async function toolZettelSurprise(\n federationDb: Database,\n params: ZettelSurpriseParams\n): Promise<ToolResult> {\n try {\n const { zettelSurprise } = await import(\"../zettelkasten/index.js\");\n const results = await zettelSurprise(federationDb, {\n referencePath: params.reference_path,\n vaultProjectId: params.vault_project_id,\n limit: params.limit,\n minSimilarity: params.min_similarity,\n minGraphDistance: params.min_graph_distance,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(results, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_surprise error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_suggest\n// ---------------------------------------------------------------------------\n\nexport interface ZettelSuggestParams {\n note_path: string;\n vault_project_id: number;\n limit?: number;\n exclude_linked?: boolean;\n}\n\nexport async function toolZettelSuggest(\n federationDb: Database,\n params: ZettelSuggestParams\n): Promise<ToolResult> {\n try {\n const { zettelSuggest } = await import(\"../zettelkasten/index.js\");\n const results = await zettelSuggest(federationDb, {\n notePath: params.note_path,\n vaultProjectId: params.vault_project_id,\n limit: params.limit,\n excludeLinked: params.exclude_linked,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(results, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_suggest error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_converse\n// ---------------------------------------------------------------------------\n\nexport interface ZettelConverseParams {\n question: string;\n vault_project_id: number;\n depth?: number;\n limit?: number;\n}\n\nexport async function toolZettelConverse(\n federationDb: Database,\n params: ZettelConverseParams\n): Promise<ToolResult> {\n try {\n const { zettelConverse } = await import(\"../zettelkasten/index.js\");\n const result = await zettelConverse(federationDb, {\n question: params.question,\n vaultProjectId: params.vault_project_id,\n depth: params.depth,\n limit: params.limit,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_converse error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_themes\n// ---------------------------------------------------------------------------\n\nexport interface ZettelThemesParams {\n vault_project_id: number;\n lookback_days?: number;\n min_cluster_size?: number;\n max_themes?: number;\n similarity_threshold?: number;\n}\n\nexport async function toolZettelThemes(\n federationDb: Database,\n params: ZettelThemesParams\n): Promise<ToolResult> {\n try {\n const { zettelThemes } = await import(\"../zettelkasten/index.js\");\n const result = await zettelThemes(federationDb, {\n vaultProjectId: params.vault_project_id,\n lookbackDays: params.lookback_days,\n minClusterSize: params.min_cluster_size,\n maxThemes: params.max_themes,\n similarityThreshold: params.similarity_threshold,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_themes error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search helper (backend-agnostic)\n// ---------------------------------------------------------------------------\n\nimport type { SearchResult } from \"../memory/search.js\";\n\n/**\n * Combine keyword + semantic results using min-max normalized scoring.\n * Mirrors the logic in searchMemoryHybrid() from memory/search.ts,\n * but works on pre-computed result arrays so it works for any backend.\n */\nfunction combineHybridResults(\n keywordResults: SearchResult[],\n semanticResults: SearchResult[],\n maxResults: number,\n keywordWeight = 0.5,\n semanticWeight = 0.5\n): SearchResult[] {\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(items: SearchResult[]): Map<string, number> {\n if (items.length === 0) return new Map();\n const min = Math.min(...items.map((r) => r.score));\n const max = Math.max(...items.map((r) => r.score));\n const range = max - min;\n const m = new Map<string, number>();\n for (const r of items) {\n m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);\n }\n return m;\n }\n\n const kwNorm = minMaxNormalize(keywordResults);\n const semNorm = minMaxNormalize(semanticResults);\n\n const allKeys = new Set<string>([\n ...keywordResults.map(keyFor),\n ...semanticResults.map(keyFor),\n ]);\n\n const metaMap = new Map<string, SearchResult>();\n for (const r of [...keywordResults, ...semanticResults]) {\n metaMap.set(keyFor(r), r);\n }\n\n const combined: Array<SearchResult & { combinedScore: number }> = [];\n for (const key of allKeys) {\n const meta = metaMap.get(key)!;\n const kwScore = kwNorm.get(key) ?? 0;\n const semScore = semNorm.get(key) ?? 0;\n const combinedScore = keywordWeight * kwScore + semanticWeight * semScore;\n combined.push({ ...meta, score: combinedScore, combinedScore });\n }\n\n return combined\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults)\n .map(({ combinedScore: _unused, ...r }) => r);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,gBACd,YACA,MACe;CACf,MAAM,SAAS,WACZ,QAAQ,yCAAyC,CACjD,IAAI,KAAK;AACZ,KAAI,OAAQ,QAAO,OAAO;CAE1B,MAAM,UAAU,WACb,QAAQ,iDAAiD,CACzD,IAAI,KAAK;AACZ,KAAI,QAAS,QAAO,QAAQ;AAE5B,QAAO;;AAqBT,SAAgB,sBACd,YACA,QACmB;CACnB,MAAM,WAAW,QAAQ,OAAO;CAEhC,MAAM,QAAQ,WACX,QACC,mHACD,CACA,IAAI,SAAS;AAEhB,KAAI,MAAO,QAAO;CAElB,MAAM,MAAM,WACT,QACC,+HACD,CACA,KAAK;AAER,MAAK,MAAM,WAAW,IACpB,KACE,SAAS,WAAW,QAAQ,YAAY,IAAI,IAC5C,aAAa,QAAQ,UAErB,QAAO;AAIX,QAAO;;AAOT,SAAgB,cAAc,YAAsB,SAA6B;CAC/E,MAAM,eACJ,WACG,QAAQ,0DAA0D,CAClE,IAAI,QAAQ,GAAG,CAClB;CAEF,MAAM,cAAc,WACjB,QACC,4EACD,CACA,IAAI,QAAQ,GAAG;CAElB,MAAM,OACJ,WACG,QACC;;;0BAID,CACA,IAAI,QAAQ,GAAG,CAClB,KAAK,MAAM,EAAE,KAAK;CAEpB,MAAM,UACJ,WACG,QAAQ,gEAAgE,CACxE,IAAI,QAAQ,GAAG,CAClB,KAAK,MAAM,EAAE,MAAM;CAErB,MAAM,QAAkB;EACtB,SAAS,QAAQ;EACjB,iBAAiB,QAAQ;EACzB,cAAc,QAAQ;EACtB,SAAS,QAAQ;EACjB,WAAW,QAAQ;EACnB,aAAa;EACd;AAED,KAAI,YAAa,OAAM,KAAK,iBAAiB,YAAY,OAAO;AAChE,KAAI,KAAK,OAAQ,OAAM,KAAK,SAAS,KAAK,KAAK,KAAK,GAAG;AACvD,KAAI,QAAQ,OAAQ,OAAM,KAAK,YAAY,QAAQ,KAAK,KAAK,GAAG;AAChE,KAAI,QAAQ,cAAe,OAAM,KAAK,kBAAkB,QAAQ,gBAAgB;AAChF,KAAI,QAAQ,YACV,OAAM,KACJ,gBAAgB,IAAI,KAAK,QAAQ,YAAY,CAAC,aAAa,CAAC,MAAM,GAAG,GAAG,GACzE;AAGH,QAAO,MAAM,KAAK,KAAK;;AAuBzB,eAAsB,iBACpB,YACA,YACA,QACA,gBACqB;AACrB,KAAI;EACF,MAAM,aAAmC,OAAO,iBACrC;GACL,MAAM,KAAK,gBAAgB,YAAY,OAAO,QAAS;AACvD,UAAO,MAAM,OAAO,CAAC,GAAG,GAAG,EAAE;MAC3B,GACJ;AAEJ,MAAI,OAAO,YAAY,CAAC,cAAc,WAAW,WAAW,GAC1D,QAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,sBAAsB,OAAO;IAAW,CAC/D;GACD,SAAS;GACV;EAMH,MAAM,OAAO,OAAO,QAAS,gBAAgB,QAAQ;EAIrD,MAAM,gBAAgB,OAAO,iBAAkB,gBAAgB,iBAAiB;EAChF,MAAM,aAAa;GACjB;GACA,SAAS,OAAO;GAChB,YAAY,OAAO,SAAU,gBAAgB,gBAAgB;GAC9D;EAED,IAAI;EAGJ,MAAM,aAAa,MACjB,iBAAiB;AAEnB,MAAI,UAAU,WAAW,CAEvB,KAAI,SAAS,UACX,WAAU,MAAM,WAAW,cAAc,OAAO,OAAO,WAAW;WACzD,SAAS,cAAc,SAAS,UAAU;GACnD,MAAM,EAAE,sBAAsB,MAAM,OAAO;GAC3C,MAAM,iBAAiB,MAAM,kBAAkB,OAAO,OAAO,KAAK;AAElE,OAAI,SAAS,WACX,WAAU,MAAM,WAAW,eAAe,gBAAgB,WAAW;QAChE;IAEL,MAAM,CAAC,WAAW,cAAc,MAAM,QAAQ,IAAI,CAChD,WAAW,cAAc,OAAO,OAAO;KAAE,GAAG;KAAY,YAAY;KAAI,CAAC,EACzE,WAAW,eAAe,gBAAgB;KAAE,GAAG;KAAY,YAAY;KAAI,CAAC,CAC7E,CAAC;AAEF,cAAU,qBAAqB,WAAW,YAAY,WAAW,cAAc,GAAG;;QAGpF,WAAU,MAAM,WAAW,cAAc,OAAO,OAAO,WAAW;OAE/D;GAEL,MAAM,EAAE,cAAc,yBAAyB,MAAM,OAAO;AAE5D,OAAI,SAAS,UACX,WAAU,aAAa,YAAY,OAAO,OAAO,WAAW;YACnD,SAAS,cAAc,SAAS,UAAU;IACnD,MAAM,EAAE,sBAAsB,MAAM,OAAO;IAC3C,MAAM,iBAAiB,MAAM,kBAAkB,OAAO,OAAO,KAAK;AAElE,QAAI,SAAS,WACX,WAAU,qBAAqB,YAAY,gBAAgB,WAAW;QAEtE,WAAU,mBACR,YACA,OAAO,OACP,gBACA,WACD;SAGH,WAAU,aAAa,YAAY,OAAO,OAAO,WAAW;;EAKhE,MAAM,eAAe,OAAO,UAAW,gBAAgB,UAAU;AACjE,MAAI,gBAAgB,QAAQ,SAAS,GAAG;GACtC,MAAM,EAAE,kBAAkB,MAAM,OAAO;AACvC,aAAU,MAAM,cAAc,OAAO,OAAO,SAAS,EACnD,MAAM,WAAW,cAAc,GAChC,CAAC;;EAIJ,MAAM,cAAc,OAAO,gBAAiB,gBAAgB,oBAAoB;AAChF,MAAI,cAAc,KAAK,QAAQ,SAAS,GAAG;GACzC,MAAM,EAAE,sBAAsB,MAAM,OAAO;AAC3C,aAAU,kBAAkB,SAAS,YAAY;;EAGnD,MAAM,YAAY,cAAc,SAAS,WAAW;AAEpD,MAAI,UAAU,WAAW,EACvB,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,gCAAgC,OAAO,MAAM,WAAW,KAAK;GACpE,CACF,EACF;EAGH,MAAM,cAAc,eAAe,aAAa;EAChD,MAAM,YAAY,UACf,KAAK,GAAG,MAAM;GACb,MAAM,SAAS,IAAI,IAAI,EAAE,IAAI,EAAE,eAAe,WAAW,EAAE,YAAY,KAAK,EAAE,KAAK,UAAU,EAAE,UAAU,GAAG,EAAE,QAAQ,UAAU,EAAE,MAAM,QAAQ,EAAE,CAAC,QAAQ,EAAE,KAAK,UAAU,EAAE;GAG9K,MAAM,MAAM,EAAE,QAAQ,MAAM;AAI5B,UAAO,GAAG,OAAO,IAHD,IAAI,SAAS,gBACzB,IAAI,MAAM,GAAG,cAAc,GAAG,QAC9B;IAEJ,CACD,KAAK,cAAc;AAEtB,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,SAAS,UAAU,OAAO,kBAAkB,OAAO,MAAM,WAAW,OAAO,YAAY,QAAQ;GACtG,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,iBAAiB,OAAO,EAAE;IAAI,CAAC;GAC/D,SAAS;GACV;;;AAeL,SAAgB,cACd,YACA,QACY;AACZ,KAAI;EACF,MAAM,YAAY,gBAAgB,YAAY,OAAO,QAAQ;AAC7D,MAAI,aAAa,KACf,QAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,sBAAsB,OAAO;IAAW,CAC/D;GACD,SAAS;GACV;EAGH,MAAM,UAAU,WACb,QAAQ,8CAA8C,CACtD,IAAI,UAAU;AAEjB,MAAI,CAAC,QACH,QAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,sBAAsB,OAAO;IAAW,CAC/D;GACD,SAAS;GACV;EAGH,MAAM,gBAAgB,OAAO;AAC7B,MAAI,cAAc,SAAS,KAAK,IAAI,WAAW,cAAc,CAC3D,QAAO;GACL,SAAS,CACP;IACE,MAAM;IACN,MAAM,iBAAiB,OAAO,KAAK;IACpC,CACF;GACD,SAAS;GACV;EAGH,MAAM,WAAW,KAAK,QAAQ,WAAW,cAAc;EACvD,MAAM,eAAe,QAAQ,SAAS;EACtC,MAAM,eAAe,QAAQ,QAAQ,UAAU;AAE/C,MACE,CAAC,aAAa,WAAW,eAAe,IAAI,IAC5C,iBAAiB,aAEjB,QAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,2BAA2B,OAAO;IAAQ,CACjE;GACD,SAAS;GACV;AAGH,MAAI,CAAC,WAAW,SAAS,CACvB,QAAO;GACL,SAAS,CACP;IACE,MAAM;IACN,MAAM,mBAAmB,cAAc,aAAa,OAAO,QAAQ;IACpE,CACF;GACD,SAAS;GACV;EAGH,MAAM,OAAO,SAAS,SAAS;AAC/B,MAAI,KAAK,OAAO,IAAI,OAAO,KACzB,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,2BAA2B,KAAK,OAAO,OAAO,MAAM,QAAQ,EAAE,CAAC;GACtE,CACF,EACF;EAIH,MAAM,WADU,aAAa,UAAU,OAAO,CACrB,MAAM,KAAK;EAEpC,MAAM,YAAY,OAAO,QAAQ,KAAK;EACtC,MAAM,SACJ,OAAO,SAAS,OACZ,KAAK,IAAI,WAAW,OAAO,OAAO,SAAS,OAAO,GAClD,SAAS;EAGf,MAAM,OADgB,SAAS,MAAM,UAAU,OAAO,CAC3B,KAAK,KAAK;AAOrC,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,GALhC,OAAO,QAAQ,OACX,GAAG,OAAO,QAAQ,GAAG,cAAc,UAAU,WAAW,EAAE,GAAG,OAAO,MACpE,GAAG,OAAO,QAAQ,GAAG,cAAc,GAGG,MAAM;GAAQ,CAAC,EAC1D;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,eAAe,OAAO,EAAE;IAAI,CAAC;GAC7D,SAAS;GACV;;;AAYL,SAAgB,gBACd,YACA,QACY;AACZ,KAAI;EACF,IAAI,UAA6B;AAEjC,MAAI,OAAO,MAAM;GACf,MAAM,YAAY,gBAAgB,YAAY,OAAO,KAAK;AAC1D,OAAI,aAAa,KACf,WAAU,WACP,QACC,mJACD,CACA,IAAI,UAAU;QAInB,WAAU,sBAAsB,YADpB,QAAQ,KAAK,CACuB;AAGlD,MAAI,CAAC,QAIH,QAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAJZ,OAAO,OACnB,sBAAsB,OAAO,SAC7B,wDAAwD,QAAQ,KAAK;IAE9B,CAAC;GAC1C,SAAS,CAAC,OAAO;GAClB;AAGH,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,cAAc,YAAY,QAAQ;GAAE,CAAC,EACtE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,uBAAuB,OAAO,EAAE;IAAI,CAAC;GACrE,SAAS;GACV;;;AAcL,SAAgB,gBACd,YACA,QACY;AACZ,KAAI;EACF,MAAM,aAAuB,EAAE;EAC/B,MAAM,cAAmC,EAAE;AAE3C,MAAI,OAAO,QAAQ;AACjB,cAAW,KAAK,eAAe;AAC/B,eAAY,KAAK,OAAO,OAAO;;AAGjC,MAAI,OAAO,KAAK;AACd,cAAW,KACT,uGACD;AACD,eAAY,KAAK,OAAO,IAAI;;EAG9B,MAAM,QACJ,WAAW,SAAS,IAAI,SAAS,WAAW,KAAK,QAAQ,KAAK;EAChE,MAAM,QAAQ,OAAO,SAAS;AAC9B,cAAY,KAAK,MAAM;EAEvB,MAAM,WAAW,WACd,QACC;;WAEG,MAAM;;kBAGV,CACA,IAAI,GAAG,YAAY;AAUtB,MAAI,SAAS,WAAW,EACtB,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM;GACP,CACF,EACF;EAGH,MAAM,QAAQ,SAAS,KACpB,MACC,GAAG,EAAE,KAAK,KAAK,EAAE,OAAO,KAAK,EAAE,UAAU,cAAc,IAAI,KAAK,EAAE,WAAW,CAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CAAC,GAC5G;AAED,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,GAAG,SAAS,OAAO,kBAAkB,MAAM,KAAK,KAAK;GAC5D,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,uBAAuB,OAAO,EAAE;IAAI,CAAC;GACrE,SAAS;GACV;;;AAcL,SAAgB,gBACd,YACA,QACY;AACZ,KAAI;EACF,MAAM,YAAY,gBAAgB,YAAY,OAAO,QAAQ;AAC7D,MAAI,aAAa,KACf,QAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,sBAAsB,OAAO;IAAW,CAC/D;GACD,SAAS;GACV;EAGH,MAAM,aAAa,CAAC,iBAAiB;EACrC,MAAM,cAAmC,CAAC,UAAU;AAEpD,MAAI,OAAO,QAAQ;AACjB,cAAW,KAAK,aAAa;AAC7B,eAAY,KAAK,OAAO,OAAO;;EAGjC,MAAM,QAAQ,OAAO,SAAS;AAC9B,cAAY,KAAK,MAAM;EAEvB,MAAM,WAAW,WACd,QACC;;iBAES,WAAW,KAAK,QAAQ,CAAC;;kBAGnC,CACA,IAAI,GAAG,YAAY;AAQtB,MAAI,SAAS,WAAW,EACtB,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,kCAAkC,OAAO;GAChD,CACF,EACF;EAGH,MAAM,QAAQ,SAAS,KACpB,MACC,IAAI,OAAO,EAAE,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,KAAK,EAAE,OAAO,KAAK,EAAE,MAAM,wBAAwB,EAAE,WACzG;AAED,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,GAAG,SAAS,OAAO,kBAAkB,OAAO,QAAQ,OAAO,MAAM,KAAK,OAAO;GACpF,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,uBAAuB,OAAO,EAAE;IAAI,CAAC;GACrE,SAAS;GACV;;;AAYL,SAAgB,mBACd,YACA,QACY;AACZ,KAAI;EACF,MAAM,IAAI,IAAI,OAAO,MAAM;EAC3B,MAAM,WAAW,WACd,QACC;;;;;;mBAOD,CACA,IAAI,GAAG,GAAG,EAAE;AAUf,MAAI,SAAS,WAAW,EACtB,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,gCAAgC,OAAO,MAAM;GACpD,CACF,EACF;EAGH,MAAM,QAAQ,SAAS,KAAK,MAAM,GAAG,EAAE,KAAK,KAAK,EAAE,OAAO,KAAK,EAAE,YAAY;AAE7E,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,GAAG,SAAS,OAAO,kBAAkB,OAAO,MAAM,QAAQ,MAAM,KAAK,KAAK;GACjF,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,0BAA0B,OAAO,EAAE;IAAI,CAC9D;GACD,SAAS;GACV;;;AAYL,SAAgB,kBACd,YACA,QACY;AACZ,KAAI;EACF,MAAM,YAAY,cAAc,YAAY,OAAO,IAAI;AAEvD,MAAI,CAAC,UAEH,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,yCALG,OAAO,OAAO,QAAQ,KAAK,CAKkB;GACvD,CACF,EACF;AAGH,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,oBAAoB,UAAU;GAAE,CAAC,EAClE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,yBAAyB,OAAO,EAAE;IAAI,CAC7D;GACD,SAAS;GACV;;;AAYL,eAAsB,kBACpB,YACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,YAAY,UAAU,aAAa,aAAa,MAAM,OAC5D;EAEF,MAAM,EACJ,MAAM,UACN,UAAU,iBACR,MAAM,OAAO;EACjB,MAAM,EAAE,YAAY,MAAM,OAAO;EACjC,MAAM,EAAE,WAAW,QAAQ,MAAM,OAAO;EAaxC,MAAM,OAAO,WACV,QACC;;;8BAID,CACA,KAAK;EAER,MAAM,OAAO,SAAS;EACtB,MAAM,iBAAiB,SAAS,MAAM,WAAW,WAAW;EAE5D,SAAS,aAAa,UAAsC;GAC1D,MAAM,OAAO,aAAa,SAAS;AAOnC,UANmB;IACjB,SAAS,MAAM,OAAO,KAAK;IAC3B,SAAS,MAAM,OAAO,MAAM,KAAK;IACjC,SAAS,MAAM,WAAW,KAAK;IAC/B,SAAS,MAAM,YAAY,KAAK;IACjC,CACiB,MAAM,MAAM,SAAS,EAAE,CAAC;;EAG5C,SAAS,eAAe,YAA6B;AACnD,OAAI,CAAC,SAAS,eAAe,CAAE,QAAO;AACtC,OAAI;AACF,SAAK,MAAM,SAAS,YAAY,eAAe,EAAE;AAC/C,SAAI,UAAU,cAAc,CAAC,MAAM,WAAW,WAAW,CAAE;KAC3D,MAAM,OAAO,SAAS,gBAAgB,MAAM;AAC5C,SAAI;AACF,UAAI,CAAC,SAAS,KAAK,CAAC,aAAa,CAAE;aAC7B;AACN;;AAEF,SAAI,SAAS,SAAS,MAAM,QAAQ,CAAC,CAAE,QAAO;;WAE1C;AAGR,UAAO;;EAoBT,SAAS,mBAAmB,UAI1B;AAOA,QAAK,MAAM,OANE;IACX;IACA;IACA;IACA;IACD,EACuB;IACtB,MAAM,OAAO,SAAS,UAAU,IAAI;AACpC,QAAI,SAAS,KAAK,CAChB,KAAI;KACF,MAAM,MAAM,aAAa,MAAM,OAAO;AAEtC,YAAO;MAAE,OAAO;MAAM,MAAM;MAAK,cADb,iBAAiB,KAAK,IAAI;MACc;YACtD;AACN,YAAO;MAAE,OAAO;MAAM,MAAM;MAAK,cAAc;MAAO;;;AAI5D,UAAO;IAAE,OAAO;IAAO,MAAM;IAAM,cAAc;IAAO;;EAG1D,MAAM,UAA0B,KAAK,KAAK,MAAM;GAC9C,MAAM,aAAa,SAAS,EAAE,UAAU;GACxC,IAAI;GACJ,IAAI,gBAA+B;AAEnC,OAAI,WACF,UAAS;QACJ;AACL,oBAAgB,aAAa,EAAE,UAAU,IAAI;AAC7C,aAAS,gBAAgB,UAAU;;GAGrC,MAAM,OAAO,aACT,mBAAmB,EAAE,UAAU,GAC/B;IAAE,OAAO;IAAO,MAAM;IAAM,cAAc;IAAO;AAErD,UAAO;IACL,MAAM,EAAE;IACR,cAAc,EAAE;IAChB,WAAW,EAAE;IACb,QAAQ,EAAE;IACV,MAAM,EAAE;IACR,eAAe,EAAE;IACjB;IACA,gBAAgB;IAChB,kBAAkB,eAAe,EAAE,YAAY;IAC/C;IACD;IACD;EAEF,MAAM,WACJ,CAAC,OAAO,YAAY,OAAO,aAAa,QACpC,UACA,QAAQ,QAAQ,MAAM,EAAE,WAAW,OAAO,SAAS;EAEzD,MAAM,UAAU;GACd,OAAO,KAAK;GACZ,QAAQ,QAAQ,QAAQ,MAAM,EAAE,WAAW,SAAS,CAAC;GACrD,OAAO,QAAQ,QAAQ,MAAM,EAAE,WAAW,QAAQ,CAAC;GACnD,MAAM,QAAQ,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC;GAClD;AAED,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,KAAK,UAAU;IAAE;IAAS,UAAU;IAAU,EAAE,MAAM,EAAE;GAC/D,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,yBAAyB,OAAO,EAAE;IAAI,CAC7D;GACD,SAAS;GACV;;;;;;;AAgBL,MAAM,iBAAiB;CACrB;EAAE,KAAK;EAAuB,OAAO;EAAiB;CACtD;EAAE,KAAK;EAAyB,OAAO;EAAyB;CAChE;EAAE,KAAK;EAAuB,OAAO;EAAiB;CACtD;EAAE,KAAK;EAAuB,OAAO;EAAW;CACjD;;;;;;;;AASD,SAAS,iBAAiB,KAIxB;CACA,MAAM,QAAQ,IAAI,MAAM,KAAK;CAG7B,MAAM,cAAc,MAAM,WACvB,MAAM,EAAE,MAAM,KAAK,cACrB;AAED,KAAI,gBAAgB,GAClB,QAAO;EAAE,iBAAiB;EAAM,aAAa;EAAK,aAAa;EAAO;CAKxE,IAAI,SAAS,MAAM;AACnB,MAAK,IAAI,IAAI,cAAc,GAAG,IAAI,MAAM,QAAQ,KAAK;EACnD,MAAM,UAAU,MAAM,GAAG,MAAM;AAC/B,MAAI,YAAY,SAAU,QAAQ,WAAW,KAAK,IAAI,YAAY,eAAgB;AAChF,YAAS;AACT;;;AAOJ,QAAO;EAAE,iBAHa,MAAM,MAAM,aAAa,OAAO,CAChB,KAAK,KAAK,CAAC,MAAM;EAE7B,aAAa;EAAK,aAAa;EAAM;;AAGjE,SAAgB,gBACd,YACA,QACY;AACZ,KAAI;EACF,IAAI;EACJ,IAAI;AAEJ,MAAI,OAAO,SAAS;GAClB,MAAM,YAAY,gBAAgB,YAAY,OAAO,QAAQ;AAC7D,OAAI,aAAa,KACf,QAAO;IACL,SAAS,CACP;KAAE,MAAM;KAAQ,MAAM,sBAAsB,OAAO;KAAW,CAC/D;IACD,SAAS;IACV;GAGH,MAAM,MAAM,WACT,QAAQ,oDAAoD,CAC5D,IAAI,UAAU;AAEjB,OAAI,CAAC,IACH,QAAO;IACL,SAAS,CACP;KAAE,MAAM;KAAQ,MAAM,sBAAsB,OAAO;KAAW,CAC/D;IACD,SAAS;IACV;AAGH,cAAW,IAAI;AACf,iBAAc,IAAI;SACb;GAEL,MAAM,UAAU,sBAAsB,YAAY,QAAQ,KAAK,CAAC;AAChE,OAAI,CAAC,QACH,QAAO,EACL,SAAS,CACP;IACE,MAAM;IACN,MAAM,wDAAwD,QAAQ,KAAK,CAAC;IAC7E,CACF,EACF;AAEH,cAAW,QAAQ;AACnB,iBAAc,QAAQ;;AAIxB,OAAK,MAAM,OAAO,gBAAgB;GAChC,MAAM,WAAW,KAAK,UAAU,IAAI,IAAI;AACxC,OAAI,WAAW,SAAS,EAAE;IAExB,MAAM,EAAE,iBAAiB,aAAa,gBAAgB,iBAD1C,aAAa,UAAU,OAAO,CACiC;IAE3E,IAAI;AACJ,QAAI,eAAe,gBAEjB,UAAS;KACP,eAAe,YAAY,GAAG,IAAI;KAClC;KACA;KACA;KACA;KACA;KACA;KACD,CAAC,KAAK,KAAK;QAEZ,UAAS;KACP,eAAe,YAAY,GAAG,IAAI;KAClC;KACA;KACD,CAAC,KAAK,KAAK;AAGd,WAAO,EACL,SAAS,CAAC;KAAE,MAAM;KAAQ,MAAM;KAAQ,CAAC,EAC1C;;;EAKL,MAAM,WAAW,eAAe,KAAK,MAAM,KAAK,SAAS,GAAG,EAAE,MAAM,CAAC,KAAK,KAAK;AAC/E,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM;IACJ,iCAAiC;IACjC;IACA;IACA;IACA;IACA;IACD,CAAC,KAAK,KAAK;GACb,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,uBAAuB,OAAO,EAAE;IAAI,CAAC;GACrE,SAAS;GACV;;;;;;;AA6BL,eAAsB,uBACpB,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,cAAc,MAAM,OAAO;EACnC,MAAM,SAAS,IAAI,WAAW;AAE9B,MAAI,OAAO,WAAW,OAAO;GAC3B,MAAM,EAAE,QAAQ,mBAAmB,MAAM,OAAO,uBAAuB;AAgBvE,UAAO,EACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAhBd;KACZ,SAAS,OAAO;KAChB,oBAAoB,eAAe,KAAK,KAAK,IAAI;KACjD;KACA;KACA,GAAG,OAAO,QAAQ,OAAO,SAAS,CAAC,KAAK,CAAC,IAAI,SAAS;AAEpD,aAAO,KAAK,GAAG,IADL,IACW,UAAU,YAAY;OAC3C;KACF;KACA;KACA,GAAG,OAAO,QAAQ,OAAO,QAAQ,CAAC,KAC/B,CAAC,OAAO,cAAc,KAAK,MAAM,IAAK,SAAsB,KAAK,KAAK,IAAI,WAC5E;KACF,CAEuC,KAAK,KAAK;IAAE,CAAC,EACpD;;AAGH,MAAI,OAAO,WAAW,OAAO;AAC3B,OAAI,CAAC,OAAO,QAAQ,CAAC,OAAO,YAAY,CAAC,OAAO,QAC9C,QAAO;IACL,SAAS,CACP;KACE,MAAM;KACN,MAAM;KACP,CACF;IACD,SAAS;IACV;AAOH,UAAO,EACL,SAAS,CACP;IACE,MAAM;IACN,MAAM,uCATG,MAAM,OAAO,sBAAsB;KAChD,MAAM,OAAO;KACb,UAAU,OAAO;KACjB,SAAS,OAAO;KACjB,CAAC,EAKuD,OAAO;IAC3D,CACF,EACF;;AAGH,MAAI,OAAO,WAAW,QAAQ;AAC5B,OAAI,CAAC,OAAO,QACV,QAAO;IACL,SAAS,CACP;KAAE,MAAM;KAAQ,MAAM;KAAkD,CACzE;IACD,SAAS;IACV;GAEH,MAAM,SAAS,MAAM,OAAO,iBAAiB;IAC3C,OAAO,OAAO,SAAS;IACvB,SAAS,OAAO;IAChB,OAAO,OAAO;IACf,CAAC;AASF,UAAO,EACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MATd;KACZ,SAAS,OAAO;KAChB,cAAc,OAAO,kBAAkB,KAAK,KAAK,IAAI;KACrD,cAAc,OAAO,kBAAkB,KAAK,KAAK,IAAI;KACrD,GAAI,OAAO,eAAe,SAAS,IAC/B,CAAC,WAAW,OAAO,eAAe,KAAK,KAAK,GAAG,GAC/C,EAAE;KACP,CAEuC,KAAK,KAAK;IAAE,CAAC,EACpD;;AAGH,SAAO;GACL,SAAS,CACP;IACE,MAAM;IACN,MAAM,mBAAmB,OAAO,OAAO,OAAO,CAAC;IAChD,CACF;GACD,SAAS;GACV;UACM,GAAG;AACV,SAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,8BAA8B,OAAO,EAAE;IAAI,CAClE;GACD,SAAS;GACV;;;;;;;;;;;AA4BL,eAAsB,gBACpB,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,cAAc,MAAM,OAAO;EAGnC,MAAM,SAAS,MAFA,IAAI,WAAW,CAEF,WAAW;GACrC,SAAS,OAAO;GAChB,gBAAgB,OAAO;GACvB,WAAW,OAAO;GACnB,CAAC;EAEF,MAAM,QAAkB;GACtB,YAAY,OAAO;GACnB,oBAAoB,OAAO,kBAAkB;GAC7C,sBAAsB,OAAO,oBAAoB;GACjD,eAAe,OAAO,WAAW,QAAQ,EAAE;GAC3C,kBAAkB,OAAO;GAC1B;AAED,MAAI,OAAO,YAAY,SAAS,GAAG;AACjC,SAAM,KAAK,GAAG;AACd,SAAM,KAAK,eAAe;AAC1B,QAAK,MAAM,KAAK,OAAO,YACrB,OAAM,KAAK,KAAK,EAAE,KAAK,KAAK,EAAE,QAAQ,KAAK,QAAQ,EAAE,CAAC,GAAG;;AAI7D,MAAI,OAAO,SAAS;AAClB,SAAM,KAAK,GAAG;AACd,SAAM,KACJ,2DAA2D,OAAO,iBAAiB,kBAClE,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC,WAAW,OAAO,eAAe,IACvF;;AAGH,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,MAAM,KAAK,KAAK;GAAE,CAAC,EACpD;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,uBAAuB,OAAO,EAAE;IAAI,CAAC;GACrE,SAAS;GACV;;;;;;;;;;;;;;AA0BL,eAAsB,iBACpB,YACA,YACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,WAAW,wBAAwB,MAAM,OAAO;EAExD,MAAM,SAAS,MAAM,UACnB,YACA,YACA,OAAO,KACP,OAAO,QACR;AAED,MAAI,CAAC,OAEH,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM;IACJ,+BANO,OAAO,OAAO,QAAQ,KAAK;IAOlC;IACA,2CACG,OAAO,UAAU,sBAAsB;IAC1C;IACA;IACA;IACD,CAAC,KAAK,KAAK;GACb,CACF,EACF;AAGH,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,oBAAoB,OAAO;GAAE,CAAC,EAC/D;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,wBAAwB,OAAO,EAAE;IAAI,CAAC;GACtE,SAAS;GACV;;;AAeL,eAAsB,kBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EACvC,MAAM,SAAS,cAAc,cAAc;GACzC,WAAW,OAAO;GAClB,OAAO,OAAO;GACd,WAAW,OAAO;GAClB,MAAM,OAAO;GACd,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,EAAE;GAAE,CAAC,EACnE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,yBAAyB,OAAO,EAAE;IAAI,CAAC;GACvE,SAAS;GACV;;;AAeL,eAAsB,iBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,iBAAiB,MAAM,OAAO;EACtC,MAAM,SAAS,aAAa,cAAc;GACxC,OAAO,OAAO;GACd,aAAa,OAAO;GACpB,YAAY,OAAO;GACnB,SAAS,OAAO;GACjB,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,EAAE;GAAE,CAAC,EACnE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,wBAAwB,OAAO,EAAE;IAAI,CAAC;GACtE,SAAS;GACV;;;AAgBL,eAAsB,mBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,mBAAmB,MAAM,OAAO;EACxC,MAAM,UAAU,MAAM,eAAe,cAAc;GACjD,eAAe,OAAO;GACtB,gBAAgB,OAAO;GACvB,OAAO,OAAO;GACd,eAAe,OAAO;GACtB,kBAAkB,OAAO;GAC1B,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,SAAS,MAAM,EAAE;GAAE,CAAC,EACpE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,0BAA0B,OAAO,EAAE;IAAI,CAAC;GACxE,SAAS;GACV;;;AAeL,eAAsB,kBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EACvC,MAAM,UAAU,MAAM,cAAc,cAAc;GAChD,UAAU,OAAO;GACjB,gBAAgB,OAAO;GACvB,OAAO,OAAO;GACd,eAAe,OAAO;GACvB,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,SAAS,MAAM,EAAE;GAAE,CAAC,EACpE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,yBAAyB,OAAO,EAAE;IAAI,CAAC;GACvE,SAAS;GACV;;;AAeL,eAAsB,mBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,mBAAmB,MAAM,OAAO;EACxC,MAAM,SAAS,MAAM,eAAe,cAAc;GAChD,UAAU,OAAO;GACjB,gBAAgB,OAAO;GACvB,OAAO,OAAO;GACd,OAAO,OAAO;GACf,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,EAAE;GAAE,CAAC,EACnE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,0BAA0B,OAAO,EAAE;IAAI,CAAC;GACxE,SAAS;GACV;;;AAgBL,eAAsB,iBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,iBAAiB,MAAM,OAAO;EACtC,MAAM,SAAS,MAAM,aAAa,cAAc;GAC9C,gBAAgB,OAAO;GACvB,cAAc,OAAO;GACrB,gBAAgB,OAAO;GACvB,WAAW,OAAO;GAClB,qBAAqB,OAAO;GAC7B,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,EAAE;GAAE,CAAC,EACnE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,wBAAwB,OAAO,EAAE;IAAI,CAAC;GACtE,SAAS;GACV;;;;;;;;AAeL,SAAS,qBACP,gBACA,iBACA,YACA,gBAAgB,IAChB,iBAAiB,IACD;AAChB,KAAI,eAAe,WAAW,KAAK,gBAAgB,WAAW,EAAG,QAAO,EAAE;CAE1E,MAAM,UAAU,MACd,GAAG,EAAE,UAAU,GAAG,EAAE,KAAK,GAAG,EAAE,UAAU,GAAG,EAAE;CAE/C,SAAS,gBAAgB,OAA4C;AACnE,MAAI,MAAM,WAAW,EAAG,wBAAO,IAAI,KAAK;EACxC,MAAM,MAAM,KAAK,IAAI,GAAG,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC;EAElD,MAAM,QADM,KAAK,IAAI,GAAG,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC,GAC9B;EACpB,MAAM,oBAAI,IAAI,KAAqB;AACnC,OAAK,MAAM,KAAK,MACd,GAAE,IAAI,OAAO,EAAE,EAAE,UAAU,IAAI,KAAK,EAAE,QAAQ,OAAO,MAAM;AAE7D,SAAO;;CAGT,MAAM,SAAS,gBAAgB,eAAe;CAC9C,MAAM,UAAU,gBAAgB,gBAAgB;CAEhD,MAAM,UAAU,IAAI,IAAY,CAC9B,GAAG,eAAe,IAAI,OAAO,EAC7B,GAAG,gBAAgB,IAAI,OAAO,CAC/B,CAAC;CAEF,MAAM,0BAAU,IAAI,KAA2B;AAC/C,MAAK,MAAM,KAAK,CAAC,GAAG,gBAAgB,GAAG,gBAAgB,CACrD,SAAQ,IAAI,OAAO,EAAE,EAAE,EAAE;CAG3B,MAAM,WAA4D,EAAE;AACpE,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,OAAO,QAAQ,IAAI,IAAI;EAC7B,MAAM,UAAU,OAAO,IAAI,IAAI,IAAI;EACnC,MAAM,WAAW,QAAQ,IAAI,IAAI,IAAI;EACrC,MAAM,gBAAgB,gBAAgB,UAAU,iBAAiB;AACjE,WAAS,KAAK;GAAE,GAAG;GAAM,OAAO;GAAe;GAAe,CAAC;;AAGjE,QAAO,SACJ,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,CACjC,MAAM,GAAG,WAAW,CACpB,KAAK,EAAE,eAAe,SAAS,GAAG,QAAQ,EAAE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",