@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,151 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/notifications/types.ts
|
|
7
|
+
const DEFAULT_ROUTING = {
|
|
8
|
+
error: [
|
|
9
|
+
"whatsapp",
|
|
10
|
+
"macos",
|
|
11
|
+
"ntfy",
|
|
12
|
+
"cli"
|
|
13
|
+
],
|
|
14
|
+
completion: [
|
|
15
|
+
"whatsapp",
|
|
16
|
+
"macos",
|
|
17
|
+
"ntfy",
|
|
18
|
+
"cli"
|
|
19
|
+
],
|
|
20
|
+
info: ["cli"],
|
|
21
|
+
progress: ["cli"],
|
|
22
|
+
debug: []
|
|
23
|
+
};
|
|
24
|
+
const DEFAULT_CHANNELS = {
|
|
25
|
+
ntfy: {
|
|
26
|
+
enabled: false,
|
|
27
|
+
url: void 0,
|
|
28
|
+
priority: "default"
|
|
29
|
+
},
|
|
30
|
+
whatsapp: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
recipient: void 0
|
|
33
|
+
},
|
|
34
|
+
macos: { enabled: true },
|
|
35
|
+
voice: {
|
|
36
|
+
enabled: false,
|
|
37
|
+
voiceName: "bm_george"
|
|
38
|
+
},
|
|
39
|
+
cli: { enabled: true }
|
|
40
|
+
};
|
|
41
|
+
const DEFAULT_NOTIFICATION_CONFIG = {
|
|
42
|
+
mode: "auto",
|
|
43
|
+
channels: DEFAULT_CHANNELS,
|
|
44
|
+
routing: DEFAULT_ROUTING
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/daemon/config.ts
|
|
49
|
+
/**
|
|
50
|
+
* config.ts — Configuration loader for PAI Daemon
|
|
51
|
+
*
|
|
52
|
+
* Loads config from ~/.config/pai/config.json (XDG convention).
|
|
53
|
+
* Deep-merges with defaults so partial configs work fine.
|
|
54
|
+
* Expands ~ in path values at runtime.
|
|
55
|
+
*/
|
|
56
|
+
var config_exports = /* @__PURE__ */ __exportAll({
|
|
57
|
+
CONFIG_DIR: () => CONFIG_DIR,
|
|
58
|
+
CONFIG_FILE: () => CONFIG_FILE,
|
|
59
|
+
DEFAULTS: () => DEFAULTS,
|
|
60
|
+
ensureConfigDir: () => ensureConfigDir,
|
|
61
|
+
expandHome: () => expandHome,
|
|
62
|
+
loadConfig: () => loadConfig
|
|
63
|
+
});
|
|
64
|
+
const DEFAULTS = {
|
|
65
|
+
socketPath: "/tmp/pai.sock",
|
|
66
|
+
indexIntervalSecs: 300,
|
|
67
|
+
embedIntervalSecs: 600,
|
|
68
|
+
storageBackend: "sqlite",
|
|
69
|
+
postgres: {
|
|
70
|
+
connectionString: "postgresql://pai:pai@localhost:5432/pai",
|
|
71
|
+
maxConnections: 5,
|
|
72
|
+
connectionTimeoutMs: 5e3
|
|
73
|
+
},
|
|
74
|
+
embeddingModel: "Snowflake/snowflake-arctic-embed-m-v1.5",
|
|
75
|
+
logLevel: "info",
|
|
76
|
+
notifications: DEFAULT_NOTIFICATION_CONFIG
|
|
77
|
+
};
|
|
78
|
+
const CONFIG_TEMPLATE = `{
|
|
79
|
+
"socketPath": "/tmp/pai.sock",
|
|
80
|
+
"indexIntervalSecs": 300,
|
|
81
|
+
"embedIntervalSecs": 600,
|
|
82
|
+
"storageBackend": "sqlite",
|
|
83
|
+
"postgres": {
|
|
84
|
+
"connectionString": "postgresql://pai:pai@localhost:5432/pai",
|
|
85
|
+
"maxConnections": 5,
|
|
86
|
+
"connectionTimeoutMs": 5000
|
|
87
|
+
},
|
|
88
|
+
"embeddingModel": "Snowflake/snowflake-arctic-embed-m-v1.5",
|
|
89
|
+
"logLevel": "info"
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
/** Expand a leading ~ to the real home directory */
|
|
93
|
+
function expandHome(p) {
|
|
94
|
+
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(1));
|
|
95
|
+
return p;
|
|
96
|
+
}
|
|
97
|
+
const CONFIG_DIR = join(homedir(), ".config", "pai");
|
|
98
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
99
|
+
function deepMerge(target, source) {
|
|
100
|
+
const result = { ...target };
|
|
101
|
+
for (const key of Object.keys(source)) {
|
|
102
|
+
const srcVal = source[key];
|
|
103
|
+
if (srcVal === void 0 || srcVal === null) continue;
|
|
104
|
+
const tgtVal = target[key];
|
|
105
|
+
if (typeof srcVal === "object" && !Array.isArray(srcVal) && typeof tgtVal === "object" && tgtVal !== null && !Array.isArray(tgtVal)) result[key] = deepMerge(tgtVal, srcVal);
|
|
106
|
+
else result[key] = srcVal;
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Load configuration from ~/.config/pai/config.json.
|
|
112
|
+
* Returns defaults merged with any values found in the file.
|
|
113
|
+
*/
|
|
114
|
+
function loadConfig() {
|
|
115
|
+
if (!existsSync(CONFIG_FILE)) return { ...DEFAULTS };
|
|
116
|
+
let raw;
|
|
117
|
+
try {
|
|
118
|
+
raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
119
|
+
} catch (e) {
|
|
120
|
+
process.stderr.write(`[pai-daemon] Could not read config file at ${CONFIG_FILE}: ${e}\n`);
|
|
121
|
+
return { ...DEFAULTS };
|
|
122
|
+
}
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(raw);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
process.stderr.write(`[pai-daemon] Config file is not valid JSON: ${e}\n`);
|
|
128
|
+
return { ...DEFAULTS };
|
|
129
|
+
}
|
|
130
|
+
return deepMerge(DEFAULTS, parsed);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Ensure ~/.config/pai/ exists and write a default config.json template
|
|
134
|
+
* if none exists yet. Call this only from the `serve` command.
|
|
135
|
+
*/
|
|
136
|
+
function ensureConfigDir() {
|
|
137
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
138
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
139
|
+
process.stderr.write(`[pai-daemon] Created config directory: ${CONFIG_DIR}\n`);
|
|
140
|
+
}
|
|
141
|
+
if (!existsSync(CONFIG_FILE)) try {
|
|
142
|
+
writeFileSync(CONFIG_FILE, CONFIG_TEMPLATE, "utf-8");
|
|
143
|
+
process.stderr.write(`[pai-daemon] Wrote default config to: ${CONFIG_FILE}\n`);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
process.stderr.write(`[pai-daemon] Could not write default config: ${e}\n`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//#endregion
|
|
150
|
+
export { expandHome as a, ensureConfigDir as i, CONFIG_FILE as n, loadConfig as o, config_exports as r, DEFAULT_NOTIFICATION_CONFIG as s, CONFIG_DIR as t };
|
|
151
|
+
//# sourceMappingURL=config-DBh1bYM2.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-DBh1bYM2.mjs","names":[],"sources":["../src/notifications/types.ts","../src/daemon/config.ts"],"sourcesContent":["/**\n * types.ts — Unified Notification Framework type definitions\n *\n * Defines the channel registry, event routing, and configuration schema\n * for PAI's notification subsystem.\n */\n\n// ---------------------------------------------------------------------------\n// Channel identifiers\n// ---------------------------------------------------------------------------\n\nexport type ChannelId = \"ntfy\" | \"whatsapp\" | \"macos\" | \"voice\" | \"cli\";\n\n// ---------------------------------------------------------------------------\n// Notification event types\n// ---------------------------------------------------------------------------\n\n/**\n * The semantic type of a notification event.\n * Used to route events to the appropriate channels.\n */\nexport type NotificationEvent =\n | \"error\"\n | \"progress\"\n | \"completion\"\n | \"info\"\n | \"debug\";\n\n// ---------------------------------------------------------------------------\n// Notification mode\n// ---------------------------------------------------------------------------\n\n/**\n * The current notification mode.\n *\n * - \"auto\" — Use the per-event routing table (default)\n * - \"voice\" — All events go to voice (WhatsApp TTS)\n * - \"whatsapp\" — All events go to WhatsApp text\n * - \"ntfy\" — All events go to ntfy.sh\n * - \"macos\" — All events go to macOS notifications\n * - \"cli\" — All events go to CLI stdout only\n * - \"off\" — Suppress all notifications\n */\nexport type NotificationMode =\n | \"auto\"\n | \"voice\"\n | \"whatsapp\"\n | \"ntfy\"\n | \"macos\"\n | \"cli\"\n | \"off\";\n\n// ---------------------------------------------------------------------------\n// Per-channel configuration\n// ---------------------------------------------------------------------------\n\nexport interface NtfyChannelConfig {\n enabled: boolean;\n /** ntfy.sh topic URL, e.g. \"https://ntfy.sh/my-topic\" */\n url?: string;\n /** ntfy priority: min | low | default | high | urgent */\n priority?: \"min\" | \"low\" | \"default\" | \"high\" | \"urgent\";\n}\n\nexport interface WhatsAppChannelConfig {\n enabled: boolean;\n /** Optional recipient (phone, JID, or contact name). Omit for self-chat. */\n recipient?: string;\n}\n\nexport interface MacOsChannelConfig {\n enabled: boolean;\n}\n\nexport interface VoiceChannelConfig {\n enabled: boolean;\n /** Kokoro voice name, e.g. \"bm_george\", \"af_bella\". Default: \"bm_george\" */\n voiceName?: string;\n}\n\nexport interface CliChannelConfig {\n enabled: boolean;\n}\n\nexport interface ChannelConfigs {\n ntfy: NtfyChannelConfig;\n whatsapp: WhatsAppChannelConfig;\n macos: MacOsChannelConfig;\n voice: VoiceChannelConfig;\n cli: CliChannelConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Routing table\n// ---------------------------------------------------------------------------\n\n/**\n * Maps each event type to the ordered list of channels that should receive it.\n * Only channels that are enabled in `channels` and present in this list are used.\n */\nexport type RoutingTable = {\n [K in NotificationEvent]: ChannelId[];\n};\n\nexport const DEFAULT_ROUTING: RoutingTable = {\n error: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n completion: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n info: [\"cli\"],\n progress: [\"cli\"],\n debug: [],\n};\n\n// ---------------------------------------------------------------------------\n// Top-level notification config (embedded in PaiDaemonConfig)\n// ---------------------------------------------------------------------------\n\nexport interface NotificationConfig {\n /** Current routing mode. Default: \"auto\" */\n mode: NotificationMode;\n /** Per-channel configuration */\n channels: ChannelConfigs;\n /** Event → channel routing (used in \"auto\" mode) */\n routing: RoutingTable;\n}\n\nexport const DEFAULT_CHANNELS: ChannelConfigs = {\n ntfy: {\n enabled: false,\n url: undefined,\n priority: \"default\",\n },\n whatsapp: {\n enabled: true,\n recipient: undefined,\n },\n macos: {\n enabled: true,\n },\n voice: {\n enabled: false,\n voiceName: \"bm_george\",\n },\n cli: {\n enabled: true,\n },\n};\n\nexport const DEFAULT_NOTIFICATION_CONFIG: NotificationConfig = {\n mode: \"auto\",\n channels: DEFAULT_CHANNELS,\n routing: DEFAULT_ROUTING,\n};\n\n// ---------------------------------------------------------------------------\n// Notification payload\n// ---------------------------------------------------------------------------\n\nexport interface NotificationPayload {\n /** Semantic event type — used for routing */\n event: NotificationEvent;\n /** The notification message body */\n message: string;\n /** Optional title (used by macOS, ntfy) */\n title?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider interface\n// ---------------------------------------------------------------------------\n\nexport interface NotificationProvider {\n readonly channelId: ChannelId;\n /**\n * Send a notification.\n * Returns true on success, false on failure (failure is non-fatal).\n */\n send(payload: NotificationPayload, config: NotificationConfig): Promise<boolean>;\n}\n\n// ---------------------------------------------------------------------------\n// Send result\n// ---------------------------------------------------------------------------\n\nexport interface SendResult {\n channelsAttempted: ChannelId[];\n channelsSucceeded: ChannelId[];\n channelsFailed: ChannelId[];\n mode: NotificationMode;\n}\n","/**\n * config.ts — Configuration loader for PAI Daemon\n *\n * Loads config from ~/.config/pai/config.json (XDG convention).\n * Deep-merges with defaults so partial configs work fine.\n * Expands ~ in path values at runtime.\n */\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { NotificationConfig } from \"../notifications/types.js\";\nimport { DEFAULT_NOTIFICATION_CONFIG } from \"../notifications/types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PostgresConfig {\n /** Connection string — if set, overrides individual host/port/etc. fields */\n connectionString?: string;\n /** Postgres host (default: \"localhost\") */\n host?: string;\n /** Postgres port (default: 5432) */\n port?: number;\n /** Postgres database name (default: \"pai\") */\n database?: string;\n /** Postgres user (default: \"pai\") */\n user?: string;\n /** Postgres password (default: \"pai\") */\n password?: string;\n /** Maximum pool connections (default: 5) */\n maxConnections?: number;\n /** Connection timeout in ms (default: 5000) */\n connectionTimeoutMs?: number;\n}\n\nexport interface PaiDaemonConfig {\n /** Unix Domain Socket path for IPC */\n socketPath: string;\n\n /** Index schedule interval in seconds (default: 300 = 5 minutes) */\n indexIntervalSecs: number;\n\n /** Embedding schedule interval in seconds (default: 600 = 10 minutes) */\n embedIntervalSecs: number;\n\n /** Storage backend: \"sqlite\" (default) or \"postgres\" */\n storageBackend: \"sqlite\" | \"postgres\";\n\n /** PostgreSQL connection config (used when storageBackend = \"postgres\") */\n postgres?: PostgresConfig;\n\n /** Embedding model name (used for semantic/hybrid search) */\n embeddingModel: string;\n\n /** Log level */\n logLevel: \"debug\" | \"info\" | \"warn\" | \"error\";\n\n /** Notification subsystem configuration */\n notifications: NotificationConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nexport const DEFAULTS: PaiDaemonConfig = {\n socketPath: \"/tmp/pai.sock\",\n indexIntervalSecs: 300,\n embedIntervalSecs: 600,\n storageBackend: \"sqlite\",\n postgres: {\n connectionString: \"postgresql://pai:pai@localhost:5432/pai\",\n maxConnections: 5,\n connectionTimeoutMs: 5000,\n },\n embeddingModel: \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n logLevel: \"info\",\n notifications: DEFAULT_NOTIFICATION_CONFIG,\n};\n\nconst CONFIG_TEMPLATE = `{\n \"socketPath\": \"/tmp/pai.sock\",\n \"indexIntervalSecs\": 300,\n \"embedIntervalSecs\": 600,\n \"storageBackend\": \"sqlite\",\n \"postgres\": {\n \"connectionString\": \"postgresql://pai:pai@localhost:5432/pai\",\n \"maxConnections\": 5,\n \"connectionTimeoutMs\": 5000\n },\n \"embeddingModel\": \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n \"logLevel\": \"info\"\n}\n`;\n\n// ---------------------------------------------------------------------------\n// Path helpers\n// ---------------------------------------------------------------------------\n\n/** Expand a leading ~ to the real home directory */\nexport function expandHome(p: string): string {\n if (p === \"~\" || p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) {\n return join(homedir(), p.slice(1));\n }\n return p;\n}\n\nexport const CONFIG_DIR = join(homedir(), \".config\", \"pai\");\nexport const CONFIG_FILE = join(CONFIG_DIR, \"config.json\");\n\n// ---------------------------------------------------------------------------\n// Deep merge (handles nested objects, not arrays)\n// ---------------------------------------------------------------------------\n\nfunction deepMerge<T extends object>(\n target: T,\n source: Record<string, unknown>\n): T {\n const result = { ...target };\n for (const key of Object.keys(source)) {\n const srcVal = source[key];\n if (srcVal === undefined || srcVal === null) continue;\n const tgtVal = (target as Record<string, unknown>)[key];\n if (\n typeof srcVal === \"object\" &&\n !Array.isArray(srcVal) &&\n typeof tgtVal === \"object\" &&\n tgtVal !== null &&\n !Array.isArray(tgtVal)\n ) {\n (result as Record<string, unknown>)[key] = deepMerge(\n tgtVal as object,\n srcVal as Record<string, unknown>\n );\n } else {\n (result as Record<string, unknown>)[key] = srcVal;\n }\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Config loader\n// ---------------------------------------------------------------------------\n\n/**\n * Load configuration from ~/.config/pai/config.json.\n * Returns defaults merged with any values found in the file.\n */\nexport function loadConfig(): PaiDaemonConfig {\n if (!existsSync(CONFIG_FILE)) {\n return { ...DEFAULTS };\n }\n\n let raw: string;\n try {\n raw = readFileSync(CONFIG_FILE, \"utf-8\");\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not read config file at ${CONFIG_FILE}: ${e}\\n`\n );\n return { ...DEFAULTS };\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Config file is not valid JSON: ${e}\\n`\n );\n return { ...DEFAULTS };\n }\n\n return deepMerge(DEFAULTS, parsed);\n}\n\n/**\n * Ensure ~/.config/pai/ exists and write a default config.json template\n * if none exists yet. Call this only from the `serve` command.\n */\nexport function ensureConfigDir(): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n process.stderr.write(\n `[pai-daemon] Created config directory: ${CONFIG_DIR}\\n`\n );\n }\n\n if (!existsSync(CONFIG_FILE)) {\n try {\n writeFileSync(CONFIG_FILE, CONFIG_TEMPLATE, \"utf-8\");\n process.stderr.write(\n `[pai-daemon] Wrote default config to: ${CONFIG_FILE}\\n`\n );\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not write default config: ${e}\\n`\n );\n }\n }\n}\n"],"mappings":";;;;;;AAwGA,MAAa,kBAAgC;CAC3C,OAAY;EAAC;EAAY;EAAS;EAAQ;EAAM;CAChD,YAAY;EAAC;EAAY;EAAS;EAAQ;EAAM;CAChD,MAAY,CAAC,MAAM;CACnB,UAAY,CAAC,MAAM;CACnB,OAAY,EAAE;CACf;AAeD,MAAa,mBAAmC;CAC9C,MAAM;EACJ,SAAS;EACT,KAAK;EACL,UAAU;EACX;CACD,UAAU;EACR,SAAS;EACT,WAAW;EACZ;CACD,OAAO,EACL,SAAS,MACV;CACD,OAAO;EACL,SAAS;EACT,WAAW;EACZ;CACD,KAAK,EACH,SAAS,MACV;CACF;AAED,MAAa,8BAAkD;CAC7D,MAAM;CACN,UAAU;CACV,SAAS;CACV;;;;;;;;;;;;;;;;;;;ACpFD,MAAa,WAA4B;CACvC,YAAY;CACZ,mBAAmB;CACnB,mBAAmB;CACnB,gBAAgB;CAChB,UAAU;EACR,kBAAkB;EAClB,gBAAgB;EAChB,qBAAqB;EACtB;CACD,gBAAgB;CAChB,UAAU;CACV,eAAe;CAChB;AAED,MAAM,kBAAkB;;;;;;;;;;;;;;;AAoBxB,SAAgB,WAAW,GAAmB;AAC5C,KAAI,MAAM,OAAO,EAAE,WAAW,KAAK,IAAI,EAAE,WAAW,MAAM,CACxD,QAAO,KAAK,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC;AAEpC,QAAO;;AAGT,MAAa,aAAa,KAAK,SAAS,EAAE,WAAW,MAAM;AAC3D,MAAa,cAAc,KAAK,YAAY,cAAc;AAM1D,SAAS,UACP,QACA,QACG;CACH,MAAM,SAAS,EAAE,GAAG,QAAQ;AAC5B,MAAK,MAAM,OAAO,OAAO,KAAK,OAAO,EAAE;EACrC,MAAM,SAAS,OAAO;AACtB,MAAI,WAAW,UAAa,WAAW,KAAM;EAC7C,MAAM,SAAU,OAAmC;AACnD,MACE,OAAO,WAAW,YAClB,CAAC,MAAM,QAAQ,OAAO,IACtB,OAAO,WAAW,YAClB,WAAW,QACX,CAAC,MAAM,QAAQ,OAAO,CAEtB,CAAC,OAAmC,OAAO,UACzC,QACA,OACD;MAED,CAAC,OAAmC,OAAO;;AAG/C,QAAO;;;;;;AAWT,SAAgB,aAA8B;AAC5C,KAAI,CAAC,WAAW,YAAY,CAC1B,QAAO,EAAE,GAAG,UAAU;CAGxB,IAAI;AACJ,KAAI;AACF,QAAM,aAAa,aAAa,QAAQ;UACjC,GAAG;AACV,UAAQ,OAAO,MACb,8CAA8C,YAAY,IAAI,EAAE,IACjE;AACD,SAAO,EAAE,GAAG,UAAU;;CAGxB,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;UACjB,GAAG;AACV,UAAQ,OAAO,MACb,+CAA+C,EAAE,IAClD;AACD,SAAO,EAAE,GAAG,UAAU;;AAGxB,QAAO,UAAU,UAAU,OAAO;;;;;;AAOpC,SAAgB,kBAAwB;AACtC,KAAI,CAAC,WAAW,WAAW,EAAE;AAC3B,YAAU,YAAY,EAAE,WAAW,MAAM,CAAC;AAC1C,UAAQ,OAAO,MACb,0CAA0C,WAAW,IACtD;;AAGH,KAAI,CAAC,WAAW,YAAY,CAC1B,KAAI;AACF,gBAAc,aAAa,iBAAiB,QAAQ;AACpD,UAAQ,OAAO,MACb,yCAAyC,YAAY,IACtD;UACM,GAAG;AACV,UAAQ,OAAO,MACb,gDAAgD,EAAE,IACnD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../db-4lSqLFb8.mjs";
|
|
3
|
+
import "../indexer-B20bPHL-.mjs";
|
|
4
|
+
import "../embeddings-mfqv-jFu.mjs";
|
|
5
|
+
import "../search-PjftDxxs.mjs";
|
|
6
|
+
import "../tools-CLK4080-.mjs";
|
|
7
|
+
import { t as PaiClient } from "../ipc-client-DPy7s3iu.mjs";
|
|
8
|
+
import { i as ensureConfigDir, o as loadConfig } from "../config-DBh1bYM2.mjs";
|
|
9
|
+
import "../factory-BDAiKtYR.mjs";
|
|
10
|
+
import "../detector-DKA83aTZ.mjs";
|
|
11
|
+
import { n as serve } from "../daemon-v5O897D4.mjs";
|
|
12
|
+
import { Command } from "commander";
|
|
13
|
+
|
|
14
|
+
//#region src/daemon/index.ts
|
|
15
|
+
/**
|
|
16
|
+
* PAI Daemon — Entry point
|
|
17
|
+
*
|
|
18
|
+
* Commands:
|
|
19
|
+
* serve — Start the PAI daemon (foreground, managed by launchd in production)
|
|
20
|
+
* status — Query daemon status via IPC
|
|
21
|
+
* index — Trigger an immediate index run via IPC
|
|
22
|
+
*/
|
|
23
|
+
const program = new Command();
|
|
24
|
+
program.name("pai-daemon").description("PAI Daemon — background service for PAI Knowledge OS").version("0.1.0");
|
|
25
|
+
program.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
|
|
26
|
+
ensureConfigDir();
|
|
27
|
+
await serve(loadConfig());
|
|
28
|
+
});
|
|
29
|
+
program.command("status").description("Query the running daemon status").action(async () => {
|
|
30
|
+
const client = new PaiClient(loadConfig().socketPath);
|
|
31
|
+
try {
|
|
32
|
+
const status = await client.status();
|
|
33
|
+
console.log(JSON.stringify(status, null, 2));
|
|
34
|
+
} catch (e) {
|
|
35
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
36
|
+
console.error(`Error: ${msg}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
program.command("index").description("Trigger an immediate index run in the running daemon").action(async () => {
|
|
41
|
+
const client = new PaiClient(loadConfig().socketPath);
|
|
42
|
+
try {
|
|
43
|
+
await client.triggerIndex();
|
|
44
|
+
console.log("Index triggered. Check daemon logs for progress.");
|
|
45
|
+
} catch (e) {
|
|
46
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
47
|
+
console.error(`Error: ${msg}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
program.parse(process.argv);
|
|
52
|
+
if (process.argv.length <= 2) program.help();
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { };
|
|
56
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/daemon/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PAI Daemon — Entry point\n *\n * Commands:\n * serve — Start the PAI daemon (foreground, managed by launchd in production)\n * status — Query daemon status via IPC\n * index — Trigger an immediate index run via IPC\n */\n\nimport { Command } from \"commander\";\nimport { loadConfig, ensureConfigDir } from \"./config.js\";\nimport { serve } from \"./daemon.js\";\nimport { PaiClient } from \"./ipc-client.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"pai-daemon\")\n .description(\"PAI Daemon — background service for PAI Knowledge OS\")\n .version(\"0.1.0\");\n\n// ---------------------------------------------------------------------------\n// serve\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"serve\")\n .description(\"Start the PAI daemon in the foreground\")\n .action(async () => {\n ensureConfigDir();\n const config = loadConfig();\n await serve(config);\n });\n\n// ---------------------------------------------------------------------------\n// status\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"status\")\n .description(\"Query the running daemon status\")\n .action(async () => {\n const config = loadConfig();\n const client = new PaiClient(config.socketPath);\n\n try {\n const status = await client.status();\n console.log(JSON.stringify(status, null, 2));\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(`Error: ${msg}`);\n process.exit(1);\n }\n });\n\n// ---------------------------------------------------------------------------\n// index\n// ---------------------------------------------------------------------------\n\nprogram\n .command(\"index\")\n .description(\"Trigger an immediate index run in the running daemon\")\n .action(async () => {\n const config = loadConfig();\n const client = new PaiClient(config.socketPath);\n\n try {\n await client.triggerIndex();\n console.log(\"Index triggered. Check daemon logs for progress.\");\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n console.error(`Error: ${msg}`);\n process.exit(1);\n }\n });\n\n// ---------------------------------------------------------------------------\n// Parse\n// ---------------------------------------------------------------------------\n\nprogram.parse(process.argv);\n\nif (process.argv.length <= 2) {\n program.help();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAeA,MAAM,UAAU,IAAI,SAAS;AAE7B,QACG,KAAK,aAAa,CAClB,YAAY,uDAAuD,CACnE,QAAQ,QAAQ;AAMnB,QACG,QAAQ,QAAQ,CAChB,YAAY,yCAAyC,CACrD,OAAO,YAAY;AAClB,kBAAiB;AAEjB,OAAM,MADS,YAAY,CACR;EACnB;AAMJ,QACG,QAAQ,SAAS,CACjB,YAAY,kCAAkC,CAC9C,OAAO,YAAY;CAElB,MAAM,SAAS,IAAI,UADJ,YAAY,CACS,WAAW;AAE/C,KAAI;EACF,MAAM,SAAS,MAAM,OAAO,QAAQ;AACpC,UAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;UACrC,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,MAAM,UAAU,MAAM;AAC9B,UAAQ,KAAK,EAAE;;EAEjB;AAMJ,QACG,QAAQ,QAAQ,CAChB,YAAY,uDAAuD,CACnE,OAAO,YAAY;CAElB,MAAM,SAAS,IAAI,UADJ,YAAY,CACS,WAAW;AAE/C,KAAI;AACF,QAAM,OAAO,cAAc;AAC3B,UAAQ,IAAI,mDAAmD;UACxD,GAAG;EACV,MAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AACtD,UAAQ,MAAM,UAAU,MAAM;AAC9B,UAAQ,KAAK,EAAE;;EAEjB;AAMJ,QAAQ,MAAM,QAAQ,KAAK;AAE3B,IAAI,QAAQ,KAAK,UAAU,EACzB,SAAQ,MAAM"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { i as number, n as array, o as string, r as boolean, t as _enum } from "../schemas-DjdwzIQ8.mjs";
|
|
3
|
+
import { t as PaiClient } from "../ipc-client-DPy7s3iu.mjs";
|
|
4
|
+
import { o as loadConfig } from "../config-DBh1bYM2.mjs";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
|
|
8
|
+
//#region src/daemon-mcp/index.ts
|
|
9
|
+
/**
|
|
10
|
+
* PAI Daemon MCP Shim
|
|
11
|
+
*
|
|
12
|
+
* A thin MCP server that proxies all PAI tool calls to the PAI daemon via IPC.
|
|
13
|
+
* One shim instance runs per Claude Code session (spawned by Claude Code's MCP
|
|
14
|
+
* mechanism). All shims share the single daemon process, which holds the
|
|
15
|
+
* database connections and embedding model singleton.
|
|
16
|
+
*
|
|
17
|
+
* Tool definitions are static (unlike Coogle which discovers tools dynamically).
|
|
18
|
+
* The 9 PAI tools are: memory_search, memory_get, project_info, project_list,
|
|
19
|
+
* session_list, registry_search, project_detect, project_health, project_todo.
|
|
20
|
+
*
|
|
21
|
+
* If the daemon is not running, tool calls return a helpful error message
|
|
22
|
+
* rather than crashing — this allows the legacy direct MCP (dist/mcp/index.mjs)
|
|
23
|
+
* to serve as fallback.
|
|
24
|
+
*/
|
|
25
|
+
let _client = null;
|
|
26
|
+
function getClient() {
|
|
27
|
+
if (!_client) _client = new PaiClient(loadConfig().socketPath);
|
|
28
|
+
return _client;
|
|
29
|
+
}
|
|
30
|
+
async function proxyTool(method, params) {
|
|
31
|
+
try {
|
|
32
|
+
const toolResult = await getClient().call(method, params);
|
|
33
|
+
return {
|
|
34
|
+
content: toolResult.content.map((c) => ({
|
|
35
|
+
type: "text",
|
|
36
|
+
text: c.text
|
|
37
|
+
})),
|
|
38
|
+
isError: toolResult.isError
|
|
39
|
+
};
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: `PAI daemon error: ${e instanceof Error ? e.message : String(e)}\n\nIs the daemon running? Start it with: pai daemon serve`
|
|
45
|
+
}],
|
|
46
|
+
isError: true
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function startShim() {
|
|
51
|
+
const server = new McpServer({
|
|
52
|
+
name: "pai",
|
|
53
|
+
version: "0.1.0"
|
|
54
|
+
});
|
|
55
|
+
server.tool("memory_search", [
|
|
56
|
+
"Search PAI federated memory using BM25 full-text ranking, semantic similarity, or a hybrid of both.",
|
|
57
|
+
"",
|
|
58
|
+
"Use this BEFORE answering questions about past work, decisions, dates, people,",
|
|
59
|
+
"preferences, project status, todos, technical choices, or anything that might",
|
|
60
|
+
"have been recorded in session notes or memory files.",
|
|
61
|
+
"",
|
|
62
|
+
"Modes:",
|
|
63
|
+
" keyword — BM25 full-text search (default, fast, no embeddings required)",
|
|
64
|
+
" semantic — Cosine similarity over vector embeddings (requires prior embed run)",
|
|
65
|
+
" hybrid — Normalized combination of BM25 + cosine (best quality)",
|
|
66
|
+
"",
|
|
67
|
+
"Returns ranked snippets with project slug, file path, line range, and score.",
|
|
68
|
+
"Higher score = more relevant."
|
|
69
|
+
].join("\n"), {
|
|
70
|
+
query: string().describe("Free-text search query. Multiple words are ORed together — any matching word returns a result, ranked by relevance."),
|
|
71
|
+
project: string().optional().describe("Scope search to a single project by slug. Omit to search all projects."),
|
|
72
|
+
all_projects: boolean().optional().describe("Explicitly search all projects (default behaviour when project is omitted)."),
|
|
73
|
+
sources: array(_enum(["memory", "notes"])).optional().describe("Restrict to specific source types: 'memory' or 'notes'."),
|
|
74
|
+
limit: number().int().min(1).max(100).optional().describe("Maximum results to return. Default: 10."),
|
|
75
|
+
mode: _enum([
|
|
76
|
+
"keyword",
|
|
77
|
+
"semantic",
|
|
78
|
+
"hybrid"
|
|
79
|
+
]).optional().describe("Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined).")
|
|
80
|
+
}, async (args) => proxyTool("memory_search", args));
|
|
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) => proxyTool("memory_get", args));
|
|
94
|
+
server.tool("project_info", [
|
|
95
|
+
"Get detailed information about a PAI registered project.",
|
|
96
|
+
"",
|
|
97
|
+
"Use this to look up a project's root path, type, status, tags, session count,",
|
|
98
|
+
"and last active date. If no slug is provided, attempts to detect the current",
|
|
99
|
+
"project from the caller's working directory."
|
|
100
|
+
].join("\n"), { slug: string().optional().describe("Project slug. Omit to auto-detect from the current working directory.") }, async (args) => proxyTool("project_info", args));
|
|
101
|
+
server.tool("project_list", [
|
|
102
|
+
"List registered PAI projects with optional filters.",
|
|
103
|
+
"",
|
|
104
|
+
"Use this to browse all known projects, find projects by status or tag,",
|
|
105
|
+
"or get a quick overview of the PAI registry."
|
|
106
|
+
].join("\n"), {
|
|
107
|
+
status: _enum([
|
|
108
|
+
"active",
|
|
109
|
+
"archived",
|
|
110
|
+
"migrating"
|
|
111
|
+
]).optional().describe("Filter by project status. Default: all statuses."),
|
|
112
|
+
tag: string().optional().describe("Filter by tag name (exact match)."),
|
|
113
|
+
limit: number().int().min(1).max(500).optional().describe("Maximum number of projects to return. Default: 50.")
|
|
114
|
+
}, async (args) => proxyTool("project_list", args));
|
|
115
|
+
server.tool("session_list", [
|
|
116
|
+
"List session notes for a PAI project.",
|
|
117
|
+
"",
|
|
118
|
+
"Use this to find what sessions exist for a project, see their dates and titles,",
|
|
119
|
+
"and identify specific session notes to read via memory_get."
|
|
120
|
+
].join("\n"), {
|
|
121
|
+
project: string().describe("Project slug to list sessions for."),
|
|
122
|
+
limit: number().int().min(1).max(500).optional().describe("Maximum sessions to return. Default: 10 (most recent first)."),
|
|
123
|
+
status: _enum([
|
|
124
|
+
"open",
|
|
125
|
+
"completed",
|
|
126
|
+
"compacted"
|
|
127
|
+
]).optional().describe("Filter by session status.")
|
|
128
|
+
}, async (args) => proxyTool("session_list", args));
|
|
129
|
+
server.tool("registry_search", [
|
|
130
|
+
"Search PAI project registry by slug, display name, or path.",
|
|
131
|
+
"",
|
|
132
|
+
"Use this to find the slug for a project when you know its name or path,",
|
|
133
|
+
"or to check if a project is registered. Returns matching project entries."
|
|
134
|
+
].join("\n"), { query: string().describe("Search term matched against project slugs, display names, and root paths (case-insensitive substring match).") }, async (args) => proxyTool("registry_search", args));
|
|
135
|
+
server.tool("project_detect", [
|
|
136
|
+
"Detect which registered PAI project a filesystem path belongs to.",
|
|
137
|
+
"",
|
|
138
|
+
"Use this at session start to auto-identify the current project from the",
|
|
139
|
+
"working directory, or to map any path back to its registered project.",
|
|
140
|
+
"",
|
|
141
|
+
"Returns: slug, display_name, root_path, type, status, match_type (exact|parent),",
|
|
142
|
+
"relative_path (if the given path is inside a project), and session stats."
|
|
143
|
+
].join("\n"), { cwd: string().optional().describe("Absolute path to detect project for. Defaults to the MCP server's process.cwd().") }, async (args) => proxyTool("project_detect", args));
|
|
144
|
+
server.tool("project_health", [
|
|
145
|
+
"Audit all registered PAI projects to find moved or deleted directories.",
|
|
146
|
+
"",
|
|
147
|
+
"Returns a JSON report categorising every project as:",
|
|
148
|
+
" active — root_path exists on disk",
|
|
149
|
+
" stale — root_path missing, but a directory with the same name was found nearby",
|
|
150
|
+
" dead — root_path missing, no candidate found",
|
|
151
|
+
"",
|
|
152
|
+
"Each active project entry also includes a 'todo' field indicating whether",
|
|
153
|
+
"a TODO.md was found and whether it has a ## Continue section."
|
|
154
|
+
].join("\n"), { category: _enum([
|
|
155
|
+
"active",
|
|
156
|
+
"stale",
|
|
157
|
+
"dead",
|
|
158
|
+
"all"
|
|
159
|
+
]).optional().describe("Filter results to a specific health category. Default: all.") }, async (args) => proxyTool("project_health", args));
|
|
160
|
+
server.tool("project_todo", [
|
|
161
|
+
"Read a project's TODO.md without needing to know the exact file path.",
|
|
162
|
+
"",
|
|
163
|
+
"Use this at session start or when resuming work to get the project's current",
|
|
164
|
+
"task list and continuation prompt. If a '## Continue' section is present,",
|
|
165
|
+
"it will be surfaced at the top of the response for quick context recovery.",
|
|
166
|
+
"",
|
|
167
|
+
"Searches these locations in order:",
|
|
168
|
+
" 1. <project_root>/Notes/TODO.md",
|
|
169
|
+
" 2. <project_root>/.claude/Notes/TODO.md",
|
|
170
|
+
" 3. <project_root>/tasks/todo.md",
|
|
171
|
+
" 4. <project_root>/TODO.md",
|
|
172
|
+
"",
|
|
173
|
+
"If no project slug is provided, auto-detects from the current working directory."
|
|
174
|
+
].join("\n"), { project: string().optional().describe("Project slug. Omit to auto-detect from the current working directory.") }, async (args) => proxyTool("project_todo", args));
|
|
175
|
+
const transport = new StdioServerTransport();
|
|
176
|
+
await server.connect(transport);
|
|
177
|
+
}
|
|
178
|
+
startShim().catch((e) => {
|
|
179
|
+
process.stderr.write(`PAI MCP shim fatal error: ${String(e)}\n`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
//#endregion
|
|
184
|
+
export { };
|
|
185
|
+
//# 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"],"sources":["../../src/daemon-mcp/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PAI Daemon MCP Shim\n *\n * A thin MCP server that proxies all PAI tool calls to the PAI daemon via IPC.\n * One shim instance runs per Claude Code session (spawned by Claude Code's MCP\n * mechanism). All shims share the single daemon process, which holds the\n * database connections and embedding model singleton.\n *\n * Tool definitions are static (unlike Coogle which discovers tools dynamically).\n * The 9 PAI tools are: memory_search, memory_get, project_info, project_list,\n * session_list, registry_search, project_detect, project_health, project_todo.\n *\n * If the daemon is not running, tool calls return a helpful error message\n * rather than crashing — this allows the legacy direct MCP (dist/mcp/index.mjs)\n * to serve as fallback.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { join, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { PaiClient } from \"../daemon/ipc-client.js\";\nimport { loadConfig } from \"../daemon/config.js\";\n\n// ---------------------------------------------------------------------------\n// IPC client singleton\n// ---------------------------------------------------------------------------\n\nlet _client: PaiClient | null = null;\n\nfunction getClient(): PaiClient {\n if (!_client) {\n const config = loadConfig();\n _client = new PaiClient(config.socketPath);\n }\n return _client;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: proxy a tool call to daemon, returning MCP-compatible content\n// ---------------------------------------------------------------------------\n\nasync function proxyTool(\n method: string,\n params: Record<string, unknown>\n): Promise<{ content: Array<{ type: \"text\"; text: string }>; isError?: boolean }> {\n try {\n const result = await getClient().call(method, params);\n // The daemon returns ToolResult objects (content + isError)\n const toolResult = result as {\n content: Array<{ type: string; text: string }>;\n isError?: boolean;\n };\n\n return {\n content: toolResult.content.map((c) => ({\n type: \"text\" as const,\n text: c.text,\n })),\n isError: toolResult.isError,\n };\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n return {\n content: [\n {\n type: \"text\" as const,\n text: `PAI daemon error: ${msg}\\n\\nIs the daemon running? Start it with: pai daemon serve`,\n },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// MCP server\n// ---------------------------------------------------------------------------\n\nasync function startShim(): Promise<void> {\n const server = new McpServer({\n name: \"pai\",\n version: \"0.1.0\",\n });\n\n // -------------------------------------------------------------------------\n // Tool: memory_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_search\",\n [\n \"Search PAI federated memory using BM25 full-text ranking, semantic similarity, or a hybrid of both.\",\n \"\",\n \"Use this BEFORE answering questions about past work, decisions, dates, people,\",\n \"preferences, project status, todos, technical choices, or anything that might\",\n \"have been recorded in session notes or memory files.\",\n \"\",\n \"Modes:\",\n \" keyword — BM25 full-text search (default, fast, no embeddings required)\",\n \" semantic — Cosine similarity over vector embeddings (requires prior embed run)\",\n \" hybrid — Normalized combination of BM25 + cosine (best quality)\",\n \"\",\n \"Returns ranked snippets with project slug, file path, line range, and score.\",\n \"Higher score = more relevant.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\"Free-text search query. Multiple words are ORed together — any matching word returns a result, ranked by relevance.\"),\n project: z\n .string()\n .optional()\n .describe(\n \"Scope search to a single project by slug. Omit to search all projects.\"\n ),\n all_projects: z\n .boolean()\n .optional()\n .describe(\n \"Explicitly search all projects (default behaviour when project is omitted).\"\n ),\n sources: z\n .array(z.enum([\"memory\", \"notes\"]))\n .optional()\n .describe(\"Restrict to specific source types: 'memory' or 'notes'.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Maximum results to return. Default: 10.\"),\n mode: z\n .enum([\"keyword\", \"semantic\", \"hybrid\"])\n .optional()\n .describe(\n \"Search mode: 'keyword' (BM25, default), 'semantic' (vector cosine), or 'hybrid' (both combined).\"\n ),\n },\n async (args) => proxyTool(\"memory_search\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: memory_get\n // -------------------------------------------------------------------------\n\n server.tool(\n \"memory_get\",\n [\n \"Read the content of a specific file from a registered PAI project.\",\n \"\",\n \"Use this to read a full memory file, session note, or document after finding\",\n \"it via memory_search. Optionally restrict to a line range.\",\n \"\",\n \"The path must be a relative path within the project root (no ../ traversal).\",\n ].join(\"\\n\"),\n {\n project: z\n .string()\n .describe(\"Project slug identifying which project's files to read from.\"),\n path: z\n .string()\n .describe(\n \"Relative path within the project root (e.g. 'Notes/0001 - 2026-01-01 - Example.md').\"\n ),\n from: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Starting line number (1-based, inclusive). Default: 1.\"),\n lines: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Number of lines to return. Default: entire file.\"),\n },\n async (args) => proxyTool(\"memory_get\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_info\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_info\",\n [\n \"Get detailed information about a PAI registered project.\",\n \"\",\n \"Use this to look up a project's root path, type, status, tags, session count,\",\n \"and last active date. If no slug is provided, attempts to detect the current\",\n \"project from the caller's working directory.\",\n ].join(\"\\n\"),\n {\n slug: z\n .string()\n .optional()\n .describe(\n \"Project slug. Omit to auto-detect from the current working directory.\"\n ),\n },\n async (args) => proxyTool(\"project_info\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_list\",\n [\n \"List registered PAI projects with optional filters.\",\n \"\",\n \"Use this to browse all known projects, find projects by status or tag,\",\n \"or get a quick overview of the PAI registry.\",\n ].join(\"\\n\"),\n {\n status: z\n .enum([\"active\", \"archived\", \"migrating\"])\n .optional()\n .describe(\"Filter by project status. Default: all statuses.\"),\n tag: z\n .string()\n .optional()\n .describe(\"Filter by tag name (exact match).\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum number of projects to return. Default: 50.\"),\n },\n async (args) => proxyTool(\"project_list\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: session_list\n // -------------------------------------------------------------------------\n\n server.tool(\n \"session_list\",\n [\n \"List session notes for a PAI project.\",\n \"\",\n \"Use this to find what sessions exist for a project, see their dates and titles,\",\n \"and identify specific session notes to read via memory_get.\",\n ].join(\"\\n\"),\n {\n project: z.string().describe(\"Project slug to list sessions for.\"),\n limit: z\n .number()\n .int()\n .min(1)\n .max(500)\n .optional()\n .describe(\"Maximum sessions to return. Default: 10 (most recent first).\"),\n status: z\n .enum([\"open\", \"completed\", \"compacted\"])\n .optional()\n .describe(\"Filter by session status.\"),\n },\n async (args) => proxyTool(\"session_list\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: registry_search\n // -------------------------------------------------------------------------\n\n server.tool(\n \"registry_search\",\n [\n \"Search PAI project registry by slug, display name, or path.\",\n \"\",\n \"Use this to find the slug for a project when you know its name or path,\",\n \"or to check if a project is registered. Returns matching project entries.\",\n ].join(\"\\n\"),\n {\n query: z\n .string()\n .describe(\n \"Search term matched against project slugs, display names, and root paths (case-insensitive substring match).\"\n ),\n },\n async (args) => proxyTool(\"registry_search\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_detect\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_detect\",\n [\n \"Detect which registered PAI project a filesystem path belongs to.\",\n \"\",\n \"Use this at session start to auto-identify the current project from the\",\n \"working directory, or to map any path back to its registered project.\",\n \"\",\n \"Returns: slug, display_name, root_path, type, status, match_type (exact|parent),\",\n \"relative_path (if the given path is inside a project), and session stats.\",\n ].join(\"\\n\"),\n {\n cwd: z\n .string()\n .optional()\n .describe(\n \"Absolute path to detect project for. Defaults to the MCP server's process.cwd().\"\n ),\n },\n async (args) => proxyTool(\"project_detect\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_health\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_health\",\n [\n \"Audit all registered PAI projects to find moved or deleted directories.\",\n \"\",\n \"Returns a JSON report categorising every project as:\",\n \" active — root_path exists on disk\",\n \" stale — root_path missing, but a directory with the same name was found nearby\",\n \" dead — root_path missing, no candidate found\",\n \"\",\n \"Each active project entry also includes a 'todo' field indicating whether\",\n \"a TODO.md was found and whether it has a ## Continue section.\",\n ].join(\"\\n\"),\n {\n category: z\n .enum([\"active\", \"stale\", \"dead\", \"all\"])\n .optional()\n .describe(\"Filter results to a specific health category. Default: all.\"),\n },\n async (args) => proxyTool(\"project_health\", args)\n );\n\n // -------------------------------------------------------------------------\n // Tool: project_todo\n // -------------------------------------------------------------------------\n\n server.tool(\n \"project_todo\",\n [\n \"Read a project's TODO.md without needing to know the exact file path.\",\n \"\",\n \"Use this at session start or when resuming work to get the project's current\",\n \"task list and continuation prompt. If a '## Continue' section is present,\",\n \"it will be surfaced at the top of the response for quick context recovery.\",\n \"\",\n \"Searches these locations in order:\",\n \" 1. <project_root>/Notes/TODO.md\",\n \" 2. <project_root>/.claude/Notes/TODO.md\",\n \" 3. <project_root>/tasks/todo.md\",\n \" 4. <project_root>/TODO.md\",\n \"\",\n \"If no project slug is provided, auto-detects from the current working directory.\",\n ].join(\"\\n\"),\n {\n project: z\n .string()\n .optional()\n .describe(\n \"Project slug. Omit to auto-detect from the current working directory.\"\n ),\n },\n async (args) => proxyTool(\"project_todo\", args)\n );\n\n // -------------------------------------------------------------------------\n // Connect transport and start serving\n // -------------------------------------------------------------------------\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nstartShim().catch((e) => {\n process.stderr.write(`PAI MCP shim fatal error: ${String(e)}\\n`);\n process.exit(1);\n});\n"],"mappings":"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"}
|