@tekmidian/pai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +567 -0
- package/FEATURE.md +108 -0
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/auto-route-D7W6RE06.mjs +86 -0
- package/dist/auto-route-D7W6RE06.mjs.map +1 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +5927 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/config-DBh1bYM2.mjs +151 -0
- package/dist/config-DBh1bYM2.mjs.map +1 -0
- package/dist/daemon/index.d.mts +1 -0
- package/dist/daemon/index.mjs +56 -0
- package/dist/daemon/index.mjs.map +1 -0
- package/dist/daemon-mcp/index.d.mts +1 -0
- package/dist/daemon-mcp/index.mjs +185 -0
- package/dist/daemon-mcp/index.mjs.map +1 -0
- package/dist/daemon-v5O897D4.mjs +773 -0
- package/dist/daemon-v5O897D4.mjs.map +1 -0
- package/dist/db-4lSqLFb8.mjs +199 -0
- package/dist/db-4lSqLFb8.mjs.map +1 -0
- package/dist/db-BcDxXVBu.mjs +110 -0
- package/dist/db-BcDxXVBu.mjs.map +1 -0
- package/dist/detect-BHqYcjJ1.mjs +86 -0
- package/dist/detect-BHqYcjJ1.mjs.map +1 -0
- package/dist/detector-DKA83aTZ.mjs +74 -0
- package/dist/detector-DKA83aTZ.mjs.map +1 -0
- package/dist/embeddings-mfqv-jFu.mjs +91 -0
- package/dist/embeddings-mfqv-jFu.mjs.map +1 -0
- package/dist/factory-BDAiKtYR.mjs +42 -0
- package/dist/factory-BDAiKtYR.mjs.map +1 -0
- package/dist/index.d.mts +307 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +11 -0
- package/dist/indexer-B20bPHL-.mjs +677 -0
- package/dist/indexer-B20bPHL-.mjs.map +1 -0
- package/dist/indexer-backend-BXaocO5r.mjs +360 -0
- package/dist/indexer-backend-BXaocO5r.mjs.map +1 -0
- package/dist/ipc-client-DPy7s3iu.mjs +156 -0
- package/dist/ipc-client-DPy7s3iu.mjs.map +1 -0
- package/dist/mcp/index.d.mts +1 -0
- package/dist/mcp/index.mjs +373 -0
- package/dist/mcp/index.mjs.map +1 -0
- package/dist/migrate-Bwj7qPaE.mjs +241 -0
- package/dist/migrate-Bwj7qPaE.mjs.map +1 -0
- package/dist/pai-marker-DX_mFLum.mjs +186 -0
- package/dist/pai-marker-DX_mFLum.mjs.map +1 -0
- package/dist/postgres-Ccvpc6fC.mjs +335 -0
- package/dist/postgres-Ccvpc6fC.mjs.map +1 -0
- package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
- package/dist/schemas-DjdwzIQ8.mjs +3405 -0
- package/dist/schemas-DjdwzIQ8.mjs.map +1 -0
- package/dist/search-PjftDxxs.mjs +282 -0
- package/dist/search-PjftDxxs.mjs.map +1 -0
- package/dist/sqlite-CHUrNtbI.mjs +90 -0
- package/dist/sqlite-CHUrNtbI.mjs.map +1 -0
- package/dist/tools-CLK4080-.mjs +805 -0
- package/dist/tools-CLK4080-.mjs.map +1 -0
- package/dist/utils-DEWdIFQ0.mjs +160 -0
- package/dist/utils-DEWdIFQ0.mjs.map +1 -0
- package/package.json +72 -0
- package/templates/README.md +181 -0
- package/templates/agent-prefs.example.md +362 -0
- package/templates/claude-md.template.md +733 -0
- package/templates/pai-project.template.md +13 -0
- package/templates/voices.example.json +251 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { connect } from "node:net";
|
|
4
|
+
|
|
5
|
+
//#region src/daemon/ipc-client.ts
|
|
6
|
+
/**
|
|
7
|
+
* ipc-client.ts — IPC client for the PAI Daemon MCP shim
|
|
8
|
+
*
|
|
9
|
+
* PaiClient connects to the Unix Domain Socket served by daemon.ts
|
|
10
|
+
* and forwards tool calls to the daemon. Uses a fresh socket connection per
|
|
11
|
+
* call (connect → write JSON + newline → read response line → parse → destroy).
|
|
12
|
+
* This keeps the client stateless and avoids connection management complexity.
|
|
13
|
+
*
|
|
14
|
+
* Adapted from the Coogle ipc-client pattern (which was adapted from Whazaa).
|
|
15
|
+
*/
|
|
16
|
+
var ipc_client_exports = /* @__PURE__ */ __exportAll({
|
|
17
|
+
IPC_SOCKET_PATH: () => IPC_SOCKET_PATH,
|
|
18
|
+
PaiClient: () => PaiClient
|
|
19
|
+
});
|
|
20
|
+
/** Default socket path */
|
|
21
|
+
const IPC_SOCKET_PATH = "/tmp/pai.sock";
|
|
22
|
+
/** Timeout for IPC calls (60 seconds) */
|
|
23
|
+
const IPC_TIMEOUT_MS = 6e4;
|
|
24
|
+
/**
|
|
25
|
+
* Thin IPC proxy that forwards tool calls to pai-daemon over a Unix
|
|
26
|
+
* Domain Socket. Each call opens a fresh connection, sends one NDJSON request,
|
|
27
|
+
* reads the response, and closes. Stateless and simple.
|
|
28
|
+
*/
|
|
29
|
+
var PaiClient = class {
|
|
30
|
+
socketPath;
|
|
31
|
+
constructor(socketPath) {
|
|
32
|
+
this.socketPath = socketPath ?? IPC_SOCKET_PATH;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Call a PAI tool by name with the given params.
|
|
36
|
+
* Returns the tool result or throws on error.
|
|
37
|
+
*/
|
|
38
|
+
async call(method, params) {
|
|
39
|
+
return this.send(method, params);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check daemon status.
|
|
43
|
+
*/
|
|
44
|
+
async status() {
|
|
45
|
+
return await this.send("status", {});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Trigger an immediate index run.
|
|
49
|
+
*/
|
|
50
|
+
async triggerIndex() {
|
|
51
|
+
await this.send("index_now", {});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the current notification config from the daemon.
|
|
55
|
+
*/
|
|
56
|
+
async getNotificationConfig() {
|
|
57
|
+
return await this.send("notification_get_config", {});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Patch the notification config on the daemon (and persist to disk).
|
|
61
|
+
*/
|
|
62
|
+
async setNotificationConfig(patch) {
|
|
63
|
+
return await this.send("notification_set_config", patch);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Send a notification via the daemon (routes to configured channels).
|
|
67
|
+
*/
|
|
68
|
+
async sendNotification(payload) {
|
|
69
|
+
return await this.send("notification_send", payload);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check whether the provided context text has drifted to a different project
|
|
73
|
+
* than the session's current routing.
|
|
74
|
+
*/
|
|
75
|
+
async topicCheck(params) {
|
|
76
|
+
return await this.send("topic_check", params);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Automatically detect which project a session belongs to.
|
|
80
|
+
* Tries path match, PAI.md marker walk, then topic detection (if context given).
|
|
81
|
+
*/
|
|
82
|
+
async sessionAutoRoute(params) {
|
|
83
|
+
const toolResult = await this.send("session_auto_route", params);
|
|
84
|
+
if (toolResult.isError) return null;
|
|
85
|
+
const text = toolResult.content?.[0]?.text ?? "";
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(text);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Send a single IPC request and wait for the response.
|
|
94
|
+
* Opens a new socket connection per call — simple and reliable.
|
|
95
|
+
*/
|
|
96
|
+
send(method, params) {
|
|
97
|
+
const socketPath = this.socketPath;
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
let socket = null;
|
|
100
|
+
let done = false;
|
|
101
|
+
let buffer = "";
|
|
102
|
+
let timer = null;
|
|
103
|
+
function finish(error, value) {
|
|
104
|
+
if (done) return;
|
|
105
|
+
done = true;
|
|
106
|
+
if (timer !== null) {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
timer = null;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
socket?.destroy();
|
|
112
|
+
} catch {}
|
|
113
|
+
if (error) reject(error);
|
|
114
|
+
else resolve(value);
|
|
115
|
+
}
|
|
116
|
+
socket = connect(socketPath, () => {
|
|
117
|
+
const request = {
|
|
118
|
+
id: randomUUID(),
|
|
119
|
+
method,
|
|
120
|
+
params
|
|
121
|
+
};
|
|
122
|
+
socket.write(JSON.stringify(request) + "\n");
|
|
123
|
+
});
|
|
124
|
+
socket.on("data", (chunk) => {
|
|
125
|
+
buffer += chunk.toString();
|
|
126
|
+
const nl = buffer.indexOf("\n");
|
|
127
|
+
if (nl === -1) return;
|
|
128
|
+
const line = buffer.slice(0, nl);
|
|
129
|
+
buffer = buffer.slice(nl + 1);
|
|
130
|
+
let response;
|
|
131
|
+
try {
|
|
132
|
+
response = JSON.parse(line);
|
|
133
|
+
} catch {
|
|
134
|
+
finish(/* @__PURE__ */ new Error(`IPC parse error: ${line}`));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!response.ok) finish(new Error(response.error ?? "IPC call failed"));
|
|
138
|
+
else finish(null, response.result);
|
|
139
|
+
});
|
|
140
|
+
socket.on("error", (e) => {
|
|
141
|
+
if (e.code === "ENOENT" || e.code === "ECONNREFUSED") finish(/* @__PURE__ */ new Error("PAI daemon not running. Start it with: pai daemon serve"));
|
|
142
|
+
else finish(e);
|
|
143
|
+
});
|
|
144
|
+
socket.on("end", () => {
|
|
145
|
+
if (!done) finish(/* @__PURE__ */ new Error("IPC connection closed before response"));
|
|
146
|
+
});
|
|
147
|
+
timer = setTimeout(() => {
|
|
148
|
+
finish(/* @__PURE__ */ new Error("IPC call timed out after 60s"));
|
|
149
|
+
}, IPC_TIMEOUT_MS);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
export { ipc_client_exports as n, PaiClient as t };
|
|
156
|
+
//# sourceMappingURL=ipc-client-DPy7s3iu.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ipc-client-DPy7s3iu.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 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,OAAkC;;;;;;CAYlF,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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as openRegistry } from "../db-4lSqLFb8.mjs";
|
|
3
|
+
import { n as openFederation } from "../db-BcDxXVBu.mjs";
|
|
4
|
+
import "../embeddings-mfqv-jFu.mjs";
|
|
5
|
+
import "../search-PjftDxxs.mjs";
|
|
6
|
+
import { a as record, i as number, n as array, o as string, r as boolean, s as unknown, t as _enum } from "../schemas-DjdwzIQ8.mjs";
|
|
7
|
+
import { a as toolProjectHealth, d as toolSessionRoute, f as toolTopicDetect, i as toolProjectDetect, l as toolRegistrySearch, n as toolMemorySearch, o as toolProjectInfo, r as toolNotificationConfig, s as toolProjectList, t as toolMemoryGet, u as toolSessionList } from "../tools-CLK4080-.mjs";
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
|
|
11
|
+
//#region src/mcp/server.ts
|
|
12
|
+
/**
|
|
13
|
+
* PAI Knowledge OS — MCP Server (Phase 3)
|
|
14
|
+
*
|
|
15
|
+
* Exposes PAI registry and memory as MCP tools callable by Claude Code.
|
|
16
|
+
*
|
|
17
|
+
* Tools:
|
|
18
|
+
* memory_search — BM25 search across indexed memory/notes
|
|
19
|
+
* memory_get — Read a specific file or lines from a project
|
|
20
|
+
* project_info — Get details for a project (by slug or current dir)
|
|
21
|
+
* project_list — List projects with optional filters
|
|
22
|
+
* session_list — List sessions for a project
|
|
23
|
+
* registry_search — Full-text search over project slugs/names/paths
|
|
24
|
+
* project_detect — Detect which project a path belongs to
|
|
25
|
+
* project_health — Audit all projects for moved/deleted directories
|
|
26
|
+
* session_route — Auto-route session to project (path/marker/topic)
|
|
27
|
+
*
|
|
28
|
+
* NOTE: All tool logic lives in tools.ts (shared with the daemon).
|
|
29
|
+
* This file wires MCP schema definitions to those pure functions.
|
|
30
|
+
*/
|
|
31
|
+
let _registryDb = null;
|
|
32
|
+
let _federationDb = null;
|
|
33
|
+
function getRegistryDb() {
|
|
34
|
+
if (!_registryDb) _registryDb = openRegistry();
|
|
35
|
+
return _registryDb;
|
|
36
|
+
}
|
|
37
|
+
function getFederationDb() {
|
|
38
|
+
if (!_federationDb) _federationDb = openFederation();
|
|
39
|
+
return _federationDb;
|
|
40
|
+
}
|
|
41
|
+
async function startMcpServer() {
|
|
42
|
+
const server = new McpServer({
|
|
43
|
+
name: "pai",
|
|
44
|
+
version: "0.1.0"
|
|
45
|
+
});
|
|
46
|
+
server.tool("memory_search", [
|
|
47
|
+
"Search PAI federated memory using BM25 full-text ranking, semantic similarity, or a hybrid of both.",
|
|
48
|
+
"",
|
|
49
|
+
"Use this BEFORE answering questions about past work, decisions, dates, people,",
|
|
50
|
+
"preferences, project status, todos, technical choices, or anything that might",
|
|
51
|
+
"have been recorded in session notes or memory files.",
|
|
52
|
+
"",
|
|
53
|
+
"Modes:",
|
|
54
|
+
" keyword — BM25 full-text search (default, fast, no embeddings required)",
|
|
55
|
+
" semantic — Cosine similarity over vector embeddings (requires prior embed run)",
|
|
56
|
+
" hybrid — Normalized combination of BM25 + cosine (best quality)",
|
|
57
|
+
"",
|
|
58
|
+
"Returns ranked snippets with project slug, file path, line range, and score.",
|
|
59
|
+
"Higher score = more relevant."
|
|
60
|
+
].join("\n"), {
|
|
61
|
+
query: string().describe("Free-text search query. Multiple words are ORed together — any matching word returns a result, ranked by relevance."),
|
|
62
|
+
project: string().optional().describe("Scope search to a single project by slug. Omit to search all projects."),
|
|
63
|
+
all_projects: boolean().optional().describe("Explicitly search all projects (default behaviour when project is omitted)."),
|
|
64
|
+
sources: array(_enum(["memory", "notes"])).optional().describe("Restrict to specific source types: 'memory' or 'notes'."),
|
|
65
|
+
limit: number().int().min(1).max(100).optional().describe("Maximum results to return. Default: 10."),
|
|
66
|
+
mode: _enum([
|
|
67
|
+
"keyword",
|
|
68
|
+
"semantic",
|
|
69
|
+
"hybrid"
|
|
70
|
+
]).optional().describe("Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined).")
|
|
71
|
+
}, async (args) => {
|
|
72
|
+
const result = await toolMemorySearch(getRegistryDb(), getFederationDb(), args);
|
|
73
|
+
return {
|
|
74
|
+
content: result.content.map((c) => ({
|
|
75
|
+
type: c.type,
|
|
76
|
+
text: c.text
|
|
77
|
+
})),
|
|
78
|
+
isError: result.isError
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
server.tool("memory_get", [
|
|
82
|
+
"Read the content of a specific file from a registered PAI project.",
|
|
83
|
+
"",
|
|
84
|
+
"Use this to read a full memory file, session note, or document after finding",
|
|
85
|
+
"it via memory_search. Optionally restrict to a line range.",
|
|
86
|
+
"",
|
|
87
|
+
"The path must be a relative path within the project root (no ../ traversal)."
|
|
88
|
+
].join("\n"), {
|
|
89
|
+
project: string().describe("Project slug identifying which project's files to read from."),
|
|
90
|
+
path: string().describe("Relative path within the project root (e.g. 'Notes/0001 - 2026-01-01 - Example.md')."),
|
|
91
|
+
from: number().int().min(1).optional().describe("Starting line number (1-based, inclusive). Default: 1."),
|
|
92
|
+
lines: number().int().min(1).optional().describe("Number of lines to return. Default: entire file.")
|
|
93
|
+
}, async (args) => {
|
|
94
|
+
const result = toolMemoryGet(getRegistryDb(), args);
|
|
95
|
+
return {
|
|
96
|
+
content: result.content.map((c) => ({
|
|
97
|
+
type: c.type,
|
|
98
|
+
text: c.text
|
|
99
|
+
})),
|
|
100
|
+
isError: result.isError
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
server.tool("project_info", [
|
|
104
|
+
"Get detailed information about a PAI registered project.",
|
|
105
|
+
"",
|
|
106
|
+
"Use this to look up a project's root path, type, status, tags, session count,",
|
|
107
|
+
"and last active date. If no slug is provided, attempts to detect the current",
|
|
108
|
+
"project from the caller's working directory."
|
|
109
|
+
].join("\n"), { slug: string().optional().describe("Project slug. Omit to auto-detect from the current working directory.") }, async (args) => {
|
|
110
|
+
const result = toolProjectInfo(getRegistryDb(), args);
|
|
111
|
+
return {
|
|
112
|
+
content: result.content.map((c) => ({
|
|
113
|
+
type: c.type,
|
|
114
|
+
text: c.text
|
|
115
|
+
})),
|
|
116
|
+
isError: result.isError
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
server.tool("project_list", [
|
|
120
|
+
"List registered PAI projects with optional filters.",
|
|
121
|
+
"",
|
|
122
|
+
"Use this to browse all known projects, find projects by status or tag,",
|
|
123
|
+
"or get a quick overview of the PAI registry."
|
|
124
|
+
].join("\n"), {
|
|
125
|
+
status: _enum([
|
|
126
|
+
"active",
|
|
127
|
+
"archived",
|
|
128
|
+
"migrating"
|
|
129
|
+
]).optional().describe("Filter by project status. Default: all statuses."),
|
|
130
|
+
tag: string().optional().describe("Filter by tag name (exact match)."),
|
|
131
|
+
limit: number().int().min(1).max(500).optional().describe("Maximum number of projects to return. Default: 50.")
|
|
132
|
+
}, async (args) => {
|
|
133
|
+
const result = toolProjectList(getRegistryDb(), args);
|
|
134
|
+
return {
|
|
135
|
+
content: result.content.map((c) => ({
|
|
136
|
+
type: c.type,
|
|
137
|
+
text: c.text
|
|
138
|
+
})),
|
|
139
|
+
isError: result.isError
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
server.tool("session_list", [
|
|
143
|
+
"List session notes for a PAI project.",
|
|
144
|
+
"",
|
|
145
|
+
"Use this to find what sessions exist for a project, see their dates and titles,",
|
|
146
|
+
"and identify specific session notes to read via memory_get."
|
|
147
|
+
].join("\n"), {
|
|
148
|
+
project: string().describe("Project slug to list sessions for."),
|
|
149
|
+
limit: number().int().min(1).max(500).optional().describe("Maximum sessions to return. Default: 10 (most recent first)."),
|
|
150
|
+
status: _enum([
|
|
151
|
+
"open",
|
|
152
|
+
"completed",
|
|
153
|
+
"compacted"
|
|
154
|
+
]).optional().describe("Filter by session status.")
|
|
155
|
+
}, async (args) => {
|
|
156
|
+
const result = toolSessionList(getRegistryDb(), args);
|
|
157
|
+
return {
|
|
158
|
+
content: result.content.map((c) => ({
|
|
159
|
+
type: c.type,
|
|
160
|
+
text: c.text
|
|
161
|
+
})),
|
|
162
|
+
isError: result.isError
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
server.tool("registry_search", [
|
|
166
|
+
"Search PAI project registry by slug, display name, or path.",
|
|
167
|
+
"",
|
|
168
|
+
"Use this to find the slug for a project when you know its name or path,",
|
|
169
|
+
"or to check if a project is registered. Returns matching project entries."
|
|
170
|
+
].join("\n"), { query: string().describe("Search term matched against project slugs, display names, and root paths (case-insensitive substring match).") }, async (args) => {
|
|
171
|
+
const result = toolRegistrySearch(getRegistryDb(), args);
|
|
172
|
+
return {
|
|
173
|
+
content: result.content.map((c) => ({
|
|
174
|
+
type: c.type,
|
|
175
|
+
text: c.text
|
|
176
|
+
})),
|
|
177
|
+
isError: result.isError
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
server.tool("project_detect", [
|
|
181
|
+
"Detect which registered PAI project a filesystem path belongs to.",
|
|
182
|
+
"",
|
|
183
|
+
"Use this at session start to auto-identify the current project from the",
|
|
184
|
+
"working directory, or to map any path back to its registered project.",
|
|
185
|
+
"",
|
|
186
|
+
"Returns: slug, display_name, root_path, type, status, match_type (exact|parent),",
|
|
187
|
+
"relative_path (if the given path is inside a project), and session stats.",
|
|
188
|
+
"",
|
|
189
|
+
"match_type 'exact' means the path IS the project root.",
|
|
190
|
+
"match_type 'parent' means the path is a subdirectory of the project root."
|
|
191
|
+
].join("\n"), { cwd: string().optional().describe("Absolute path to detect project for. Defaults to the MCP server's process.cwd().") }, async (args) => {
|
|
192
|
+
const result = toolProjectDetect(getRegistryDb(), args);
|
|
193
|
+
return {
|
|
194
|
+
content: result.content.map((c) => ({
|
|
195
|
+
type: c.type,
|
|
196
|
+
text: c.text
|
|
197
|
+
})),
|
|
198
|
+
isError: result.isError
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
server.tool("project_health", [
|
|
202
|
+
"Audit all registered PAI projects to find moved or deleted directories.",
|
|
203
|
+
"",
|
|
204
|
+
"Returns a JSON report categorising every project as:",
|
|
205
|
+
" active — root_path exists on disk",
|
|
206
|
+
" stale — root_path missing, but a directory with the same name was found nearby",
|
|
207
|
+
" dead — root_path missing, no candidate found",
|
|
208
|
+
"",
|
|
209
|
+
"Use this to diagnose orphaned sessions or missing project paths."
|
|
210
|
+
].join("\n"), { category: _enum([
|
|
211
|
+
"active",
|
|
212
|
+
"stale",
|
|
213
|
+
"dead",
|
|
214
|
+
"all"
|
|
215
|
+
]).optional().describe("Filter results to a specific health category. Default: all.") }, async (args) => {
|
|
216
|
+
const result = await toolProjectHealth(getRegistryDb(), args);
|
|
217
|
+
return {
|
|
218
|
+
content: result.content.map((c) => ({
|
|
219
|
+
type: c.type,
|
|
220
|
+
text: c.text
|
|
221
|
+
})),
|
|
222
|
+
isError: result.isError
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
server.tool("notification_config", [
|
|
226
|
+
"Query or update the PAI unified notification configuration.",
|
|
227
|
+
"",
|
|
228
|
+
"Actions:",
|
|
229
|
+
" get — Return the current notification mode, active channels, and routing table.",
|
|
230
|
+
" set — Change the notification mode or update channel/routing config.",
|
|
231
|
+
" send — Send a notification through the configured channels.",
|
|
232
|
+
"",
|
|
233
|
+
"Notification modes:",
|
|
234
|
+
" auto — Use the per-event routing table (default)",
|
|
235
|
+
" voice — All events sent as WhatsApp voice (TTS)",
|
|
236
|
+
" whatsapp — All events sent as WhatsApp text",
|
|
237
|
+
" ntfy — All events sent to ntfy.sh",
|
|
238
|
+
" macos — All events sent as macOS notifications",
|
|
239
|
+
" cli — All events written to CLI output only",
|
|
240
|
+
" off — Suppress all notifications",
|
|
241
|
+
"",
|
|
242
|
+
"Event types for send: error | progress | completion | info | debug",
|
|
243
|
+
"",
|
|
244
|
+
"Examples:",
|
|
245
|
+
" { \"action\": \"get\" }",
|
|
246
|
+
" { \"action\": \"set\", \"mode\": \"voice\" }",
|
|
247
|
+
" { \"action\": \"send\", \"event\": \"completion\", \"message\": \"Done!\" }"
|
|
248
|
+
].join("\n"), {
|
|
249
|
+
action: _enum([
|
|
250
|
+
"get",
|
|
251
|
+
"set",
|
|
252
|
+
"send"
|
|
253
|
+
]).describe("Action: 'get' (read config), 'set' (update config), 'send' (send notification)."),
|
|
254
|
+
mode: _enum([
|
|
255
|
+
"auto",
|
|
256
|
+
"voice",
|
|
257
|
+
"whatsapp",
|
|
258
|
+
"ntfy",
|
|
259
|
+
"macos",
|
|
260
|
+
"cli",
|
|
261
|
+
"off"
|
|
262
|
+
]).optional().describe("For action=set: new notification mode."),
|
|
263
|
+
channels: record(string(), unknown()).optional().describe("For action=set: partial channel config overrides as a JSON object. E.g. { \"whatsapp\": { \"enabled\": true }, \"macos\": { \"enabled\": false } }"),
|
|
264
|
+
routing: record(string(), unknown()).optional().describe("For action=set: partial routing overrides as a JSON object. E.g. { \"error\": [\"whatsapp\", \"macos\"], \"progress\": [\"cli\"] }"),
|
|
265
|
+
event: _enum([
|
|
266
|
+
"error",
|
|
267
|
+
"progress",
|
|
268
|
+
"completion",
|
|
269
|
+
"info",
|
|
270
|
+
"debug"
|
|
271
|
+
]).optional().describe("For action=send: event type. Default: 'info'."),
|
|
272
|
+
message: string().optional().describe("For action=send: the notification message body."),
|
|
273
|
+
title: string().optional().describe("For action=send: optional notification title (used by macOS and ntfy).")
|
|
274
|
+
}, async (args) => {
|
|
275
|
+
const result = await toolNotificationConfig(args);
|
|
276
|
+
return {
|
|
277
|
+
content: result.content.map((c) => ({
|
|
278
|
+
type: c.type,
|
|
279
|
+
text: c.text
|
|
280
|
+
})),
|
|
281
|
+
isError: result.isError
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
server.tool("topic_detect", [
|
|
285
|
+
"Detect whether recent conversation context has shifted to a different project.",
|
|
286
|
+
"",
|
|
287
|
+
"Call this when the conversation may have drifted away from the initially-routed project.",
|
|
288
|
+
"Provide a short summary of the recent context (last few messages or tool call results).",
|
|
289
|
+
"",
|
|
290
|
+
"Returns:",
|
|
291
|
+
" shifted — true if a topic shift was detected",
|
|
292
|
+
" current_project — the project the session is currently routed to",
|
|
293
|
+
" suggested_project — the project that best matches the context",
|
|
294
|
+
" confidence — [0,1] fraction of memory mass held by suggested_project",
|
|
295
|
+
" chunks_scored — number of memory chunks that contributed to scoring",
|
|
296
|
+
" top_matches — top-3 projects with their confidence percentages",
|
|
297
|
+
"",
|
|
298
|
+
"A shift is reported when confidence >= threshold (default 0.6) and the",
|
|
299
|
+
"best-matching project differs from current_project.",
|
|
300
|
+
"",
|
|
301
|
+
"Use cases:",
|
|
302
|
+
" - Call at session start to confirm routing is correct",
|
|
303
|
+
" - Call periodically when working across multiple concerns",
|
|
304
|
+
" - Integrate with pre-tool hooks for automatic drift detection"
|
|
305
|
+
].join("\n"), {
|
|
306
|
+
context: string().describe("Recent conversation context: a few sentences summarising what the session has been discussing. Can include file paths, feature names, commands run, or any relevant text."),
|
|
307
|
+
current_project: string().optional().describe("The project slug this session is currently routed to. If omitted, the tool still returns the best-matching project but shifted will always be false."),
|
|
308
|
+
threshold: number().min(0).max(1).optional().describe("Minimum confidence [0,1] to declare a shift. Default: 0.6. Increase to reduce false positives. Decrease to catch subtle drifts.")
|
|
309
|
+
}, async (args) => {
|
|
310
|
+
const result = await toolTopicDetect(args);
|
|
311
|
+
return {
|
|
312
|
+
content: result.content.map((c) => ({
|
|
313
|
+
type: c.type,
|
|
314
|
+
text: c.text
|
|
315
|
+
})),
|
|
316
|
+
isError: result.isError
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
server.tool("session_route", [
|
|
320
|
+
"Automatically detect which project this session belongs to.",
|
|
321
|
+
"",
|
|
322
|
+
"Call this at session start (e.g., from CLAUDE.md or a session-start hook)",
|
|
323
|
+
"to route the session to the correct project automatically.",
|
|
324
|
+
"",
|
|
325
|
+
"Detection strategy (in priority order):",
|
|
326
|
+
" 1. path — exact or parent-directory match in the project registry",
|
|
327
|
+
" 2. marker — walk up from cwd looking for Notes/PAI.md marker files",
|
|
328
|
+
" 3. topic — BM25 keyword search against memory (only if context provided)",
|
|
329
|
+
"",
|
|
330
|
+
"Returns:",
|
|
331
|
+
" slug — the matched project slug",
|
|
332
|
+
" display_name — human-readable project name",
|
|
333
|
+
" root_path — absolute path to the project root",
|
|
334
|
+
" method — how it was detected: 'path', 'marker', or 'topic'",
|
|
335
|
+
" confidence — 1.0 for path/marker matches, BM25 fraction for topic",
|
|
336
|
+
"",
|
|
337
|
+
"If no match is found, returns a message explaining what was tried.",
|
|
338
|
+
"Run 'pai project add .' to register the current directory."
|
|
339
|
+
].join("\n"), {
|
|
340
|
+
cwd: string().optional().describe("Working directory to detect from. Defaults to process.cwd(). Pass the session's actual working directory for accurate detection."),
|
|
341
|
+
context: string().optional().describe("Optional conversation context for topic-based fallback routing. A few sentences summarising what the session will work on. Only used if path and marker detection both fail.")
|
|
342
|
+
}, async (args) => {
|
|
343
|
+
const result = await toolSessionRoute(getRegistryDb(), getFederationDb(), args);
|
|
344
|
+
return {
|
|
345
|
+
content: result.content.map((c) => ({
|
|
346
|
+
type: c.type,
|
|
347
|
+
text: c.text
|
|
348
|
+
})),
|
|
349
|
+
isError: result.isError
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
const transport = new StdioServerTransport();
|
|
353
|
+
await server.connect(transport);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/mcp/index.ts
|
|
358
|
+
/**
|
|
359
|
+
* PAI Knowledge OS — MCP server entry point
|
|
360
|
+
*
|
|
361
|
+
* When invoked as `node dist/mcp/index.mjs` (or via the `pai-mcp` bin),
|
|
362
|
+
* starts the PAI MCP server on stdio transport so Claude Code can call
|
|
363
|
+
* memory_search, memory_get, project_info, project_list, session_list,
|
|
364
|
+
* and registry_search tools directly during conversations.
|
|
365
|
+
*/
|
|
366
|
+
startMcpServer().catch((err) => {
|
|
367
|
+
process.stderr.write(`PAI MCP server fatal error: ${String(err)}\n`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
//#endregion
|
|
372
|
+
export { };
|
|
373
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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} from \"./tools.js\";\n\n// ---------------------------------------------------------------------------\n// Database singletons (opened lazily, once per MCP server process)\n// ---------------------------------------------------------------------------\n\nlet _registryDb: ReturnType<typeof openRegistry> | null = null;\nlet _federationDb: ReturnType<typeof openFederation> | null = null;\n\nfunction getRegistryDb() {\n if (!_registryDb) _registryDb = openRegistry();\n return _registryDb;\n}\n\nfunction getFederationDb() {\n if (!_federationDb) _federationDb = openFederation();\n return _federationDb;\n}\n\n// ---------------------------------------------------------------------------\n// MCP server startup\n// ---------------------------------------------------------------------------\n\nexport async function startMcpServer(): Promise<void> {\n const server = new McpServer({\n name: \"pai\",\n version: \"0.1.0\",\n });\n\n // -------------------------------------------------------------------------\n // Tool: memory_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_search\",\n [\n \"Search PAI federated memory using BM25 full-text ranking, semantic similarity, or a hybrid of both.\",\n \"\",\n \"Use this BEFORE answering questions about past work, decisions, dates, people,\",\n \"preferences, project status, todos, technical choices, or anything that might\",\n \"have been recorded in session notes or memory files.\",\n \"\",\n \"Modes:\",\n \" keyword — BM25 full-text search (default, fast, no embeddings required)\",\n \" semantic — Cosine similarity over vector embeddings (requires prior embed run)\",\n \" hybrid — Normalized combination of BM25 + cosine (best quality)\",\n \"\",\n \"Returns ranked snippets with project slug, file path, line range, and score.\",\n \"Higher score = more relevant.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\"Free-text search query. Multiple words are ORed together — any matching word returns a result, ranked by relevance.\"),\n project: z\n .string()\n .optional()\n .describe(\n \"Scope search to a single project by slug. Omit to search all projects.\"\n ),\n all_projects: z\n .boolean()\n .optional()\n .describe(\n \"Explicitly search all projects (default behaviour when project is omitted).\"\n ),\n sources: z\n .array(z.enum([\"memory\", \"notes\"]))\n .optional()\n .describe(\"Restrict to specific source types: 'memory' or 'notes'.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum results to return. Default: 10.\"),\n mode: z\n .enum([\"keyword\", \"semantic\", \"hybrid\"])\n .optional()\n .describe(\n \"Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined).\"\n ),\n },\n async (args) => {\n const result = await toolMemorySearch(\n getRegistryDb(),\n getFederationDb(),\n args\n );\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: memory_get\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_get\",\n [\n \"Read the content of a specific file from a registered PAI project.\",\n \"\",\n \"Use this to read a full memory file, session note, or document after finding\",\n \"it via memory_search. Optionally restrict to a line range.\",\n \"\",\n \"The path must be a relative path within the project root (no ../ traversal).\",\n ].join(\"\\n\"),\n {\n project: z\n .string()\n .describe(\"Project slug identifying which project's files to read from.\"),\n path: z\n .string()\n .describe(\n \"Relative path within the project root (e.g. 'Notes/0001 - 2026-01-01 - Example.md').\"\n ),\n from: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Starting line number (1-based, inclusive). Default: 1.\"),\n lines: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Number of lines to return. Default: entire file.\"),\n },\n async (args) => {\n const result = toolMemoryGet(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_info\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_info\",\n [\n \"Get detailed information about a PAI registered project.\",\n \"\",\n \"Use this to look up a project's root path, type, status, tags, session count,\",\n \"and last active date. If no slug is provided, attempts to detect the current\",\n \"project from the caller's working directory.\",\n ].join(\"\\n\"),\n {\n slug: z\n .string()\n .optional()\n .describe(\n \"Project slug. Omit to auto-detect from the current working directory.\"\n ),\n },\n async (args) => {\n const result = toolProjectInfo(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_list\",\n [\n \"List registered PAI projects with optional filters.\",\n \"\",\n \"Use this to browse all known projects, find projects by status or tag,\",\n \"or get a quick overview of the PAI registry.\",\n ].join(\"\\n\"),\n {\n status: z\n .enum([\"active\", \"archived\", \"migrating\"])\n .optional()\n .describe(\"Filter by project status. Default: all statuses.\"),\n tag: z\n .string()\n .optional()\n .describe(\"Filter by tag name (exact match).\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum number of projects to return. Default: 50.\"),\n },\n async (args) => {\n const result = toolProjectList(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_list\",\n [\n \"List session notes for a PAI project.\",\n \"\",\n \"Use this to find what sessions exist for a project, see their dates and titles,\",\n \"and identify specific session notes to read via memory_get.\",\n ].join(\"\\n\"),\n {\n project: z.string().describe(\"Project slug to list sessions for.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum sessions to return. Default: 10 (most recent first).\"),\n status: z\n .enum([\"open\", \"completed\", \"compacted\"])\n .optional()\n .describe(\"Filter by session status.\"),\n },\n async (args) => {\n const result = toolSessionList(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: registry_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"registry_search\",\n [\n \"Search PAI project registry by slug, display name, or path.\",\n \"\",\n \"Use this to find the slug for a project when you know its name or path,\",\n \"or to check if a project is registered. Returns matching project entries.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\n \"Search term matched against project slugs, display names, and root paths (case-insensitive substring match).\"\n ),\n },\n async (args) => {\n const result = toolRegistrySearch(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_detect\",\n [\n \"Detect which registered PAI project a filesystem path belongs to.\",\n \"\",\n \"Use this at session start to auto-identify the current project from the\",\n \"working directory, or to map any path back to its registered project.\",\n \"\",\n \"Returns: slug, display_name, root_path, type, status, match_type (exact|parent),\",\n \"relative_path (if the given path is inside a project), and session stats.\",\n \"\",\n \"match_type 'exact' means the path IS the project root.\",\n \"match_type 'parent' means the path is a subdirectory of the project root.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Absolute path to detect project for. Defaults to the MCP server's process.cwd().\"\n ),\n },\n async (args) => {\n const result = toolProjectDetect(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_health\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_health\",\n [\n \"Audit all registered PAI projects to find moved or deleted directories.\",\n \"\",\n \"Returns a JSON report categorising every project as:\",\n \" active — root_path exists on disk\",\n \" stale — root_path missing, but a directory with the same name was found nearby\",\n \" dead — root_path missing, no candidate found\",\n \"\",\n \"Use this to diagnose orphaned sessions or missing project paths.\",\n ].join(\"\\n\"),\n {\n category: z\n .enum([\"active\", \"stale\", \"dead\", \"all\"])\n .optional()\n .describe(\"Filter results to a specific health category. Default: all.\"),\n },\n async (args) => {\n const result = await toolProjectHealth(getRegistryDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: notification_config\n // -------------------------------------------------------------------------\n\n server.tool(\n \"notification_config\",\n [\n \"Query or update the PAI unified notification configuration.\",\n \"\",\n \"Actions:\",\n \" get — Return the current notification mode, active channels, and routing table.\",\n \" set — Change the notification mode or update channel/routing config.\",\n \" send — Send a notification through the configured channels.\",\n \"\",\n \"Notification modes:\",\n \" auto — Use the per-event routing table (default)\",\n \" voice — All events sent as WhatsApp voice (TTS)\",\n \" whatsapp — All events sent as WhatsApp text\",\n \" ntfy — All events sent to ntfy.sh\",\n \" macos — All events sent as macOS notifications\",\n \" cli — All events written to CLI output only\",\n \" off — Suppress all notifications\",\n \"\",\n \"Event types for send: error | progress | completion | info | debug\",\n \"\",\n \"Examples:\",\n ' { \"action\": \"get\" }',\n ' { \"action\": \"set\", \"mode\": \"voice\" }',\n ' { \"action\": \"send\", \"event\": \"completion\", \"message\": \"Done!\" }',\n ].join(\"\\n\"),\n {\n action: z\n .enum([\"get\", \"set\", \"send\"])\n .describe(\"Action: 'get' (read config), 'set' (update config), 'send' (send notification).\"),\n mode: z\n .enum([\"auto\", \"voice\", \"whatsapp\", \"ntfy\", \"macos\", \"cli\", \"off\"])\n .optional()\n .describe(\"For action=set: new notification mode.\"),\n channels: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"For action=set: partial channel config overrides as a JSON object. \" +\n 'E.g. { \"whatsapp\": { \"enabled\": true }, \"macos\": { \"enabled\": false } }'\n ),\n routing: z\n .record(z.string(), z.unknown())\n .optional()\n .describe(\n \"For action=set: partial routing overrides as a JSON object. \" +\n 'E.g. { \"error\": [\"whatsapp\", \"macos\"], \"progress\": [\"cli\"] }'\n ),\n event: z\n .enum([\"error\", \"progress\", \"completion\", \"info\", \"debug\"])\n .optional()\n .describe(\"For action=send: event type. Default: 'info'.\"),\n message: z\n .string()\n .optional()\n .describe(\"For action=send: the notification message body.\"),\n title: z\n .string()\n .optional()\n .describe(\"For action=send: optional notification title (used by macOS and ntfy).\"),\n },\n async (args) => {\n const result = await toolNotificationConfig(args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: topic_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"topic_detect\",\n [\n \"Detect whether recent conversation context has shifted to a different project.\",\n \"\",\n \"Call this when the conversation may have drifted away from the initially-routed project.\",\n \"Provide a short summary of the recent context (last few messages or tool call results).\",\n \"\",\n \"Returns:\",\n \" shifted — true if a topic shift was detected\",\n \" current_project — the project the session is currently routed to\",\n \" suggested_project — the project that best matches the context\",\n \" confidence — [0,1] fraction of memory mass held by suggested_project\",\n \" chunks_scored — number of memory chunks that contributed to scoring\",\n \" top_matches — top-3 projects with their confidence percentages\",\n \"\",\n \"A shift is reported when confidence >= threshold (default 0.6) and the\",\n \"best-matching project differs from current_project.\",\n \"\",\n \"Use cases:\",\n \" - Call at session start to confirm routing is correct\",\n \" - Call periodically when working across multiple concerns\",\n \" - Integrate with pre-tool hooks for automatic drift detection\",\n ].join(\"\\n\"),\n {\n context: z\n .string()\n .describe(\n \"Recent conversation context: a few sentences summarising what the session has been discussing. \" +\n \"Can include file paths, feature names, commands run, or any relevant text.\"\n ),\n current_project: z\n .string()\n .optional()\n .describe(\n \"The project slug this session is currently routed to. \" +\n \"If omitted, the tool still returns the best-matching project but shifted will always be false.\"\n ),\n threshold: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .describe(\n \"Minimum confidence [0,1] to declare a shift. Default: 0.6. \" +\n \"Increase to reduce false positives. Decrease to catch subtle drifts.\"\n ),\n },\n async (args) => {\n const result = await toolTopicDetect(args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_route\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_route\",\n [\n \"Automatically detect which project this session belongs to.\",\n \"\",\n \"Call this at session start (e.g., from CLAUDE.md or a session-start hook)\",\n \"to route the session to the correct project automatically.\",\n \"\",\n \"Detection strategy (in priority order):\",\n \" 1. path — exact or parent-directory match in the project registry\",\n \" 2. marker — walk up from cwd looking for Notes/PAI.md marker files\",\n \" 3. topic — BM25 keyword search against memory (only if context provided)\",\n \"\",\n \"Returns:\",\n \" slug — the matched project slug\",\n \" display_name — human-readable project name\",\n \" root_path — absolute path to the project root\",\n \" method — how it was detected: 'path', 'marker', or 'topic'\",\n \" confidence — 1.0 for path/marker matches, BM25 fraction for topic\",\n \"\",\n \"If no match is found, returns a message explaining what was tried.\",\n \"Run 'pai project add .' to register the current directory.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Working directory to detect from. Defaults to process.cwd(). \" +\n \"Pass the session's actual working directory for accurate detection.\"\n ),\n context: z\n .string()\n .optional()\n .describe(\n \"Optional conversation context for topic-based fallback routing. \" +\n \"A few sentences summarising what the session will work on. \" +\n \"Only used if path and marker detection both fail.\"\n ),\n },\n async (args) => {\n const result = await toolSessionRoute(getRegistryDb(), getFederationDb(), args);\n return {\n content: result.content.map((c) => ({ type: c.type as \"text\", text: c.text })),\n isError: result.isError,\n };\n }\n );\n\n // -------------------------------------------------------------------------\n // 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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"}
|