@tekmidian/pai 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/ARCHITECTURE.md +121 -0
  2. package/FEATURE.md +5 -0
  3. package/README.md +54 -0
  4. package/dist/cli/index.mjs +11 -11
  5. package/dist/daemon/index.mjs +3 -3
  6. package/dist/{daemon-nXyhvdzz.mjs → daemon-VIFoKc_z.mjs} +31 -6
  7. package/dist/daemon-VIFoKc_z.mjs.map +1 -0
  8. package/dist/daemon-mcp/index.mjs +51 -0
  9. package/dist/daemon-mcp/index.mjs.map +1 -1
  10. package/dist/{factory-Ygqe_bVZ.mjs → factory-e0k1HWuc.mjs} +2 -2
  11. package/dist/{factory-Ygqe_bVZ.mjs.map → factory-e0k1HWuc.mjs.map} +1 -1
  12. package/dist/hooks/load-project-context.mjs +276 -89
  13. package/dist/hooks/load-project-context.mjs.map +4 -4
  14. package/dist/hooks/stop-hook.mjs +152 -2
  15. package/dist/hooks/stop-hook.mjs.map +3 -3
  16. package/dist/{postgres-CKf-EDtS.mjs → postgres-DvEPooLO.mjs} +45 -10
  17. package/dist/postgres-DvEPooLO.mjs.map +1 -0
  18. package/dist/query-feedback-Dv43XKHM.mjs +76 -0
  19. package/dist/query-feedback-Dv43XKHM.mjs.map +1 -0
  20. package/dist/tools-C4SBZHga.mjs +1731 -0
  21. package/dist/tools-C4SBZHga.mjs.map +1 -0
  22. package/dist/{vault-indexer-Bi2cRmn7.mjs → vault-indexer-B-aJpRZC.mjs} +3 -2
  23. package/dist/{vault-indexer-Bi2cRmn7.mjs.map → vault-indexer-B-aJpRZC.mjs.map} +1 -1
  24. package/dist/{zettelkasten-cdajbnPr.mjs → zettelkasten-DhBKZQHF.mjs} +358 -3
  25. package/dist/zettelkasten-DhBKZQHF.mjs.map +1 -0
  26. package/package.json +1 -1
  27. package/src/hooks/ts/session-start/load-project-context.ts +36 -0
  28. package/src/hooks/ts/stop/stop-hook.ts +203 -1
  29. package/dist/daemon-nXyhvdzz.mjs.map +0 -1
  30. package/dist/postgres-CKf-EDtS.mjs.map +0 -1
  31. package/dist/tools-DcaJlYDN.mjs +0 -869
  32. package/dist/tools-DcaJlYDN.mjs.map +0 -1
  33. package/dist/zettelkasten-cdajbnPr.mjs.map +0 -1
@@ -1,869 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-95iHPtFO.mjs";
2
- import { i as searchMemoryHybrid, n as populateSlugs } from "./search-DC1qhkKn.mjs";
3
- import { r as formatDetectionJson, t as detectProject } from "./detect-CdaA48EI.mjs";
4
- import { existsSync, readFileSync, statSync } from "node:fs";
5
- import { isAbsolute, join, resolve } from "node:path";
6
-
7
- //#region src/mcp/tools/types.ts
8
- /**
9
- * Shared types and project-row helpers used across all MCP tool handler modules.
10
- */
11
- function lookupProjectId(registryDb, slug) {
12
- const bySlug = registryDb.prepare("SELECT id FROM projects WHERE slug = ?").get(slug);
13
- if (bySlug) return bySlug.id;
14
- const byAlias = registryDb.prepare("SELECT project_id FROM aliases WHERE alias = ?").get(slug);
15
- if (byAlias) return byAlias.project_id;
16
- return null;
17
- }
18
- function detectProjectFromPath(registryDb, fsPath) {
19
- const resolved = resolve(fsPath);
20
- const exact = registryDb.prepare("SELECT id, slug, display_name, root_path, type, status, created_at, updated_at FROM projects WHERE root_path = ?").get(resolved);
21
- if (exact) return exact;
22
- const all = registryDb.prepare("SELECT id, slug, display_name, root_path, type, status, created_at, updated_at FROM projects ORDER BY LENGTH(root_path) DESC").all();
23
- for (const project of all) if (resolved.startsWith(project.root_path + "/") || resolved === project.root_path) return project;
24
- return null;
25
- }
26
- function formatProject(registryDb, project) {
27
- const sessionCount = registryDb.prepare("SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?").get(project.id).n;
28
- const lastSession = registryDb.prepare("SELECT date FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1").get(project.id);
29
- const tags = registryDb.prepare(`SELECT t.name FROM tags t
30
- JOIN project_tags pt ON pt.tag_id = t.id
31
- WHERE pt.project_id = ?
32
- ORDER BY t.name`).all(project.id).map((r) => r.name);
33
- const aliases = registryDb.prepare("SELECT alias FROM aliases WHERE project_id = ? ORDER BY alias").all(project.id).map((r) => r.alias);
34
- const lines = [
35
- `slug: ${project.slug}`,
36
- `display_name: ${project.display_name}`,
37
- `root_path: ${project.root_path}`,
38
- `type: ${project.type}`,
39
- `status: ${project.status}`,
40
- `sessions: ${sessionCount}`
41
- ];
42
- if (lastSession) lines.push(`last_session: ${lastSession.date}`);
43
- if (tags.length) lines.push(`tags: ${tags.join(", ")}`);
44
- if (aliases.length) lines.push(`aliases: ${aliases.join(", ")}`);
45
- if (project.obsidian_link) lines.push(`obsidian_link: ${project.obsidian_link}`);
46
- if (project.archived_at) lines.push(`archived_at: ${new Date(project.archived_at).toISOString().slice(0, 10)}`);
47
- return lines.join("\n");
48
- }
49
-
50
- //#endregion
51
- //#region src/mcp/tools/memory.ts
52
- /**
53
- * MCP tool handlers: memory_search, memory_get
54
- */
55
- async function toolMemorySearch(registryDb, federation, params, searchDefaults) {
56
- try {
57
- const projectIds = params.project ? (() => {
58
- const id = lookupProjectId(registryDb, params.project);
59
- return id != null ? [id] : [];
60
- })() : void 0;
61
- if (params.project && (!projectIds || projectIds.length === 0)) return {
62
- content: [{
63
- type: "text",
64
- text: `Project not found: ${params.project}`
65
- }],
66
- isError: true
67
- };
68
- const mode = params.mode ?? searchDefaults?.mode ?? "keyword";
69
- const snippetLength = params.snippetLength ?? searchDefaults?.snippetLength ?? 200;
70
- const searchOpts = {
71
- projectIds,
72
- sources: params.sources,
73
- maxResults: params.limit ?? searchDefaults?.defaultLimit ?? 5
74
- };
75
- let results;
76
- const isBackend = (x) => "backendType" in x;
77
- if (isBackend(federation)) if (mode === "keyword") results = await federation.searchKeyword(params.query, searchOpts);
78
- else if (mode === "semantic" || mode === "hybrid") {
79
- const { generateEmbedding } = await import("./embeddings-DGRAPAYb.mjs").then((n) => n.i);
80
- const queryEmbedding = await generateEmbedding(params.query, true);
81
- if (mode === "semantic") results = await federation.searchSemantic(queryEmbedding, searchOpts);
82
- else {
83
- const [kwResults, semResults] = await Promise.all([federation.searchKeyword(params.query, {
84
- ...searchOpts,
85
- maxResults: 50
86
- }), federation.searchSemantic(queryEmbedding, {
87
- ...searchOpts,
88
- maxResults: 50
89
- })]);
90
- results = combineHybridResults(kwResults, semResults, searchOpts.maxResults ?? 10);
91
- }
92
- } else results = await federation.searchKeyword(params.query, searchOpts);
93
- else {
94
- const { searchMemory, searchMemorySemantic } = await import("./search-DC1qhkKn.mjs").then((n) => n.o);
95
- if (mode === "keyword") results = searchMemory(federation, params.query, searchOpts);
96
- else if (mode === "semantic" || mode === "hybrid") {
97
- const { generateEmbedding } = await import("./embeddings-DGRAPAYb.mjs").then((n) => n.i);
98
- const queryEmbedding = await generateEmbedding(params.query, true);
99
- if (mode === "semantic") results = searchMemorySemantic(federation, queryEmbedding, searchOpts);
100
- else results = searchMemoryHybrid(federation, params.query, queryEmbedding, searchOpts);
101
- } else results = searchMemory(federation, params.query, searchOpts);
102
- }
103
- const shouldRerank = params.rerank ?? searchDefaults?.rerank ?? true;
104
- if (shouldRerank && results.length > 0) {
105
- const { rerankResults } = await import("./reranker-CMNZcfVx.mjs").then((n) => n.r);
106
- results = await rerankResults(params.query, results, { topK: searchOpts.maxResults ?? 5 });
107
- }
108
- const recencyDays = params.recencyBoost ?? searchDefaults?.recencyBoostDays ?? 0;
109
- if (recencyDays > 0 && results.length > 0) {
110
- const { applyRecencyBoost } = await import("./search-DC1qhkKn.mjs").then((n) => n.o);
111
- results = applyRecencyBoost(results, recencyDays);
112
- }
113
- const withSlugs = populateSlugs(results, registryDb);
114
- if (withSlugs.length === 0) return { content: [{
115
- type: "text",
116
- text: `No results found for query: "${params.query}" (mode: ${mode})`
117
- }] };
118
- const rerankLabel = shouldRerank ? " +rerank" : "";
119
- const formatted = withSlugs.map((r, i) => {
120
- const header = `[${i + 1}] ${r.projectSlug ?? `project:${r.projectId}`} — ${r.path} (lines ${r.startLine}-${r.endLine}) score=${r.score.toFixed(4)} tier=${r.tier} source=${r.source}`;
121
- const raw = r.snippet.trim();
122
- return `${header}\n${raw.length > snippetLength ? raw.slice(0, snippetLength) + "..." : raw}`;
123
- }).join("\n\n---\n\n");
124
- return { content: [{
125
- type: "text",
126
- text: `Found ${withSlugs.length} result(s) for "${params.query}" (mode: ${mode}${rerankLabel}):\n\n${formatted}`
127
- }] };
128
- } catch (e) {
129
- return {
130
- content: [{
131
- type: "text",
132
- text: `Search error: ${String(e)}`
133
- }],
134
- isError: true
135
- };
136
- }
137
- }
138
- function toolMemoryGet(registryDb, params) {
139
- try {
140
- const projectId = lookupProjectId(registryDb, params.project);
141
- if (projectId == null) return {
142
- content: [{
143
- type: "text",
144
- text: `Project not found: ${params.project}`
145
- }],
146
- isError: true
147
- };
148
- const project = registryDb.prepare("SELECT root_path FROM projects WHERE id = ?").get(projectId);
149
- if (!project) return {
150
- content: [{
151
- type: "text",
152
- text: `Project not found: ${params.project}`
153
- }],
154
- isError: true
155
- };
156
- const requestedPath = params.path;
157
- if (requestedPath.includes("..") || isAbsolute(requestedPath)) return {
158
- content: [{
159
- type: "text",
160
- text: `Invalid path: ${params.path} (must be a relative path within the project root, no ../ allowed)`
161
- }],
162
- isError: true
163
- };
164
- const fullPath = join(project.root_path, requestedPath);
165
- const resolvedFull = resolve(fullPath);
166
- const resolvedRoot = resolve(project.root_path);
167
- if (!resolvedFull.startsWith(resolvedRoot + "/") && resolvedFull !== resolvedRoot) return {
168
- content: [{
169
- type: "text",
170
- text: `Path traversal blocked: ${params.path}`
171
- }],
172
- isError: true
173
- };
174
- if (!existsSync(fullPath)) return {
175
- content: [{
176
- type: "text",
177
- text: `File not found: ${requestedPath} (project: ${params.project})`
178
- }],
179
- isError: true
180
- };
181
- const stat = statSync(fullPath);
182
- if (stat.size > 5 * 1024 * 1024) return { content: [{
183
- type: "text",
184
- text: `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Maximum 5 MB.`
185
- }] };
186
- const allLines = readFileSync(fullPath, "utf8").split("\n");
187
- const fromLine = (params.from ?? 1) - 1;
188
- const toLine = params.lines != null ? Math.min(fromLine + params.lines, allLines.length) : allLines.length;
189
- const text = allLines.slice(fromLine, toLine).join("\n");
190
- return { content: [{
191
- type: "text",
192
- text: `${params.from != null ? `${params.project}/${requestedPath} (lines ${fromLine + 1}-${toLine}):` : `${params.project}/${requestedPath}:`}\n\n${text}`
193
- }] };
194
- } catch (e) {
195
- return {
196
- content: [{
197
- type: "text",
198
- text: `Read error: ${String(e)}`
199
- }],
200
- isError: true
201
- };
202
- }
203
- }
204
- /**
205
- * Combine keyword + semantic results using min-max normalized scoring.
206
- * Mirrors the logic in searchMemoryHybrid() from memory/search.ts,
207
- * but works on pre-computed result arrays so it works for any backend.
208
- */
209
- function combineHybridResults(keywordResults, semanticResults, maxResults, keywordWeight = .5, semanticWeight = .5) {
210
- if (keywordResults.length === 0 && semanticResults.length === 0) return [];
211
- const keyFor = (r) => `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;
212
- function minMaxNormalize(items) {
213
- if (items.length === 0) return /* @__PURE__ */ new Map();
214
- const min = Math.min(...items.map((r) => r.score));
215
- const range = Math.max(...items.map((r) => r.score)) - min;
216
- const m = /* @__PURE__ */ new Map();
217
- for (const r of items) m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);
218
- return m;
219
- }
220
- const kwNorm = minMaxNormalize(keywordResults);
221
- const semNorm = minMaxNormalize(semanticResults);
222
- const allKeys = new Set([...keywordResults.map(keyFor), ...semanticResults.map(keyFor)]);
223
- const metaMap = /* @__PURE__ */ new Map();
224
- for (const r of [...keywordResults, ...semanticResults]) metaMap.set(keyFor(r), r);
225
- const combined = [];
226
- for (const key of allKeys) {
227
- const meta = metaMap.get(key);
228
- const kwScore = kwNorm.get(key) ?? 0;
229
- const semScore = semNorm.get(key) ?? 0;
230
- const combinedScore = keywordWeight * kwScore + semanticWeight * semScore;
231
- combined.push({
232
- ...meta,
233
- score: combinedScore,
234
- combinedScore
235
- });
236
- }
237
- return combined.sort((a, b) => b.score - a.score).slice(0, maxResults).map(({ combinedScore: _unused, ...r }) => r);
238
- }
239
-
240
- //#endregion
241
- //#region src/mcp/tools/projects.ts
242
- /**
243
- * MCP tool handlers: project_info, project_list, project_detect,
244
- * project_health, project_todo
245
- */
246
- function toolProjectInfo(registryDb, params) {
247
- try {
248
- let project = null;
249
- if (params.slug) {
250
- const projectId = lookupProjectId(registryDb, params.slug);
251
- if (projectId != null) project = registryDb.prepare("SELECT id, slug, display_name, root_path, type, status, created_at, updated_at, archived_at, parent_id, obsidian_link FROM projects WHERE id = ?").get(projectId);
252
- } else project = detectProjectFromPath(registryDb, process.cwd());
253
- if (!project) return {
254
- content: [{
255
- type: "text",
256
- text: params.slug ? `Project not found: ${params.slug}` : `No PAI project found matching the current directory: ${process.cwd()}`
257
- }],
258
- isError: !params.slug
259
- };
260
- return { content: [{
261
- type: "text",
262
- text: formatProject(registryDb, project)
263
- }] };
264
- } catch (e) {
265
- return {
266
- content: [{
267
- type: "text",
268
- text: `project_info error: ${String(e)}`
269
- }],
270
- isError: true
271
- };
272
- }
273
- }
274
- function toolProjectList(registryDb, params) {
275
- try {
276
- const conditions = [];
277
- const queryParams = [];
278
- if (params.status) {
279
- conditions.push("p.status = ?");
280
- queryParams.push(params.status);
281
- }
282
- if (params.tag) {
283
- conditions.push("p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = ?)");
284
- queryParams.push(params.tag);
285
- }
286
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
287
- const limit = params.limit ?? 50;
288
- queryParams.push(limit);
289
- const projects = registryDb.prepare(`SELECT p.id, p.slug, p.display_name, p.root_path, p.type, p.status, p.updated_at
290
- FROM projects p
291
- ${where}
292
- ORDER BY p.updated_at DESC
293
- LIMIT ?`).all(...queryParams);
294
- if (projects.length === 0) return { content: [{
295
- type: "text",
296
- text: "No projects found matching the given filters."
297
- }] };
298
- const lines = projects.map((p) => `${p.slug} [${p.status}] ${p.root_path} (updated: ${new Date(p.updated_at).toISOString().slice(0, 10)})`);
299
- return { content: [{
300
- type: "text",
301
- text: `${projects.length} project(s):\n\n${lines.join("\n")}`
302
- }] };
303
- } catch (e) {
304
- return {
305
- content: [{
306
- type: "text",
307
- text: `project_list error: ${String(e)}`
308
- }],
309
- isError: true
310
- };
311
- }
312
- }
313
- function toolProjectDetect(registryDb, params) {
314
- try {
315
- const detection = detectProject(registryDb, params.cwd);
316
- if (!detection) return { content: [{
317
- type: "text",
318
- text: `No registered project found for path: ${params.cwd ?? process.cwd()}\n\nRun 'pai project add .' to register this directory.`
319
- }] };
320
- return { content: [{
321
- type: "text",
322
- text: formatDetectionJson(detection)
323
- }] };
324
- } catch (e) {
325
- return {
326
- content: [{
327
- type: "text",
328
- text: `project_detect error: ${String(e)}`
329
- }],
330
- isError: true
331
- };
332
- }
333
- }
334
- async function toolProjectHealth(registryDb, params) {
335
- try {
336
- const { existsSync: fsExists, readdirSync, statSync } = await import("node:fs");
337
- const { join: pathJoin, basename: pathBasename } = await import("node:path");
338
- const { homedir } = await import("node:os");
339
- const { encodeDir: enc } = await import("./utils-QSfKagcj.mjs").then((n) => n.g);
340
- const rows = registryDb.prepare(`SELECT p.id, p.slug, p.display_name, p.root_path, p.encoded_dir, p.status, p.type,
341
- (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count
342
- FROM projects p
343
- ORDER BY p.slug ASC`).all();
344
- const home = homedir();
345
- const claudeProjects = pathJoin(home, ".claude", "projects");
346
- function suggestMoved(rootPath) {
347
- const name = pathBasename(rootPath);
348
- return [
349
- pathJoin(home, "dev", name),
350
- pathJoin(home, "dev", "ai", name),
351
- pathJoin(home, "Desktop", name),
352
- pathJoin(home, "Projects", name)
353
- ].find((c) => fsExists(c));
354
- }
355
- function hasClaudeNotes(encodedDir) {
356
- if (!fsExists(claudeProjects)) return false;
357
- try {
358
- for (const entry of readdirSync(claudeProjects)) {
359
- if (entry !== encodedDir && !entry.startsWith(encodedDir)) continue;
360
- const full = pathJoin(claudeProjects, entry);
361
- try {
362
- if (!statSync(full).isDirectory()) continue;
363
- } catch {
364
- continue;
365
- }
366
- if (fsExists(pathJoin(full, "Notes"))) return true;
367
- }
368
- } catch {}
369
- return false;
370
- }
371
- function findTodoForProject(rootPath) {
372
- for (const rel of [
373
- "Notes/TODO.md",
374
- ".claude/Notes/TODO.md",
375
- "tasks/todo.md",
376
- "TODO.md"
377
- ]) {
378
- const full = pathJoin(rootPath, rel);
379
- if (fsExists(full)) try {
380
- const raw = readFileSync(full, "utf8");
381
- return {
382
- found: true,
383
- path: rel,
384
- has_continue: /^## Continue$/m.test(raw)
385
- };
386
- } catch {
387
- return {
388
- found: true,
389
- path: rel,
390
- has_continue: false
391
- };
392
- }
393
- }
394
- return {
395
- found: false,
396
- path: null,
397
- has_continue: false
398
- };
399
- }
400
- const results = rows.map((p) => {
401
- const pathExists = fsExists(p.root_path);
402
- let health;
403
- let suggestedPath = null;
404
- if (pathExists) health = "active";
405
- else {
406
- suggestedPath = suggestMoved(p.root_path) ?? null;
407
- health = suggestedPath ? "stale" : "dead";
408
- }
409
- const todo = pathExists ? findTodoForProject(p.root_path) : {
410
- found: false,
411
- path: null,
412
- has_continue: false
413
- };
414
- return {
415
- slug: p.slug,
416
- display_name: p.display_name,
417
- root_path: p.root_path,
418
- status: p.status,
419
- type: p.type,
420
- session_count: p.session_count,
421
- health,
422
- suggested_path: suggestedPath,
423
- has_claude_notes: hasClaudeNotes(p.encoded_dir),
424
- todo
425
- };
426
- });
427
- const filtered = !params.category || params.category === "all" ? results : results.filter((r) => r.health === params.category);
428
- const summary = {
429
- total: rows.length,
430
- active: results.filter((r) => r.health === "active").length,
431
- stale: results.filter((r) => r.health === "stale").length,
432
- dead: results.filter((r) => r.health === "dead").length
433
- };
434
- return { content: [{
435
- type: "text",
436
- text: JSON.stringify({
437
- summary,
438
- projects: filtered
439
- }, null, 2)
440
- }] };
441
- } catch (e) {
442
- return {
443
- content: [{
444
- type: "text",
445
- text: `project_health error: ${String(e)}`
446
- }],
447
- isError: true
448
- };
449
- }
450
- }
451
- /**
452
- * TODO candidate locations searched in priority order.
453
- * Returns the first one that exists, along with its label.
454
- */
455
- const TODO_LOCATIONS = [
456
- {
457
- rel: "Notes/TODO.md",
458
- label: "Notes/TODO.md"
459
- },
460
- {
461
- rel: ".claude/Notes/TODO.md",
462
- label: ".claude/Notes/TODO.md"
463
- },
464
- {
465
- rel: "tasks/todo.md",
466
- label: "tasks/todo.md"
467
- },
468
- {
469
- rel: "TODO.md",
470
- label: "TODO.md"
471
- }
472
- ];
473
- /**
474
- * Given TODO file content, extract and surface the ## Continue section first,
475
- * then return the remaining content. Returns an object with:
476
- * continueSection: string | null
477
- * fullContent: string
478
- * hasContinue: boolean
479
- */
480
- function parseTodoContent(raw) {
481
- const lines = raw.split("\n");
482
- const continueIdx = lines.findIndex((l) => l.trim() === "## Continue");
483
- if (continueIdx === -1) return {
484
- continueSection: null,
485
- fullContent: raw,
486
- hasContinue: false
487
- };
488
- let endIdx = lines.length;
489
- for (let i = continueIdx + 1; i < lines.length; i++) {
490
- const trimmed = lines[i].trim();
491
- if (trimmed === "---" || trimmed.startsWith("##") && trimmed !== "## Continue") {
492
- endIdx = i;
493
- break;
494
- }
495
- }
496
- return {
497
- continueSection: lines.slice(continueIdx, endIdx).join("\n").trim(),
498
- fullContent: raw,
499
- hasContinue: true
500
- };
501
- }
502
- function toolProjectTodo(registryDb, params) {
503
- try {
504
- let rootPath;
505
- let projectSlug;
506
- if (params.project) {
507
- const projectId = lookupProjectId(registryDb, params.project);
508
- if (projectId == null) return {
509
- content: [{
510
- type: "text",
511
- text: `Project not found: ${params.project}`
512
- }],
513
- isError: true
514
- };
515
- const row = registryDb.prepare("SELECT root_path, slug FROM projects WHERE id = ?").get(projectId);
516
- if (!row) return {
517
- content: [{
518
- type: "text",
519
- text: `Project not found: ${params.project}`
520
- }],
521
- isError: true
522
- };
523
- rootPath = row.root_path;
524
- projectSlug = row.slug;
525
- } else {
526
- const project = detectProjectFromPath(registryDb, process.cwd());
527
- if (!project) return { content: [{
528
- type: "text",
529
- text: `No PAI project found matching the current directory: ${process.cwd()}\n\nProvide a project slug or run 'pai project add .' to register this directory.`
530
- }] };
531
- rootPath = project.root_path;
532
- projectSlug = project.slug;
533
- }
534
- for (const loc of TODO_LOCATIONS) {
535
- const fullPath = join(rootPath, loc.rel);
536
- if (existsSync(fullPath)) {
537
- const { continueSection, fullContent, hasContinue } = parseTodoContent(readFileSync(fullPath, "utf8"));
538
- let output;
539
- if (hasContinue && continueSection) output = [
540
- `TODO found: ${projectSlug}/${loc.label}`,
541
- "",
542
- "=== CONTINUE SECTION (surfaced first) ===",
543
- continueSection,
544
- "",
545
- "=== FULL TODO CONTENT ===",
546
- fullContent
547
- ].join("\n");
548
- else output = [
549
- `TODO found: ${projectSlug}/${loc.label}`,
550
- "",
551
- fullContent
552
- ].join("\n");
553
- return { content: [{
554
- type: "text",
555
- text: output
556
- }] };
557
- }
558
- }
559
- const searched = TODO_LOCATIONS.map((l) => ` ${rootPath}/${l.rel}`).join("\n");
560
- return { content: [{
561
- type: "text",
562
- text: [
563
- `No TODO.md found for project: ${projectSlug}`,
564
- "",
565
- "Searched locations (in order):",
566
- searched,
567
- "",
568
- "Create a TODO with: echo '## Tasks\\n- [ ] First task' > Notes/TODO.md"
569
- ].join("\n")
570
- }] };
571
- } catch (e) {
572
- return {
573
- content: [{
574
- type: "text",
575
- text: `project_todo error: ${String(e)}`
576
- }],
577
- isError: true
578
- };
579
- }
580
- }
581
-
582
- //#endregion
583
- //#region src/mcp/tools/sessions.ts
584
- function toolSessionList(registryDb, params) {
585
- try {
586
- const projectId = lookupProjectId(registryDb, params.project);
587
- if (projectId == null) return {
588
- content: [{
589
- type: "text",
590
- text: `Project not found: ${params.project}`
591
- }],
592
- isError: true
593
- };
594
- const conditions = ["project_id = ?"];
595
- const queryParams = [projectId];
596
- if (params.status) {
597
- conditions.push("status = ?");
598
- queryParams.push(params.status);
599
- }
600
- const limit = params.limit ?? 10;
601
- queryParams.push(limit);
602
- const sessions = registryDb.prepare(`SELECT number, date, title, filename, status
603
- FROM sessions
604
- WHERE ${conditions.join(" AND ")}
605
- ORDER BY number DESC
606
- LIMIT ?`).all(...queryParams);
607
- if (sessions.length === 0) return { content: [{
608
- type: "text",
609
- text: `No sessions found for project: ${params.project}`
610
- }] };
611
- const lines = sessions.map((s) => `#${String(s.number).padStart(4, "0")} ${s.date} [${s.status}] ${s.title}\n file: Notes/${s.filename}`);
612
- return { content: [{
613
- type: "text",
614
- text: `${sessions.length} session(s) for ${params.project}:\n\n${lines.join("\n\n")}`
615
- }] };
616
- } catch (e) {
617
- return {
618
- content: [{
619
- type: "text",
620
- text: `session_list error: ${String(e)}`
621
- }],
622
- isError: true
623
- };
624
- }
625
- }
626
- /**
627
- * Automatically suggest which project a session belongs to.
628
- *
629
- * Strategy (in priority order):
630
- * 1. path — exact or parent-directory match in the project registry
631
- * 2. marker — walk up from cwd looking for Notes/PAI.md
632
- * 3. topic — BM25 keyword search against memory (requires context)
633
- *
634
- * Call this at session start (e.g., from CLAUDE.md or a session-start hook)
635
- * to automatically route the session to the correct project.
636
- */
637
- async function toolSessionRoute(registryDb, federation, params) {
638
- try {
639
- const { autoRoute, formatAutoRouteJson } = await import("./auto-route-C-DrW6BL.mjs");
640
- const result = await autoRoute(registryDb, federation, params.cwd, params.context);
641
- if (!result) return { content: [{
642
- type: "text",
643
- text: [
644
- `No project match found for: ${params.cwd ?? process.cwd()}`,
645
- "",
646
- "Tried: path match, PAI.md marker walk" + (params.context ? ", topic detection" : ""),
647
- "",
648
- "Run 'pai project add .' to register this directory,",
649
- "or provide conversation context for topic-based routing."
650
- ].join("\n")
651
- }] };
652
- return { content: [{
653
- type: "text",
654
- text: formatAutoRouteJson(result)
655
- }] };
656
- } catch (e) {
657
- return {
658
- content: [{
659
- type: "text",
660
- text: `session_route error: ${String(e)}`
661
- }],
662
- isError: true
663
- };
664
- }
665
- }
666
-
667
- //#endregion
668
- //#region src/mcp/tools/registry.ts
669
- function toolRegistrySearch(registryDb, params) {
670
- try {
671
- const q = `%${params.query}%`;
672
- const projects = registryDb.prepare(`SELECT id, slug, display_name, root_path, type, status, updated_at
673
- FROM projects
674
- WHERE slug LIKE ?
675
- OR display_name LIKE ?
676
- OR root_path LIKE ?
677
- ORDER BY updated_at DESC
678
- LIMIT 20`).all(q, q, q);
679
- if (projects.length === 0) return { content: [{
680
- type: "text",
681
- text: `No projects found matching: "${params.query}"`
682
- }] };
683
- const lines = projects.map((p) => `${p.slug} [${p.status}] ${p.root_path}`);
684
- return { content: [{
685
- type: "text",
686
- text: `${projects.length} match(es) for "${params.query}":\n\n${lines.join("\n")}`
687
- }] };
688
- } catch (e) {
689
- return {
690
- content: [{
691
- type: "text",
692
- text: `registry_search error: ${String(e)}`
693
- }],
694
- isError: true
695
- };
696
- }
697
- }
698
-
699
- //#endregion
700
- //#region src/mcp/tools/zettel.ts
701
- async function toolZettelExplore(backend, params) {
702
- try {
703
- const { zettelExplore } = await import("./zettelkasten-cdajbnPr.mjs");
704
- const result = await zettelExplore(backend, {
705
- startNote: params.start_note,
706
- depth: params.depth,
707
- direction: params.direction,
708
- mode: params.mode
709
- });
710
- return { content: [{
711
- type: "text",
712
- text: JSON.stringify(result, null, 2)
713
- }] };
714
- } catch (e) {
715
- return {
716
- content: [{
717
- type: "text",
718
- text: `zettel_explore error: ${String(e)}`
719
- }],
720
- isError: true
721
- };
722
- }
723
- }
724
- async function toolZettelHealth(backend, params) {
725
- try {
726
- const { zettelHealth } = await import("./zettelkasten-cdajbnPr.mjs");
727
- const result = await zettelHealth(backend, {
728
- scope: params.scope,
729
- projectPath: params.project_path,
730
- recentDays: params.recent_days,
731
- include: params.include
732
- });
733
- return { content: [{
734
- type: "text",
735
- text: JSON.stringify(result, null, 2)
736
- }] };
737
- } catch (e) {
738
- return {
739
- content: [{
740
- type: "text",
741
- text: `zettel_health error: ${String(e)}`
742
- }],
743
- isError: true
744
- };
745
- }
746
- }
747
- async function toolZettelSurprise(backend, params) {
748
- try {
749
- const { zettelSurprise } = await import("./zettelkasten-cdajbnPr.mjs");
750
- const results = await zettelSurprise(backend, {
751
- referencePath: params.reference_path,
752
- vaultProjectId: params.vault_project_id,
753
- limit: params.limit,
754
- minSimilarity: params.min_similarity,
755
- minGraphDistance: params.min_graph_distance
756
- });
757
- return { content: [{
758
- type: "text",
759
- text: JSON.stringify(results, null, 2)
760
- }] };
761
- } catch (e) {
762
- return {
763
- content: [{
764
- type: "text",
765
- text: `zettel_surprise error: ${String(e)}`
766
- }],
767
- isError: true
768
- };
769
- }
770
- }
771
- async function toolZettelSuggest(backend, params) {
772
- try {
773
- const { zettelSuggest } = await import("./zettelkasten-cdajbnPr.mjs");
774
- const results = await zettelSuggest(backend, {
775
- notePath: params.note_path,
776
- vaultProjectId: params.vault_project_id,
777
- limit: params.limit,
778
- excludeLinked: params.exclude_linked
779
- });
780
- return { content: [{
781
- type: "text",
782
- text: JSON.stringify(results, null, 2)
783
- }] };
784
- } catch (e) {
785
- return {
786
- content: [{
787
- type: "text",
788
- text: `zettel_suggest error: ${String(e)}`
789
- }],
790
- isError: true
791
- };
792
- }
793
- }
794
- async function toolZettelConverse(backend, params) {
795
- try {
796
- const { zettelConverse } = await import("./zettelkasten-cdajbnPr.mjs");
797
- const result = await zettelConverse(backend, {
798
- question: params.question,
799
- vaultProjectId: params.vault_project_id,
800
- depth: params.depth,
801
- limit: params.limit
802
- });
803
- return { content: [{
804
- type: "text",
805
- text: JSON.stringify(result, null, 2)
806
- }] };
807
- } catch (e) {
808
- return {
809
- content: [{
810
- type: "text",
811
- text: `zettel_converse error: ${String(e)}`
812
- }],
813
- isError: true
814
- };
815
- }
816
- }
817
- async function toolZettelThemes(backend, params) {
818
- try {
819
- const { zettelThemes } = await import("./zettelkasten-cdajbnPr.mjs");
820
- const result = await zettelThemes(backend, {
821
- vaultProjectId: params.vault_project_id,
822
- lookbackDays: params.lookback_days,
823
- minClusterSize: params.min_cluster_size,
824
- maxThemes: params.max_themes,
825
- similarityThreshold: params.similarity_threshold
826
- });
827
- return { content: [{
828
- type: "text",
829
- text: JSON.stringify(result, null, 2)
830
- }] };
831
- } catch (e) {
832
- return {
833
- content: [{
834
- type: "text",
835
- text: `zettel_themes error: ${String(e)}`
836
- }],
837
- isError: true
838
- };
839
- }
840
- }
841
-
842
- //#endregion
843
- //#region src/mcp/tools.ts
844
- var tools_exports = /* @__PURE__ */ __exportAll({
845
- combineHybridResults: () => combineHybridResults,
846
- detectProjectFromPath: () => detectProjectFromPath,
847
- formatProject: () => formatProject,
848
- lookupProjectId: () => lookupProjectId,
849
- toolMemoryGet: () => toolMemoryGet,
850
- toolMemorySearch: () => toolMemorySearch,
851
- toolProjectDetect: () => toolProjectDetect,
852
- toolProjectHealth: () => toolProjectHealth,
853
- toolProjectInfo: () => toolProjectInfo,
854
- toolProjectList: () => toolProjectList,
855
- toolProjectTodo: () => toolProjectTodo,
856
- toolRegistrySearch: () => toolRegistrySearch,
857
- toolSessionList: () => toolSessionList,
858
- toolSessionRoute: () => toolSessionRoute,
859
- toolZettelConverse: () => toolZettelConverse,
860
- toolZettelExplore: () => toolZettelExplore,
861
- toolZettelHealth: () => toolZettelHealth,
862
- toolZettelSuggest: () => toolZettelSuggest,
863
- toolZettelSurprise: () => toolZettelSurprise,
864
- toolZettelThemes: () => toolZettelThemes
865
- });
866
-
867
- //#endregion
868
- export { toolProjectDetect as a, toolProjectList as c, toolMemorySearch as d, toolSessionRoute as i, toolProjectTodo as l, toolRegistrySearch as n, toolProjectHealth as o, toolSessionList as r, toolProjectInfo as s, tools_exports as t, toolMemoryGet as u };
869
- //# sourceMappingURL=tools-DcaJlYDN.mjs.map