@tekmidian/pai 0.66.1 → 0.66.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{aibroker-client-B8c42Lh8.mjs → aibroker-client-CHEEvJZW.mjs} +2 -2
- package/dist/{aibroker-client-C5Fw7DNz.mjs → aibroker-client-Dfv7j1Od.mjs} +3 -3
- package/dist/{aibroker-client-C5Fw7DNz.mjs.map → aibroker-client-Dfv7j1Od.mjs.map} +1 -1
- package/dist/{auto-route-o0BOsXn7.mjs → auto-route-BnizyALK.mjs} +65 -4
- package/dist/auto-route-BnizyALK.mjs.map +1 -0
- package/dist/auto-route-D6fW1Q8z.mjs +3 -0
- package/dist/{chain-CZfKIEq9.mjs → chain-D5876UQX.mjs} +3 -4
- package/dist/{chain-CZfKIEq9.mjs.map → chain-D5876UQX.mjs.map} +1 -1
- package/dist/cli/index.mjs +18 -23
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.mjs +17 -22
- package/dist/{config-BbLFD7Uf.mjs → config-C-LiGlop.mjs} +2 -2
- package/dist/{config-BbLFD7Uf.mjs.map → config-C-LiGlop.mjs.map} +1 -1
- package/dist/{config-YinjgXEJ.mjs → config-DKIA7wgF.mjs} +1 -1
- package/dist/{context-handover-cache-pkHzmL3c.mjs → context-handover-cache-9PGvXRIw.mjs} +984 -10
- package/dist/context-handover-cache-9PGvXRIw.mjs.map +1 -0
- package/dist/daemon/index.mjs +17 -19
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BUgNvMUh.mjs +5029 -0
- package/dist/daemon-BUgNvMUh.mjs.map +1 -0
- package/dist/daemon-DL0eglMJ.mjs +18 -0
- package/dist/daemon-mcp/index.mjs +12 -9
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{embeddings-CcscYWwk.mjs → embeddings-CEBGrzwu.mjs} +1 -1
- package/dist/{embeddings-Bx3q0QOY.mjs → embeddings-DOLZnT1X.mjs} +1 -1
- package/dist/{embeddings-Bx3q0QOY.mjs.map → embeddings-DOLZnT1X.mjs.map} +1 -1
- package/dist/{env-JNEIrQWg.mjs → env-DiolswKQ.mjs} +1 -1
- package/dist/{env-JNEIrQWg.mjs.map → env-DiolswKQ.mjs.map} +1 -1
- package/dist/factory-DrBg24UT.mjs +8 -0
- package/dist/factory-Ypf8r0fK.mjs +2892 -0
- package/dist/factory-Ypf8r0fK.mjs.map +1 -0
- package/dist/{fallback-CIiq2Raw.mjs → fallback-D51R4shY.mjs} +23 -11
- package/dist/fallback-D51R4shY.mjs.map +1 -0
- package/dist/{chunker-BH4i-F2b.mjs → helpers-CZsi_49C.mjs} +221 -2
- package/dist/helpers-CZsi_49C.mjs.map +1 -0
- package/dist/hooks/worker-status-line.mjs.map +2 -2
- package/dist/index.mjs +6 -7
- package/dist/{ipc-client-D16Xw6Uo.mjs → ipc-client-BLYX51nG.mjs} +2 -2
- package/dist/{ipc-client-D16Xw6Uo.mjs.map → ipc-client-BLYX51nG.mjs.map} +1 -1
- package/dist/main-resolver-BYgP86C1.mjs +7 -0
- package/dist/{main-resolver-DPPwHNtn.mjs → main-resolver-kVkMJZUD.mjs} +6 -6
- package/dist/{main-resolver-DPPwHNtn.mjs.map → main-resolver-kVkMJZUD.mjs.map} +1 -1
- package/dist/{migrate-BD7D8EEh.mjs → migrate-Bq0esMOa.mjs} +2 -2
- package/dist/{migrate-BD7D8EEh.mjs.map → migrate-Bq0esMOa.mjs.map} +1 -1
- package/dist/{pai-marker-D1MMswkz.mjs → pai-marker-DXVpFsYz.mjs} +1 -1
- package/dist/{pai-marker-D1MMswkz.mjs.map → pai-marker-DXVpFsYz.mjs.map} +1 -1
- package/dist/{planner-Dm5Acyo1.mjs → planner-D5yt0EfA.mjs} +10 -7
- package/dist/{planner-Dm5Acyo1.mjs.map → planner-D5yt0EfA.mjs.map} +1 -1
- package/dist/postgres-BmLr0MUm.mjs +5 -0
- package/dist/{postgres-Ceqsa64C.mjs → postgres-D3xc2RB4.mjs} +364 -9
- package/dist/postgres-D3xc2RB4.mjs.map +1 -0
- package/dist/{program-JJ0TwSHI.mjs → program-DIXRtAFy.mjs} +77 -70
- package/dist/program-DIXRtAFy.mjs.map +1 -0
- package/dist/{query-feedback-BIaZTTFO.mjs → query-feedback-B7FYE4JR.mjs} +1 -1
- package/dist/{query-feedback-BIaZTTFO.mjs.map → query-feedback-B7FYE4JR.mjs.map} +1 -1
- package/dist/{reranker-DKv80KO5.mjs → reranker-3lnggwgq.mjs} +1 -1
- package/dist/{reranker-DKv80KO5.mjs.map → reranker-3lnggwgq.mjs.map} +1 -1
- package/dist/{reranker-CFiEzHQu.mjs → reranker-CZ2mP4cf.mjs} +1 -1
- package/dist/router-BK-hFeQ6.mjs +3 -0
- package/dist/{router-a5G7q8_3.mjs → router-BaTbc9VX.mjs} +2 -2
- package/dist/{router-a5G7q8_3.mjs.map → router-BaTbc9VX.mjs.map} +1 -1
- package/dist/{run-BCpbxqA5.mjs → run-CLYTOm0x.mjs} +599 -22
- package/dist/run-CLYTOm0x.mjs.map +1 -0
- package/dist/{run-env-DRcp7A8K.mjs → run-env-Bnmf6bRI.mjs} +2 -2
- package/dist/{run-env-DRcp7A8K.mjs.map → run-env-Bnmf6bRI.mjs.map} +1 -1
- package/dist/{runtime-paths-CHTg3ywb.mjs → runtime-paths-D3FKAm69.mjs} +1 -1
- package/dist/{runtime-paths-CHTg3ywb.mjs.map → runtime-paths-D3FKAm69.mjs.map} +1 -1
- package/dist/{search-BOSphCJ1.mjs → search-CNAGTiJP.mjs} +2 -2
- package/dist/{search-BOSphCJ1.mjs.map → search-CNAGTiJP.mjs.map} +1 -1
- package/dist/{sources-CijVso3n.mjs → sources-CM2g-CLT.mjs} +2 -2
- package/dist/{sources-CijVso3n.mjs.map → sources-CM2g-CLT.mjs.map} +1 -1
- package/dist/{stop-words-BdQuaE9K.mjs → stop-words-DtxaTWU_.mjs} +1 -1
- package/dist/{stop-words-BdQuaE9K.mjs.map → stop-words-DtxaTWU_.mjs.map} +1 -1
- package/dist/{utils-C9HsYDpO.mjs → utils-DddRwMsG.mjs} +2 -2
- package/dist/{utils-C9HsYDpO.mjs.map → utils-DddRwMsG.mjs.map} +1 -1
- package/dist/{zettelkasten-CtGHQWPU.mjs → zettelkasten-BhZMvmoK.mjs} +149 -7
- package/dist/zettelkasten-BhZMvmoK.mjs.map +1 -0
- package/dist/zettelkasten-BwV1pluL.mjs +5 -0
- package/docs/commands/worker.md +2 -0
- package/package.json +1 -1
- package/dist/async-C2Bm_Lal.mjs +0 -300
- package/dist/async-C2Bm_Lal.mjs.map +0 -1
- package/dist/auto-route-o0BOsXn7.mjs.map +0 -1
- package/dist/chunker-BH4i-F2b.mjs.map +0 -1
- package/dist/clusters-BCtD3fbe.mjs +0 -169
- package/dist/clusters-BCtD3fbe.mjs.map +0 -1
- package/dist/context-handover-cache-pkHzmL3c.mjs.map +0 -1
- package/dist/daemon-DXlaVCkw.mjs +0 -1741
- package/dist/daemon-DXlaVCkw.mjs.map +0 -1
- package/dist/daemon-DyORmxZu.mjs +0 -19
- package/dist/detector-BEPFsINR.mjs +0 -3
- package/dist/detector-BRTtAWrM.mjs +0 -65
- package/dist/detector-BRTtAWrM.mjs.map +0 -1
- package/dist/factory-CNpPQQ-5.mjs +0 -249
- package/dist/factory-CNpPQQ-5.mjs.map +0 -1
- package/dist/factory-DY7x73mE.mjs +0 -3
- package/dist/fallback-CIiq2Raw.mjs.map +0 -1
- package/dist/federation-db-BTyoufBh.mjs +0 -139
- package/dist/federation-db-BTyoufBh.mjs.map +0 -1
- package/dist/federation-db-HfIFc7FG.mjs +0 -3
- package/dist/helpers-BRCJg0G3.mjs +0 -223
- package/dist/helpers-BRCJg0G3.mjs.map +0 -1
- package/dist/indexer-backend-DFF2FrYx.mjs +0 -5
- package/dist/indexer-backend-isSLg6yE.mjs +0 -1
- package/dist/kg-entity-DCOcsVFD.mjs +0 -29
- package/dist/kg-entity-DCOcsVFD.mjs.map +0 -1
- package/dist/latent-ideas-wwAXquGe.mjs +0 -191
- package/dist/latent-ideas-wwAXquGe.mjs.map +0 -1
- package/dist/link-boost-CnI7UVMJ.mjs +0 -34
- package/dist/link-boost-CnI7UVMJ.mjs.map +0 -1
- package/dist/main-resolver-BKz_OuEh.mjs +0 -7
- package/dist/merge-2gqRPFu2.mjs +0 -3
- package/dist/merge-DgU9OgZy.mjs +0 -6
- package/dist/merge-DgU9OgZy.mjs.map +0 -1
- package/dist/module-paths-DdRzbkUI.mjs +0 -44
- package/dist/module-paths-DdRzbkUI.mjs.map +0 -1
- package/dist/neighborhood-2FsmzoxG.mjs +0 -114
- package/dist/neighborhood-2FsmzoxG.mjs.map +0 -1
- package/dist/note-context-De63k8na.mjs +0 -106
- package/dist/note-context-De63k8na.mjs.map +0 -1
- package/dist/postgres-Ceqsa64C.mjs.map +0 -1
- package/dist/program-JJ0TwSHI.mjs.map +0 -1
- package/dist/query-feedback-B_iigYj-.mjs +0 -3
- package/dist/registry-db-C7voqML9.mjs +0 -213
- package/dist/registry-db-C7voqML9.mjs.map +0 -1
- package/dist/registry-db-JHPhA8vF.mjs +0 -3
- package/dist/registry-postgres-nOfyBSIW.mjs +0 -795
- package/dist/registry-postgres-nOfyBSIW.mjs.map +0 -1
- package/dist/registry-sqlite-DrCW1aRK.mjs +0 -593
- package/dist/registry-sqlite-DrCW1aRK.mjs.map +0 -1
- package/dist/router-CXUGsv85.mjs +0 -3
- package/dist/run-BCpbxqA5.mjs.map +0 -1
- package/dist/search-Bf3Kub3F.mjs +0 -3
- package/dist/server-DgmAHyFK.mjs +0 -403
- package/dist/server-DgmAHyFK.mjs.map +0 -1
- package/dist/session-keepalive-DxjTHGcK.mjs +0 -582
- package/dist/session-keepalive-DxjTHGcK.mjs.map +0 -1
- package/dist/sqlite-CqsTy6xo.mjs +0 -936
- package/dist/sqlite-CqsTy6xo.mjs.map +0 -1
- package/dist/state-DW8zdweW.mjs +0 -3
- package/dist/state-HyjqTihC.mjs +0 -76
- package/dist/state-HyjqTihC.mjs.map +0 -1
- package/dist/themes-CTaOj3e1.mjs +0 -148
- package/dist/themes-CTaOj3e1.mjs.map +0 -1
- package/dist/tools-BcOKPBtg.mjs +0 -1489
- package/dist/tools-BcOKPBtg.mjs.map +0 -1
- package/dist/tools-C-_l0o8Z.mjs +0 -6
- package/dist/trace-CtV0RO6n.mjs +0 -137
- package/dist/trace-CtV0RO6n.mjs.map +0 -1
- package/dist/utils-BrX9FLeK.mjs +0 -3
- package/dist/vault-indexer-BG1sEVyM.mjs +0 -537
- package/dist/vault-indexer-BG1sEVyM.mjs.map +0 -1
- package/dist/wakeup-D8n33hYV.mjs +0 -335
- package/dist/wakeup-D8n33hYV.mjs.map +0 -1
- package/dist/work-queue-worker-4tLa5gpQ.mjs +0 -567
- package/dist/work-queue-worker-4tLa5gpQ.mjs.map +0 -1
- package/dist/work-queue-worker-rFZvTO3p.mjs +0 -11
- package/dist/zettelkasten-CtGHQWPU.mjs.map +0 -1
package/dist/tools-BcOKPBtg.mjs
DELETED
|
@@ -1,1489 +0,0 @@
|
|
|
1
|
-
import { t as buildOutline } from "./chunker-BH4i-F2b.mjs";
|
|
2
|
-
import { i as populateSlugs } from "./search-BOSphCJ1.mjs";
|
|
3
|
-
import { a as formatDetectionJson, r as detectProject, t as buildWakeupContext } from "./wakeup-D8n33hYV.mjs";
|
|
4
|
-
import { n as cosineSimilarity } from "./embeddings-Bx3q0QOY.mjs";
|
|
5
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
7
|
-
|
|
8
|
-
//#region src/mcp/tools/types.ts
|
|
9
|
-
/**
|
|
10
|
-
* Shared types and project-row helpers used across all MCP tool handler modules.
|
|
11
|
-
*/
|
|
12
|
-
async function lookupProjectId(registry, slug) {
|
|
13
|
-
const bySlug = await registry.getProjectBySlug(slug);
|
|
14
|
-
if (bySlug) return bySlug.id;
|
|
15
|
-
return await registry.resolveAlias(slug) ?? null;
|
|
16
|
-
}
|
|
17
|
-
async function detectProjectFromPath(registry, fsPath) {
|
|
18
|
-
const resolved = resolve(fsPath);
|
|
19
|
-
const exact = await registry.getProjectByRootPath(resolved);
|
|
20
|
-
if (exact) return exact;
|
|
21
|
-
const all = await registry.listProjectsByPathLengthDesc();
|
|
22
|
-
for (const project of all) if (resolved.startsWith(project.root_path + "/") || resolved === project.root_path) return project;
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
async function formatProject(registry, project) {
|
|
26
|
-
const sessionCount = await registry.countSessionsForProject(project.id);
|
|
27
|
-
const lastSessionDate = await registry.getMostRecentSessionDate(project.id);
|
|
28
|
-
const tags = await registry.listTagsForProject(project.id);
|
|
29
|
-
const aliases = await registry.listAliasesForProject(project.id);
|
|
30
|
-
const lines = [
|
|
31
|
-
`slug: ${project.slug}`,
|
|
32
|
-
`display_name: ${project.display_name}`,
|
|
33
|
-
`root_path: ${project.root_path}`,
|
|
34
|
-
`type: ${project.type}`,
|
|
35
|
-
`status: ${project.status}`,
|
|
36
|
-
`sessions: ${sessionCount}`
|
|
37
|
-
];
|
|
38
|
-
if (lastSessionDate) lines.push(`last_session: ${lastSessionDate}`);
|
|
39
|
-
if (tags.length) lines.push(`tags: ${tags.join(", ")}`);
|
|
40
|
-
if (aliases.length) lines.push(`aliases: ${aliases.join(", ")}`);
|
|
41
|
-
if (project.obsidian_link) lines.push(`obsidian_link: ${project.obsidian_link}`);
|
|
42
|
-
if (project.archived_at) lines.push(`archived_at: ${new Date(project.archived_at).toISOString().slice(0, 10)}`);
|
|
43
|
-
return lines.join("\n");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
//#endregion
|
|
47
|
-
//#region src/mcp/tools/memory.ts
|
|
48
|
-
/**
|
|
49
|
-
* MCP tool handlers: memory_search, memory_get, memory_outline
|
|
50
|
-
*/
|
|
51
|
-
async function toolMemorySearch(registry, federation, params, searchDefaults) {
|
|
52
|
-
try {
|
|
53
|
-
const projectIds = params.project ? await (async () => {
|
|
54
|
-
const id = await lookupProjectId(registry, params.project);
|
|
55
|
-
return id != null ? [id] : [];
|
|
56
|
-
})() : void 0;
|
|
57
|
-
if (params.project && (!projectIds || projectIds.length === 0)) return {
|
|
58
|
-
content: [{
|
|
59
|
-
type: "text",
|
|
60
|
-
text: `Project not found: ${params.project}`
|
|
61
|
-
}],
|
|
62
|
-
isError: true
|
|
63
|
-
};
|
|
64
|
-
const mode = params.mode ?? searchDefaults?.mode ?? "keyword";
|
|
65
|
-
const snippetLength = params.snippetLength ?? searchDefaults?.snippetLength ?? 200;
|
|
66
|
-
const searchOpts = {
|
|
67
|
-
projectIds,
|
|
68
|
-
sources: params.sources,
|
|
69
|
-
maxResults: params.limit ?? searchDefaults?.defaultLimit ?? 5
|
|
70
|
-
};
|
|
71
|
-
let results;
|
|
72
|
-
if (mode === "keyword") results = await federation.searchKeyword(params.query, searchOpts);
|
|
73
|
-
else if (mode === "semantic" || mode === "hybrid") {
|
|
74
|
-
const { generateEmbedding } = await import("./embeddings-CcscYWwk.mjs");
|
|
75
|
-
const queryEmbedding = await generateEmbedding(params.query, true);
|
|
76
|
-
if (mode === "semantic") results = await federation.searchSemantic(queryEmbedding, searchOpts);
|
|
77
|
-
else {
|
|
78
|
-
const [kwResults, semResults] = await Promise.all([federation.searchKeyword(params.query, {
|
|
79
|
-
...searchOpts,
|
|
80
|
-
maxResults: 50
|
|
81
|
-
}), federation.searchSemantic(queryEmbedding, {
|
|
82
|
-
...searchOpts,
|
|
83
|
-
maxResults: 50
|
|
84
|
-
})]);
|
|
85
|
-
results = combineHybridResults(kwResults, semResults, searchOpts.maxResults ?? 10);
|
|
86
|
-
}
|
|
87
|
-
} else results = await federation.searchKeyword(params.query, searchOpts);
|
|
88
|
-
try {
|
|
89
|
-
const { applyLinkBoost } = await import("./link-boost-CnI7UVMJ.mjs");
|
|
90
|
-
const paths = [...new Set(results.map((r) => r.path))];
|
|
91
|
-
if (paths.length > 1) {
|
|
92
|
-
const edges = [];
|
|
93
|
-
for (const p of paths) for (const l of await federation.getLinksToTarget(p)) {
|
|
94
|
-
if (l.confidence && l.confidence !== "EXTRACTED") continue;
|
|
95
|
-
edges.push({
|
|
96
|
-
sourcePath: l.sourcePath,
|
|
97
|
-
targetPath: p
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
if (edges.length > 0) results = applyLinkBoost(results, edges);
|
|
101
|
-
}
|
|
102
|
-
} catch {}
|
|
103
|
-
try {
|
|
104
|
-
const chunkIds = results.map((r) => r.chunkId).filter((id) => id != null);
|
|
105
|
-
if (chunkIds.length > 0) await federation.touchChunksLastAccessed(chunkIds);
|
|
106
|
-
} catch {}
|
|
107
|
-
const shouldRerank = params.rerank ?? searchDefaults?.rerank ?? true;
|
|
108
|
-
if (shouldRerank && results.length > 0) {
|
|
109
|
-
const { rerankResults } = await import("./reranker-CFiEzHQu.mjs");
|
|
110
|
-
results = await rerankResults(params.query, results, { topK: searchOpts.maxResults ?? 5 });
|
|
111
|
-
}
|
|
112
|
-
const recencyDays = params.recencyBoost ?? searchDefaults?.recencyBoostDays ?? 0;
|
|
113
|
-
if (recencyDays > 0 && results.length > 0) {
|
|
114
|
-
const { applyRecencyBoost } = await import("./search-Bf3Kub3F.mjs");
|
|
115
|
-
results = applyRecencyBoost(results, recencyDays);
|
|
116
|
-
}
|
|
117
|
-
const withSlugs = await populateSlugs(results, registry);
|
|
118
|
-
if (withSlugs.length === 0) return { content: [{
|
|
119
|
-
type: "text",
|
|
120
|
-
text: `No results found for query: "${params.query}" (mode: ${mode})`
|
|
121
|
-
}] };
|
|
122
|
-
const rerankLabel = shouldRerank ? " +rerank" : "";
|
|
123
|
-
const useCompact = params.format === "compact";
|
|
124
|
-
const formatted = withSlugs.map((r, i) => {
|
|
125
|
-
const slug = r.projectSlug ?? `project:${r.projectId}`;
|
|
126
|
-
const idPart = r.chunkId ? ` id=${r.chunkId}` : "";
|
|
127
|
-
if (useCompact) return `[${i + 1}]${idPart} ${slug} — ${r.path} L${r.startLine}-${r.endLine} score=${r.score.toFixed(3)}`;
|
|
128
|
-
const header = `[${i + 1}]${idPart} ${slug} — ${r.path} (lines ${r.startLine}-${r.endLine}) score=${r.score.toFixed(4)} tier=${r.tier} source=${r.source}`;
|
|
129
|
-
const raw = r.snippet.trim();
|
|
130
|
-
return `${header}\n${raw.length > snippetLength ? raw.slice(0, snippetLength) + "..." : raw}`;
|
|
131
|
-
}).join(useCompact ? "\n" : "\n\n---\n\n");
|
|
132
|
-
try {
|
|
133
|
-
const { saveQueryResult } = await import("./query-feedback-B_iigYj-.mjs");
|
|
134
|
-
saveQueryResult({
|
|
135
|
-
query: params.query,
|
|
136
|
-
timestamp: Date.now(),
|
|
137
|
-
source: "memory_search",
|
|
138
|
-
sourceSlugs: withSlugs.slice(0, 5).map((r) => r.path),
|
|
139
|
-
answerPreview: withSlugs.slice(0, 3).map((r) => r.snippet.trim().slice(0, 150)).join(" | "),
|
|
140
|
-
resultCount: withSlugs.length
|
|
141
|
-
});
|
|
142
|
-
} catch {}
|
|
143
|
-
return { content: [{
|
|
144
|
-
type: "text",
|
|
145
|
-
text: `Found ${withSlugs.length} result(s) for "${params.query}" (mode: ${mode}${rerankLabel}):\n\n${formatted}`
|
|
146
|
-
}] };
|
|
147
|
-
} catch (e) {
|
|
148
|
-
return {
|
|
149
|
-
content: [{
|
|
150
|
-
type: "text",
|
|
151
|
-
text: `Search error: ${String(e)}`
|
|
152
|
-
}],
|
|
153
|
-
isError: true
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Resolve `project` + `path` to a readable file inside the project root.
|
|
159
|
-
* Shared by memory_get and memory_outline so both apply the same path and
|
|
160
|
-
* access checks. Returns a ToolResult on any failure.
|
|
161
|
-
*/
|
|
162
|
-
async function resolveProjectFile(registry, projectSlug, requestedPath) {
|
|
163
|
-
const fail = (text) => ({
|
|
164
|
-
content: [{
|
|
165
|
-
type: "text",
|
|
166
|
-
text
|
|
167
|
-
}],
|
|
168
|
-
isError: true
|
|
169
|
-
});
|
|
170
|
-
const projectId = await lookupProjectId(registry, projectSlug);
|
|
171
|
-
if (projectId == null) return fail(`Project not found: ${projectSlug}`);
|
|
172
|
-
const project = await registry.getProjectById(projectId);
|
|
173
|
-
if (!project) return fail(`Project not found: ${projectSlug}`);
|
|
174
|
-
if (requestedPath.includes("..") || isAbsolute(requestedPath)) return fail(`Invalid path: ${requestedPath} (must be a relative path within the project root, no ../ allowed)`);
|
|
175
|
-
const fullPath = join(project.root_path, requestedPath);
|
|
176
|
-
const resolvedFull = resolve(fullPath);
|
|
177
|
-
const resolvedRoot = resolve(project.root_path);
|
|
178
|
-
if (!resolvedFull.startsWith(resolvedRoot + "/") && resolvedFull !== resolvedRoot) return fail(`Path traversal blocked: ${requestedPath}`);
|
|
179
|
-
if (!existsSync(fullPath)) return fail(`File not found: ${requestedPath} (project: ${projectSlug})`);
|
|
180
|
-
const stat = statSync(fullPath);
|
|
181
|
-
if (stat.size > 5 * 1024 * 1024) return { content: [{
|
|
182
|
-
type: "text",
|
|
183
|
-
text: `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Maximum 5 MB.`
|
|
184
|
-
}] };
|
|
185
|
-
return { fullPath };
|
|
186
|
-
}
|
|
187
|
-
async function toolMemoryGet(registry, params) {
|
|
188
|
-
try {
|
|
189
|
-
const resolved = await resolveProjectFile(registry, params.project, params.path);
|
|
190
|
-
if ("content" in resolved) return resolved;
|
|
191
|
-
const { fullPath } = resolved;
|
|
192
|
-
const requestedPath = params.path;
|
|
193
|
-
const allLines = readFileSync(fullPath, "utf8").split("\n");
|
|
194
|
-
const fromLine = (params.from ?? 1) - 1;
|
|
195
|
-
const toLine = params.lines != null ? Math.min(fromLine + params.lines, allLines.length) : allLines.length;
|
|
196
|
-
const text = allLines.slice(fromLine, toLine).join("\n");
|
|
197
|
-
return { content: [{
|
|
198
|
-
type: "text",
|
|
199
|
-
text: `${params.from != null ? `${params.project}/${requestedPath} (lines ${fromLine + 1}-${toLine}):` : `${params.project}/${requestedPath}:`}\n\n${text}`
|
|
200
|
-
}] };
|
|
201
|
-
} catch (e) {
|
|
202
|
-
return {
|
|
203
|
-
content: [{
|
|
204
|
-
type: "text",
|
|
205
|
-
text: `Read error: ${String(e)}`
|
|
206
|
-
}],
|
|
207
|
-
isError: true
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
function renderOutline(nodes, maxDepth, depth = 0, out = []) {
|
|
212
|
-
for (const n of nodes) {
|
|
213
|
-
out.push(`${" ".repeat(depth)}${"#".repeat(n.level)} ${n.title} L${n.startLine}-${n.endLine} ~${n.tokens}t`);
|
|
214
|
-
if (depth + 1 < maxDepth) renderOutline(n.children, maxDepth, depth + 1, out);
|
|
215
|
-
}
|
|
216
|
-
return out;
|
|
217
|
-
}
|
|
218
|
-
async function toolMemoryOutline(registry, params) {
|
|
219
|
-
try {
|
|
220
|
-
const resolved = await resolveProjectFile(registry, params.project, params.path);
|
|
221
|
-
if ("content" in resolved) return resolved;
|
|
222
|
-
const outline = buildOutline(readFileSync(resolved.fullPath, "utf8"));
|
|
223
|
-
const body = outline.length ? renderOutline(outline, params.max_depth ?? Infinity).join("\n") : "(no headings)";
|
|
224
|
-
return { content: [{
|
|
225
|
-
type: "text",
|
|
226
|
-
text: `${params.project}/${params.path} outline:\n\n${body}`
|
|
227
|
-
}] };
|
|
228
|
-
} catch (e) {
|
|
229
|
-
return {
|
|
230
|
-
content: [{
|
|
231
|
-
type: "text",
|
|
232
|
-
text: `Read error: ${String(e)}`
|
|
233
|
-
}],
|
|
234
|
-
isError: true
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Combine keyword + semantic results using min-max normalized scoring.
|
|
240
|
-
* Mirrors the logic in searchMemoryHybrid() from memory/search.ts,
|
|
241
|
-
* but works on pre-computed result arrays so it works for any backend.
|
|
242
|
-
*/
|
|
243
|
-
function combineHybridResults(keywordResults, semanticResults, maxResults, keywordWeight = .5, semanticWeight = .5) {
|
|
244
|
-
if (keywordResults.length === 0 && semanticResults.length === 0) return [];
|
|
245
|
-
const keyFor = (r) => `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;
|
|
246
|
-
function minMaxNormalize(items) {
|
|
247
|
-
if (items.length === 0) return /* @__PURE__ */ new Map();
|
|
248
|
-
const min = Math.min(...items.map((r) => r.score));
|
|
249
|
-
const range = Math.max(...items.map((r) => r.score)) - min;
|
|
250
|
-
const m = /* @__PURE__ */ new Map();
|
|
251
|
-
for (const r of items) m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);
|
|
252
|
-
return m;
|
|
253
|
-
}
|
|
254
|
-
const kwNorm = minMaxNormalize(keywordResults);
|
|
255
|
-
const semNorm = minMaxNormalize(semanticResults);
|
|
256
|
-
const allKeys = new Set([...keywordResults.map(keyFor), ...semanticResults.map(keyFor)]);
|
|
257
|
-
const metaMap = /* @__PURE__ */ new Map();
|
|
258
|
-
for (const r of [...keywordResults, ...semanticResults]) metaMap.set(keyFor(r), r);
|
|
259
|
-
const combined = [];
|
|
260
|
-
for (const key of allKeys) {
|
|
261
|
-
const meta = metaMap.get(key);
|
|
262
|
-
const kwScore = kwNorm.get(key) ?? 0;
|
|
263
|
-
const semScore = semNorm.get(key) ?? 0;
|
|
264
|
-
const combinedScore = keywordWeight * kwScore + semanticWeight * semScore;
|
|
265
|
-
combined.push({
|
|
266
|
-
...meta,
|
|
267
|
-
score: combinedScore,
|
|
268
|
-
combinedScore
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
return combined.sort((a, b) => b.score - a.score).slice(0, maxResults).map(({ combinedScore: _unused, ...r }) => r);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
//#endregion
|
|
275
|
-
//#region src/mcp/tools/projects.ts
|
|
276
|
-
/**
|
|
277
|
-
* MCP tool handlers: project_info, project_list, project_detect,
|
|
278
|
-
* project_health, project_todo
|
|
279
|
-
*/
|
|
280
|
-
async function toolProjectInfo(registry, params) {
|
|
281
|
-
try {
|
|
282
|
-
let project = null;
|
|
283
|
-
if (params.slug) {
|
|
284
|
-
const projectId = await lookupProjectId(registry, params.slug);
|
|
285
|
-
if (projectId != null) project = await registry.getProjectById(projectId);
|
|
286
|
-
} else project = await detectProjectFromPath(registry, process.cwd());
|
|
287
|
-
if (!project) return {
|
|
288
|
-
content: [{
|
|
289
|
-
type: "text",
|
|
290
|
-
text: params.slug ? `Project not found: ${params.slug}` : `No PAI project found matching the current directory: ${process.cwd()}`
|
|
291
|
-
}],
|
|
292
|
-
isError: !params.slug
|
|
293
|
-
};
|
|
294
|
-
return { content: [{
|
|
295
|
-
type: "text",
|
|
296
|
-
text: await formatProject(registry, project)
|
|
297
|
-
}] };
|
|
298
|
-
} catch (e) {
|
|
299
|
-
return {
|
|
300
|
-
content: [{
|
|
301
|
-
type: "text",
|
|
302
|
-
text: `project_info error: ${String(e)}`
|
|
303
|
-
}],
|
|
304
|
-
isError: true
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
async function toolProjectList(registry, params) {
|
|
309
|
-
try {
|
|
310
|
-
const limit = params.limit ?? 50;
|
|
311
|
-
const projects = await registry.listProjects({
|
|
312
|
-
status: params.status,
|
|
313
|
-
tagName: params.tag,
|
|
314
|
-
orderBy: "updated_desc",
|
|
315
|
-
limit
|
|
316
|
-
});
|
|
317
|
-
if (projects.length === 0) return { content: [{
|
|
318
|
-
type: "text",
|
|
319
|
-
text: "No projects found matching the given filters."
|
|
320
|
-
}] };
|
|
321
|
-
const lines = projects.map((p) => `${p.slug} [${p.status}] ${p.root_path} (updated: ${new Date(p.updated_at).toISOString().slice(0, 10)})`);
|
|
322
|
-
return { content: [{
|
|
323
|
-
type: "text",
|
|
324
|
-
text: `${projects.length} project(s):\n\n${lines.join("\n")}`
|
|
325
|
-
}] };
|
|
326
|
-
} catch (e) {
|
|
327
|
-
return {
|
|
328
|
-
content: [{
|
|
329
|
-
type: "text",
|
|
330
|
-
text: `project_list error: ${String(e)}`
|
|
331
|
-
}],
|
|
332
|
-
isError: true
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
async function toolProjectDetect(params) {
|
|
337
|
-
try {
|
|
338
|
-
const detection = await detectProject(params.cwd);
|
|
339
|
-
if (!detection) return { content: [{
|
|
340
|
-
type: "text",
|
|
341
|
-
text: `No registered project found for path: ${params.cwd ?? process.cwd()}\n\nRun 'pai project add .' to register this directory.`
|
|
342
|
-
}] };
|
|
343
|
-
return { content: [{
|
|
344
|
-
type: "text",
|
|
345
|
-
text: formatDetectionJson(detection)
|
|
346
|
-
}] };
|
|
347
|
-
} catch (e) {
|
|
348
|
-
return {
|
|
349
|
-
content: [{
|
|
350
|
-
type: "text",
|
|
351
|
-
text: `project_detect error: ${String(e)}`
|
|
352
|
-
}],
|
|
353
|
-
isError: true
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
async function toolProjectHealth(registry, params) {
|
|
358
|
-
try {
|
|
359
|
-
const { existsSync: fsExists, readdirSync, statSync } = await import("node:fs");
|
|
360
|
-
const { join: pathJoin, basename: pathBasename } = await import("node:path");
|
|
361
|
-
const { homedir } = await import("node:os");
|
|
362
|
-
const { encodeDir: enc } = await import("./utils-BrX9FLeK.mjs");
|
|
363
|
-
const rows = (await registry.listProjectsWithSessionStats()).slice().sort((a, b) => a.slug.localeCompare(b.slug));
|
|
364
|
-
const home = homedir();
|
|
365
|
-
const claudeProjects = pathJoin(home, ".claude", "projects");
|
|
366
|
-
function suggestMoved(rootPath) {
|
|
367
|
-
const name = pathBasename(rootPath);
|
|
368
|
-
return [
|
|
369
|
-
pathJoin(home, "dev", name),
|
|
370
|
-
pathJoin(home, "dev", "ai", name),
|
|
371
|
-
pathJoin(home, "Desktop", name),
|
|
372
|
-
pathJoin(home, "Projects", name)
|
|
373
|
-
].find((c) => fsExists(c));
|
|
374
|
-
}
|
|
375
|
-
function hasClaudeNotes(encodedDir) {
|
|
376
|
-
if (!fsExists(claudeProjects)) return false;
|
|
377
|
-
try {
|
|
378
|
-
for (const entry of readdirSync(claudeProjects)) {
|
|
379
|
-
if (entry !== encodedDir && !entry.startsWith(encodedDir)) continue;
|
|
380
|
-
const full = pathJoin(claudeProjects, entry);
|
|
381
|
-
try {
|
|
382
|
-
if (!statSync(full).isDirectory()) continue;
|
|
383
|
-
} catch {
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (fsExists(pathJoin(full, "Notes"))) return true;
|
|
387
|
-
}
|
|
388
|
-
} catch {}
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
function findTodoForProject(rootPath) {
|
|
392
|
-
for (const rel of [
|
|
393
|
-
"Notes/TODO.md",
|
|
394
|
-
".claude/Notes/TODO.md",
|
|
395
|
-
"tasks/todo.md",
|
|
396
|
-
"TODO.md"
|
|
397
|
-
]) {
|
|
398
|
-
const full = pathJoin(rootPath, rel);
|
|
399
|
-
if (fsExists(full)) try {
|
|
400
|
-
const raw = readFileSync(full, "utf8");
|
|
401
|
-
return {
|
|
402
|
-
found: true,
|
|
403
|
-
path: rel,
|
|
404
|
-
has_continue: /^## Continue$/m.test(raw)
|
|
405
|
-
};
|
|
406
|
-
} catch {
|
|
407
|
-
return {
|
|
408
|
-
found: true,
|
|
409
|
-
path: rel,
|
|
410
|
-
has_continue: false
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return {
|
|
415
|
-
found: false,
|
|
416
|
-
path: null,
|
|
417
|
-
has_continue: false
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
const results = rows.map((p) => {
|
|
421
|
-
const pathExists = fsExists(p.root_path);
|
|
422
|
-
let health;
|
|
423
|
-
let suggestedPath = null;
|
|
424
|
-
if (pathExists) health = "active";
|
|
425
|
-
else {
|
|
426
|
-
suggestedPath = suggestMoved(p.root_path) ?? null;
|
|
427
|
-
health = suggestedPath ? "stale" : "dead";
|
|
428
|
-
}
|
|
429
|
-
const todo = pathExists ? findTodoForProject(p.root_path) : {
|
|
430
|
-
found: false,
|
|
431
|
-
path: null,
|
|
432
|
-
has_continue: false
|
|
433
|
-
};
|
|
434
|
-
return {
|
|
435
|
-
slug: p.slug,
|
|
436
|
-
display_name: p.display_name,
|
|
437
|
-
root_path: p.root_path,
|
|
438
|
-
status: p.status,
|
|
439
|
-
type: p.type,
|
|
440
|
-
session_count: p.session_count,
|
|
441
|
-
health,
|
|
442
|
-
suggested_path: suggestedPath,
|
|
443
|
-
has_claude_notes: hasClaudeNotes(p.encoded_dir),
|
|
444
|
-
todo
|
|
445
|
-
};
|
|
446
|
-
});
|
|
447
|
-
const filtered = !params.category || params.category === "all" ? results : results.filter((r) => r.health === params.category);
|
|
448
|
-
const summary = {
|
|
449
|
-
total: rows.length,
|
|
450
|
-
active: results.filter((r) => r.health === "active").length,
|
|
451
|
-
stale: results.filter((r) => r.health === "stale").length,
|
|
452
|
-
dead: results.filter((r) => r.health === "dead").length
|
|
453
|
-
};
|
|
454
|
-
return { content: [{
|
|
455
|
-
type: "text",
|
|
456
|
-
text: JSON.stringify({
|
|
457
|
-
summary,
|
|
458
|
-
projects: filtered
|
|
459
|
-
}, null, 2)
|
|
460
|
-
}] };
|
|
461
|
-
} catch (e) {
|
|
462
|
-
return {
|
|
463
|
-
content: [{
|
|
464
|
-
type: "text",
|
|
465
|
-
text: `project_health error: ${String(e)}`
|
|
466
|
-
}],
|
|
467
|
-
isError: true
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
/**
|
|
472
|
-
* TODO candidate locations searched in priority order.
|
|
473
|
-
* Returns the first one that exists, along with its label.
|
|
474
|
-
*/
|
|
475
|
-
const TODO_LOCATIONS = [
|
|
476
|
-
{
|
|
477
|
-
rel: "Notes/TODO.md",
|
|
478
|
-
label: "Notes/TODO.md"
|
|
479
|
-
},
|
|
480
|
-
{
|
|
481
|
-
rel: ".claude/Notes/TODO.md",
|
|
482
|
-
label: ".claude/Notes/TODO.md"
|
|
483
|
-
},
|
|
484
|
-
{
|
|
485
|
-
rel: "tasks/todo.md",
|
|
486
|
-
label: "tasks/todo.md"
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
rel: "TODO.md",
|
|
490
|
-
label: "TODO.md"
|
|
491
|
-
}
|
|
492
|
-
];
|
|
493
|
-
/**
|
|
494
|
-
* Given TODO file content, extract and surface the ## Continue section first,
|
|
495
|
-
* then return the remaining content. Returns an object with:
|
|
496
|
-
* continueSection: string | null
|
|
497
|
-
* fullContent: string
|
|
498
|
-
* hasContinue: boolean
|
|
499
|
-
*/
|
|
500
|
-
function parseTodoContent(raw) {
|
|
501
|
-
const lines = raw.split("\n");
|
|
502
|
-
const continueIdx = lines.findIndex((l) => l.trim() === "## Continue");
|
|
503
|
-
if (continueIdx === -1) return {
|
|
504
|
-
continueSection: null,
|
|
505
|
-
fullContent: raw,
|
|
506
|
-
hasContinue: false
|
|
507
|
-
};
|
|
508
|
-
let endIdx = lines.length;
|
|
509
|
-
for (let i = continueIdx + 1; i < lines.length; i++) {
|
|
510
|
-
const trimmed = lines[i].trim();
|
|
511
|
-
if (trimmed === "---" || trimmed.startsWith("##") && trimmed !== "## Continue") {
|
|
512
|
-
endIdx = i;
|
|
513
|
-
break;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return {
|
|
517
|
-
continueSection: lines.slice(continueIdx, endIdx).join("\n").trim(),
|
|
518
|
-
fullContent: raw,
|
|
519
|
-
hasContinue: true
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
async function toolProjectTodo(registry, params) {
|
|
523
|
-
try {
|
|
524
|
-
let rootPath;
|
|
525
|
-
let projectSlug;
|
|
526
|
-
if (params.project) {
|
|
527
|
-
const projectId = await lookupProjectId(registry, params.project);
|
|
528
|
-
if (projectId == null) return {
|
|
529
|
-
content: [{
|
|
530
|
-
type: "text",
|
|
531
|
-
text: `Project not found: ${params.project}`
|
|
532
|
-
}],
|
|
533
|
-
isError: true
|
|
534
|
-
};
|
|
535
|
-
const row = await registry.getProjectById(projectId);
|
|
536
|
-
if (!row) return {
|
|
537
|
-
content: [{
|
|
538
|
-
type: "text",
|
|
539
|
-
text: `Project not found: ${params.project}`
|
|
540
|
-
}],
|
|
541
|
-
isError: true
|
|
542
|
-
};
|
|
543
|
-
rootPath = row.root_path;
|
|
544
|
-
projectSlug = row.slug;
|
|
545
|
-
} else {
|
|
546
|
-
const project = await detectProjectFromPath(registry, process.cwd());
|
|
547
|
-
if (!project) return { content: [{
|
|
548
|
-
type: "text",
|
|
549
|
-
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.`
|
|
550
|
-
}] };
|
|
551
|
-
rootPath = project.root_path;
|
|
552
|
-
projectSlug = project.slug;
|
|
553
|
-
}
|
|
554
|
-
for (const loc of TODO_LOCATIONS) {
|
|
555
|
-
const fullPath = join(rootPath, loc.rel);
|
|
556
|
-
if (existsSync(fullPath)) {
|
|
557
|
-
const { continueSection, fullContent, hasContinue } = parseTodoContent(readFileSync(fullPath, "utf8"));
|
|
558
|
-
let output;
|
|
559
|
-
if (hasContinue && continueSection) output = [
|
|
560
|
-
`TODO found: ${projectSlug}/${loc.label}`,
|
|
561
|
-
"",
|
|
562
|
-
"=== CONTINUE SECTION (surfaced first) ===",
|
|
563
|
-
continueSection,
|
|
564
|
-
"",
|
|
565
|
-
"=== FULL TODO CONTENT ===",
|
|
566
|
-
fullContent
|
|
567
|
-
].join("\n");
|
|
568
|
-
else output = [
|
|
569
|
-
`TODO found: ${projectSlug}/${loc.label}`,
|
|
570
|
-
"",
|
|
571
|
-
fullContent
|
|
572
|
-
].join("\n");
|
|
573
|
-
return { content: [{
|
|
574
|
-
type: "text",
|
|
575
|
-
text: output
|
|
576
|
-
}] };
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
const searched = TODO_LOCATIONS.map((l) => ` ${rootPath}/${l.rel}`).join("\n");
|
|
580
|
-
return { content: [{
|
|
581
|
-
type: "text",
|
|
582
|
-
text: [
|
|
583
|
-
`No TODO.md found for project: ${projectSlug}`,
|
|
584
|
-
"",
|
|
585
|
-
"Searched locations (in order):",
|
|
586
|
-
searched,
|
|
587
|
-
"",
|
|
588
|
-
"Create a TODO with: echo '## Tasks\\n- [ ] First task' > Notes/TODO.md"
|
|
589
|
-
].join("\n")
|
|
590
|
-
}] };
|
|
591
|
-
} catch (e) {
|
|
592
|
-
return {
|
|
593
|
-
content: [{
|
|
594
|
-
type: "text",
|
|
595
|
-
text: `project_todo error: ${String(e)}`
|
|
596
|
-
}],
|
|
597
|
-
isError: true
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
//#endregion
|
|
603
|
-
//#region src/mcp/tools/sessions.ts
|
|
604
|
-
async function toolSessionList(registry, params) {
|
|
605
|
-
try {
|
|
606
|
-
const projectId = await lookupProjectId(registry, params.project);
|
|
607
|
-
if (projectId == null) return {
|
|
608
|
-
content: [{
|
|
609
|
-
type: "text",
|
|
610
|
-
text: `Project not found: ${params.project}`
|
|
611
|
-
}],
|
|
612
|
-
isError: true
|
|
613
|
-
};
|
|
614
|
-
const limit = params.limit ?? 10;
|
|
615
|
-
const sessions = await registry.listSessions({
|
|
616
|
-
projectId,
|
|
617
|
-
status: params.status,
|
|
618
|
-
limit
|
|
619
|
-
});
|
|
620
|
-
if (sessions.length === 0) return { content: [{
|
|
621
|
-
type: "text",
|
|
622
|
-
text: `No sessions found for project: ${params.project}`
|
|
623
|
-
}] };
|
|
624
|
-
const lines = sessions.map((s) => `#${String(s.number).padStart(4, "0")} ${s.date} [${s.status}] ${s.title}\n file: Notes/${s.filename}`);
|
|
625
|
-
return { content: [{
|
|
626
|
-
type: "text",
|
|
627
|
-
text: `${sessions.length} session(s) for ${params.project}:\n\n${lines.join("\n\n")}`
|
|
628
|
-
}] };
|
|
629
|
-
} catch (e) {
|
|
630
|
-
return {
|
|
631
|
-
content: [{
|
|
632
|
-
type: "text",
|
|
633
|
-
text: `session_list error: ${String(e)}`
|
|
634
|
-
}],
|
|
635
|
-
isError: true
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
/**
|
|
640
|
-
* Automatically suggest which project a session belongs to.
|
|
641
|
-
*
|
|
642
|
-
* Strategy (in priority order):
|
|
643
|
-
* 1. path — exact or parent-directory match in the project registry
|
|
644
|
-
* 2. marker — walk up from cwd looking for Notes/PAI.md
|
|
645
|
-
* 3. topic — BM25 keyword search against memory (requires context)
|
|
646
|
-
*
|
|
647
|
-
* Call this at session start (e.g., from CLAUDE.md or a session-start hook)
|
|
648
|
-
* to automatically route the session to the correct project.
|
|
649
|
-
*/
|
|
650
|
-
async function toolSessionRoute(registry, federation, params) {
|
|
651
|
-
try {
|
|
652
|
-
const { autoRoute, formatAutoRouteJson } = await import("./auto-route-o0BOsXn7.mjs");
|
|
653
|
-
const result = await autoRoute(registry, federation, params.cwd, params.context);
|
|
654
|
-
if (!result) return { content: [{
|
|
655
|
-
type: "text",
|
|
656
|
-
text: [
|
|
657
|
-
`No project match found for: ${params.cwd ?? process.cwd()}`,
|
|
658
|
-
"",
|
|
659
|
-
"Tried: path match, PAI.md marker walk" + (params.context ? ", topic detection" : ""),
|
|
660
|
-
"",
|
|
661
|
-
"Run 'pai project add .' to register this directory,",
|
|
662
|
-
"or provide conversation context for topic-based routing."
|
|
663
|
-
].join("\n")
|
|
664
|
-
}] };
|
|
665
|
-
return { content: [{
|
|
666
|
-
type: "text",
|
|
667
|
-
text: formatAutoRouteJson(result)
|
|
668
|
-
}] };
|
|
669
|
-
} catch (e) {
|
|
670
|
-
return {
|
|
671
|
-
content: [{
|
|
672
|
-
type: "text",
|
|
673
|
-
text: `session_route error: ${String(e)}`
|
|
674
|
-
}],
|
|
675
|
-
isError: true
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
//#endregion
|
|
681
|
-
//#region src/mcp/tools/registry.ts
|
|
682
|
-
async function toolRegistrySearch(registry, params) {
|
|
683
|
-
try {
|
|
684
|
-
const projects = await registry.searchProjects(params.query, 20);
|
|
685
|
-
if (projects.length === 0) return { content: [{
|
|
686
|
-
type: "text",
|
|
687
|
-
text: `No projects found matching: "${params.query}"`
|
|
688
|
-
}] };
|
|
689
|
-
const lines = projects.map((p) => `${p.slug} [${p.status}] ${p.root_path}`);
|
|
690
|
-
return { content: [{
|
|
691
|
-
type: "text",
|
|
692
|
-
text: `${projects.length} match(es) for "${params.query}":\n\n${lines.join("\n")}`
|
|
693
|
-
}] };
|
|
694
|
-
} catch (e) {
|
|
695
|
-
return {
|
|
696
|
-
content: [{
|
|
697
|
-
type: "text",
|
|
698
|
-
text: `registry_search error: ${String(e)}`
|
|
699
|
-
}],
|
|
700
|
-
isError: true
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
//#endregion
|
|
706
|
-
//#region src/mcp/tools/zettel.ts
|
|
707
|
-
async function toolZettelExplore(backend, params) {
|
|
708
|
-
try {
|
|
709
|
-
const { zettelExplore } = await import("./zettelkasten-CtGHQWPU.mjs");
|
|
710
|
-
const result = await zettelExplore(backend, {
|
|
711
|
-
startNote: params.start_note,
|
|
712
|
-
depth: params.depth,
|
|
713
|
-
direction: params.direction,
|
|
714
|
-
mode: params.mode
|
|
715
|
-
});
|
|
716
|
-
return { content: [{
|
|
717
|
-
type: "text",
|
|
718
|
-
text: JSON.stringify(result, null, 2)
|
|
719
|
-
}] };
|
|
720
|
-
} catch (e) {
|
|
721
|
-
return {
|
|
722
|
-
content: [{
|
|
723
|
-
type: "text",
|
|
724
|
-
text: `zettel_explore error: ${String(e)}`
|
|
725
|
-
}],
|
|
726
|
-
isError: true
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
async function toolZettelHealth(backend, params) {
|
|
731
|
-
try {
|
|
732
|
-
const { zettelHealth } = await import("./zettelkasten-CtGHQWPU.mjs");
|
|
733
|
-
const result = await zettelHealth(backend, {
|
|
734
|
-
scope: params.scope,
|
|
735
|
-
projectPath: params.project_path,
|
|
736
|
-
recentDays: params.recent_days,
|
|
737
|
-
include: params.include
|
|
738
|
-
});
|
|
739
|
-
return { content: [{
|
|
740
|
-
type: "text",
|
|
741
|
-
text: JSON.stringify(result, null, 2)
|
|
742
|
-
}] };
|
|
743
|
-
} catch (e) {
|
|
744
|
-
return {
|
|
745
|
-
content: [{
|
|
746
|
-
type: "text",
|
|
747
|
-
text: `zettel_health error: ${String(e)}`
|
|
748
|
-
}],
|
|
749
|
-
isError: true
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
async function toolZettelSurprise(backend, params) {
|
|
754
|
-
try {
|
|
755
|
-
const { zettelSurprise } = await import("./zettelkasten-CtGHQWPU.mjs");
|
|
756
|
-
const results = await zettelSurprise(backend, {
|
|
757
|
-
referencePath: params.reference_path,
|
|
758
|
-
vaultProjectId: params.vault_project_id,
|
|
759
|
-
limit: params.limit,
|
|
760
|
-
minSimilarity: params.min_similarity,
|
|
761
|
-
minGraphDistance: params.min_graph_distance
|
|
762
|
-
});
|
|
763
|
-
return { content: [{
|
|
764
|
-
type: "text",
|
|
765
|
-
text: JSON.stringify(results, null, 2)
|
|
766
|
-
}] };
|
|
767
|
-
} catch (e) {
|
|
768
|
-
return {
|
|
769
|
-
content: [{
|
|
770
|
-
type: "text",
|
|
771
|
-
text: `zettel_surprise error: ${String(e)}`
|
|
772
|
-
}],
|
|
773
|
-
isError: true
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
async function toolZettelSuggest(backend, params) {
|
|
778
|
-
try {
|
|
779
|
-
const { zettelSuggest } = await import("./zettelkasten-CtGHQWPU.mjs");
|
|
780
|
-
const results = await zettelSuggest(backend, {
|
|
781
|
-
notePath: params.note_path,
|
|
782
|
-
vaultProjectId: params.vault_project_id,
|
|
783
|
-
limit: params.limit,
|
|
784
|
-
excludeLinked: params.exclude_linked
|
|
785
|
-
});
|
|
786
|
-
return { content: [{
|
|
787
|
-
type: "text",
|
|
788
|
-
text: JSON.stringify(results, null, 2)
|
|
789
|
-
}] };
|
|
790
|
-
} catch (e) {
|
|
791
|
-
return {
|
|
792
|
-
content: [{
|
|
793
|
-
type: "text",
|
|
794
|
-
text: `zettel_suggest error: ${String(e)}`
|
|
795
|
-
}],
|
|
796
|
-
isError: true
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
async function toolZettelConverse(backend, params) {
|
|
801
|
-
try {
|
|
802
|
-
const { zettelConverse } = await import("./zettelkasten-CtGHQWPU.mjs");
|
|
803
|
-
const result = await zettelConverse(backend, {
|
|
804
|
-
question: params.question,
|
|
805
|
-
vaultProjectId: params.vault_project_id,
|
|
806
|
-
depth: params.depth,
|
|
807
|
-
limit: params.limit
|
|
808
|
-
});
|
|
809
|
-
try {
|
|
810
|
-
const { saveQueryResult } = await import("./query-feedback-B_iigYj-.mjs");
|
|
811
|
-
saveQueryResult({
|
|
812
|
-
query: params.question,
|
|
813
|
-
timestamp: Date.now(),
|
|
814
|
-
source: "zettel_converse",
|
|
815
|
-
sourceSlugs: result.relevantNotes.slice(0, 5).map((n) => n.path),
|
|
816
|
-
answerPreview: result.relevantNotes.slice(0, 3).map((n) => {
|
|
817
|
-
return `${n.title ?? "(untitled)"}: ${n.snippet.trim().slice(0, 100)}`;
|
|
818
|
-
}).join(" | "),
|
|
819
|
-
resultCount: result.relevantNotes.length
|
|
820
|
-
});
|
|
821
|
-
} catch {}
|
|
822
|
-
return { content: [{
|
|
823
|
-
type: "text",
|
|
824
|
-
text: JSON.stringify(result, null, 2)
|
|
825
|
-
}] };
|
|
826
|
-
} catch (e) {
|
|
827
|
-
return {
|
|
828
|
-
content: [{
|
|
829
|
-
type: "text",
|
|
830
|
-
text: `zettel_converse error: ${String(e)}`
|
|
831
|
-
}],
|
|
832
|
-
isError: true
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
async function toolZettelThemes(backend, params) {
|
|
837
|
-
try {
|
|
838
|
-
const { zettelThemes } = await import("./zettelkasten-CtGHQWPU.mjs");
|
|
839
|
-
const result = await zettelThemes(backend, {
|
|
840
|
-
vaultProjectId: params.vault_project_id,
|
|
841
|
-
lookbackDays: params.lookback_days,
|
|
842
|
-
minClusterSize: params.min_cluster_size,
|
|
843
|
-
maxThemes: params.max_themes,
|
|
844
|
-
similarityThreshold: params.similarity_threshold
|
|
845
|
-
});
|
|
846
|
-
return { content: [{
|
|
847
|
-
type: "text",
|
|
848
|
-
text: JSON.stringify(result, null, 2)
|
|
849
|
-
}] };
|
|
850
|
-
} catch (e) {
|
|
851
|
-
return {
|
|
852
|
-
content: [{
|
|
853
|
-
type: "text",
|
|
854
|
-
text: `zettel_themes error: ${String(e)}`
|
|
855
|
-
}],
|
|
856
|
-
isError: true
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
async function toolZettelGodNotes(backend, params) {
|
|
861
|
-
try {
|
|
862
|
-
const { zettelGodNotes } = await import("./zettelkasten-CtGHQWPU.mjs");
|
|
863
|
-
const result = await zettelGodNotes(backend, {
|
|
864
|
-
limit: params.limit,
|
|
865
|
-
minInbound: params.min_inbound
|
|
866
|
-
});
|
|
867
|
-
return { content: [{
|
|
868
|
-
type: "text",
|
|
869
|
-
text: JSON.stringify(result, null, 2)
|
|
870
|
-
}] };
|
|
871
|
-
} catch (e) {
|
|
872
|
-
return {
|
|
873
|
-
content: [{
|
|
874
|
-
type: "text",
|
|
875
|
-
text: `zettel_god_notes error: ${String(e)}`
|
|
876
|
-
}],
|
|
877
|
-
isError: true
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
async function toolZettelCommunities(backend, params) {
|
|
882
|
-
try {
|
|
883
|
-
const { zettelCommunities } = await import("./zettelkasten-CtGHQWPU.mjs");
|
|
884
|
-
const result = await zettelCommunities(backend, {
|
|
885
|
-
minSize: params.min_size,
|
|
886
|
-
maxCommunities: params.max_communities,
|
|
887
|
-
resolution: params.resolution
|
|
888
|
-
});
|
|
889
|
-
return { content: [{
|
|
890
|
-
type: "text",
|
|
891
|
-
text: JSON.stringify(result, null, 2)
|
|
892
|
-
}] };
|
|
893
|
-
} catch (e) {
|
|
894
|
-
return {
|
|
895
|
-
content: [{
|
|
896
|
-
type: "text",
|
|
897
|
-
text: `zettel_communities error: ${String(e)}`
|
|
898
|
-
}],
|
|
899
|
-
isError: true
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
//#endregion
|
|
905
|
-
//#region src/mcp/tools/kg.ts
|
|
906
|
-
async function toolKgAdd(backend, params) {
|
|
907
|
-
try {
|
|
908
|
-
if (!params.subject || !params.predicate || !params.object) return {
|
|
909
|
-
content: [{
|
|
910
|
-
type: "text",
|
|
911
|
-
text: "kg_add error: subject, predicate, and object are required"
|
|
912
|
-
}],
|
|
913
|
-
isError: true
|
|
914
|
-
};
|
|
915
|
-
const triple = await backend.addKgTriple(params);
|
|
916
|
-
return { content: [{
|
|
917
|
-
type: "text",
|
|
918
|
-
text: JSON.stringify(triple, null, 2)
|
|
919
|
-
}] };
|
|
920
|
-
} catch (e) {
|
|
921
|
-
return {
|
|
922
|
-
content: [{
|
|
923
|
-
type: "text",
|
|
924
|
-
text: `kg_add error: ${String(e)}`
|
|
925
|
-
}],
|
|
926
|
-
isError: true
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
async function toolKgQuery(backend, params) {
|
|
931
|
-
try {
|
|
932
|
-
const asOf = params.as_of ? new Date(params.as_of) : void 0;
|
|
933
|
-
if (asOf && isNaN(asOf.getTime())) return {
|
|
934
|
-
content: [{
|
|
935
|
-
type: "text",
|
|
936
|
-
text: `kg_query error: invalid as_of date: ${params.as_of}`
|
|
937
|
-
}],
|
|
938
|
-
isError: true
|
|
939
|
-
};
|
|
940
|
-
const triples = await backend.queryKgTriples({
|
|
941
|
-
subject: params.subject,
|
|
942
|
-
predicate: params.predicate,
|
|
943
|
-
object: params.object,
|
|
944
|
-
project_id: params.project_id,
|
|
945
|
-
as_of: asOf,
|
|
946
|
-
include_invalidated: params.include_invalidated
|
|
947
|
-
});
|
|
948
|
-
return { content: [{
|
|
949
|
-
type: "text",
|
|
950
|
-
text: JSON.stringify(triples, null, 2)
|
|
951
|
-
}] };
|
|
952
|
-
} catch (e) {
|
|
953
|
-
return {
|
|
954
|
-
content: [{
|
|
955
|
-
type: "text",
|
|
956
|
-
text: `kg_query error: ${String(e)}`
|
|
957
|
-
}],
|
|
958
|
-
isError: true
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
async function toolKgInvalidate(backend, params) {
|
|
963
|
-
try {
|
|
964
|
-
if (params.triple_id === void 0 || params.triple_id === null) return {
|
|
965
|
-
content: [{
|
|
966
|
-
type: "text",
|
|
967
|
-
text: "kg_invalidate error: triple_id is required"
|
|
968
|
-
}],
|
|
969
|
-
isError: true
|
|
970
|
-
};
|
|
971
|
-
await backend.invalidateKgTriple(params.triple_id);
|
|
972
|
-
return { content: [{
|
|
973
|
-
type: "text",
|
|
974
|
-
text: JSON.stringify({
|
|
975
|
-
invalidated: true,
|
|
976
|
-
triple_id: params.triple_id
|
|
977
|
-
})
|
|
978
|
-
}] };
|
|
979
|
-
} catch (e) {
|
|
980
|
-
return {
|
|
981
|
-
content: [{
|
|
982
|
-
type: "text",
|
|
983
|
-
text: `kg_invalidate error: ${String(e)}`
|
|
984
|
-
}],
|
|
985
|
-
isError: true
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
async function toolKgContradictions(backend, params) {
|
|
990
|
-
try {
|
|
991
|
-
if (!params.subject) return {
|
|
992
|
-
content: [{
|
|
993
|
-
type: "text",
|
|
994
|
-
text: "kg_contradictions error: subject is required"
|
|
995
|
-
}],
|
|
996
|
-
isError: true
|
|
997
|
-
};
|
|
998
|
-
const contradictions = await backend.getKgContradictions(params.subject);
|
|
999
|
-
return { content: [{
|
|
1000
|
-
type: "text",
|
|
1001
|
-
text: JSON.stringify(contradictions, null, 2)
|
|
1002
|
-
}] };
|
|
1003
|
-
} catch (e) {
|
|
1004
|
-
return {
|
|
1005
|
-
content: [{
|
|
1006
|
-
type: "text",
|
|
1007
|
-
text: `kg_contradictions error: ${String(e)}`
|
|
1008
|
-
}],
|
|
1009
|
-
isError: true
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
//#endregion
|
|
1015
|
-
//#region src/mcp/tools/wakeup.ts
|
|
1016
|
-
const DEFAULT_TOKEN_BUDGET = 800;
|
|
1017
|
-
async function toolMemoryWakeup(registry, params) {
|
|
1018
|
-
try {
|
|
1019
|
-
const tokenBudget = params.token_budget ?? DEFAULT_TOKEN_BUDGET;
|
|
1020
|
-
let rootPath;
|
|
1021
|
-
if (params.project) {
|
|
1022
|
-
const bySlug = await registry.getProjectBySlug(params.project);
|
|
1023
|
-
if (bySlug) rootPath = bySlug.root_path;
|
|
1024
|
-
else {
|
|
1025
|
-
const detected = await detectProjectFromPath(registry, params.project);
|
|
1026
|
-
if (detected) rootPath = detected.root_path;
|
|
1027
|
-
}
|
|
1028
|
-
} else {
|
|
1029
|
-
const detected = await detectProjectFromPath(registry, process.cwd());
|
|
1030
|
-
if (detected) rootPath = detected.root_path;
|
|
1031
|
-
}
|
|
1032
|
-
const wakeupBlock = buildWakeupContext(rootPath, tokenBudget);
|
|
1033
|
-
if (!wakeupBlock) return { content: [{
|
|
1034
|
-
type: "text",
|
|
1035
|
-
text: "No wake-up context available. Create ~/.pai/identity.txt for L0 identity, or ensure session notes exist for L1 story."
|
|
1036
|
-
}] };
|
|
1037
|
-
return { content: [{
|
|
1038
|
-
type: "text",
|
|
1039
|
-
text: `WAKEUP CONTEXT\n\n${wakeupBlock}`
|
|
1040
|
-
}] };
|
|
1041
|
-
} catch (e) {
|
|
1042
|
-
return {
|
|
1043
|
-
content: [{
|
|
1044
|
-
type: "text",
|
|
1045
|
-
text: `Wakeup context error: ${String(e)}`
|
|
1046
|
-
}],
|
|
1047
|
-
isError: true
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
//#endregion
|
|
1053
|
-
//#region src/memory/taxonomy.ts
|
|
1054
|
-
/**
|
|
1055
|
-
* Build a taxonomy of stored memory — what projects exist, how much is stored,
|
|
1056
|
-
* and what has been active recently.
|
|
1057
|
-
*
|
|
1058
|
-
* Registry queries (projects, sessions) go through RegistryBackend (async).
|
|
1059
|
-
* Storage backend queries (files, chunks) are async.
|
|
1060
|
-
*/
|
|
1061
|
-
async function getTaxonomy(registry, storage, options = {}) {
|
|
1062
|
-
const includeArchived = options.include_archived ?? false;
|
|
1063
|
-
const limit = options.limit ?? 50;
|
|
1064
|
-
const projectRows = includeArchived ? await registry.listProjects({
|
|
1065
|
-
orderBy: "updated_desc",
|
|
1066
|
-
limit
|
|
1067
|
-
}) : await registry.listProjects({
|
|
1068
|
-
status: "active",
|
|
1069
|
-
orderBy: "updated_desc",
|
|
1070
|
-
limit
|
|
1071
|
-
});
|
|
1072
|
-
if (projectRows.length === 0) return {
|
|
1073
|
-
projects: [],
|
|
1074
|
-
totals: {
|
|
1075
|
-
projects: 0,
|
|
1076
|
-
sessions: 0,
|
|
1077
|
-
notes: 0,
|
|
1078
|
-
chunks: 0
|
|
1079
|
-
},
|
|
1080
|
-
recent_activity: []
|
|
1081
|
-
};
|
|
1082
|
-
const projectIds = projectRows.map((p) => p.id);
|
|
1083
|
-
const projectIdSet = new Set(projectIds);
|
|
1084
|
-
const sessionCountsByProject = /* @__PURE__ */ new Map();
|
|
1085
|
-
const lastSessionDateByProject = /* @__PURE__ */ new Map();
|
|
1086
|
-
for (const projectId of projectIds) {
|
|
1087
|
-
sessionCountsByProject.set(projectId, await registry.countSessionsForProject(projectId));
|
|
1088
|
-
lastSessionDateByProject.set(projectId, await registry.getMostRecentSessionDate(projectId));
|
|
1089
|
-
}
|
|
1090
|
-
const tagsByProject = /* @__PURE__ */ new Map();
|
|
1091
|
-
for (const projectId of projectIds) tagsByProject.set(projectId, await registry.listTagsForProject(projectId));
|
|
1092
|
-
const noteCountsByProject = /* @__PURE__ */ new Map();
|
|
1093
|
-
const chunkCountsByProject = /* @__PURE__ */ new Map();
|
|
1094
|
-
for (const projectId of projectIds) {
|
|
1095
|
-
const projectStats = await storage.getProjectStats(projectId);
|
|
1096
|
-
noteCountsByProject.set(projectId, projectStats.files);
|
|
1097
|
-
chunkCountsByProject.set(projectId, projectStats.chunks);
|
|
1098
|
-
}
|
|
1099
|
-
const stats = await storage.getStats();
|
|
1100
|
-
const totalProjects = includeArchived ? await registry.countProjects() : await registry.countProjects({ status: "active" });
|
|
1101
|
-
const totalSessions = [...sessionCountsByProject.values()].reduce((a, b) => a + b, 0);
|
|
1102
|
-
const recentActivity = (await registry.listSessions({ limit: Math.max(50, limit) })).filter((s) => projectIdSet.has(s.project_id)).slice(0, 10).map((row) => ({
|
|
1103
|
-
project_slug: row.project_slug,
|
|
1104
|
-
action: `session: ${row.title || "(untitled)"}`,
|
|
1105
|
-
timestamp: row.date
|
|
1106
|
-
}));
|
|
1107
|
-
return {
|
|
1108
|
-
projects: projectRows.map((row) => ({
|
|
1109
|
-
slug: row.slug,
|
|
1110
|
-
display_name: row.display_name,
|
|
1111
|
-
session_count: sessionCountsByProject.get(row.id) ?? 0,
|
|
1112
|
-
note_count: noteCountsByProject.get(row.id) ?? 0,
|
|
1113
|
-
last_activity: lastSessionDateByProject.get(row.id) ?? null,
|
|
1114
|
-
top_tags: tagsByProject.get(row.id) ?? []
|
|
1115
|
-
})),
|
|
1116
|
-
totals: {
|
|
1117
|
-
projects: totalProjects,
|
|
1118
|
-
sessions: totalSessions,
|
|
1119
|
-
notes: stats.files,
|
|
1120
|
-
chunks: stats.chunks
|
|
1121
|
-
},
|
|
1122
|
-
recent_activity: recentActivity
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
//#endregion
|
|
1127
|
-
//#region src/mcp/tools/taxonomy.ts
|
|
1128
|
-
async function toolMemoryTaxonomy(registry, storage, params = {}) {
|
|
1129
|
-
try {
|
|
1130
|
-
const result = await getTaxonomy(registry, storage, {
|
|
1131
|
-
include_archived: params.include_archived,
|
|
1132
|
-
limit: params.limit
|
|
1133
|
-
});
|
|
1134
|
-
const lines = [];
|
|
1135
|
-
lines.push(`PAI Memory Taxonomy — ${result.totals.projects} project(s), ${result.totals.sessions} session(s), ${result.totals.notes} indexed file(s), ${result.totals.chunks} chunk(s)`);
|
|
1136
|
-
lines.push("");
|
|
1137
|
-
if (result.projects.length === 0) lines.push("No active projects found.");
|
|
1138
|
-
else {
|
|
1139
|
-
lines.push("Projects:");
|
|
1140
|
-
for (const p of result.projects) {
|
|
1141
|
-
const tagStr = p.top_tags.length > 0 ? ` [${p.top_tags.join(", ")}]` : "";
|
|
1142
|
-
const activityStr = p.last_activity ? ` last: ${p.last_activity}` : "";
|
|
1143
|
-
lines.push(` ${p.slug} — ${p.display_name} sessions=${p.session_count}` + (p.note_count > 0 ? ` files=${p.note_count}` : "") + activityStr + tagStr);
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
if (result.recent_activity.length > 0) {
|
|
1147
|
-
lines.push("");
|
|
1148
|
-
lines.push("Recent activity (last 10 sessions across all projects):");
|
|
1149
|
-
for (const a of result.recent_activity) lines.push(` ${a.timestamp} ${a.project_slug} ${a.action}`);
|
|
1150
|
-
}
|
|
1151
|
-
return { content: [{
|
|
1152
|
-
type: "text",
|
|
1153
|
-
text: lines.join("\n")
|
|
1154
|
-
}] };
|
|
1155
|
-
} catch (e) {
|
|
1156
|
-
return {
|
|
1157
|
-
content: [{
|
|
1158
|
-
type: "text",
|
|
1159
|
-
text: `memory_taxonomy error: ${String(e)}`
|
|
1160
|
-
}],
|
|
1161
|
-
isError: true
|
|
1162
|
-
};
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
//#endregion
|
|
1167
|
-
//#region src/memory/tunnels.ts
|
|
1168
|
-
/**
|
|
1169
|
-
* Find cross-project concept tunnels.
|
|
1170
|
-
*
|
|
1171
|
-
* Works with both SQLite and Postgres storage backends.
|
|
1172
|
-
* Requires the RegistryBackend for project slug resolution.
|
|
1173
|
-
*
|
|
1174
|
-
* @param backend Active PAI storage backend.
|
|
1175
|
-
* @param registry Registry backend for project slug resolution.
|
|
1176
|
-
* @param options Filter and limit options.
|
|
1177
|
-
*/
|
|
1178
|
-
async function findTunnels(backend, registry, options) {
|
|
1179
|
-
return backend.findTunnels(registry, options);
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
//#endregion
|
|
1183
|
-
//#region src/mcp/tools/tunnels.ts
|
|
1184
|
-
async function toolMemoryTunnels(registry, backend, params) {
|
|
1185
|
-
try {
|
|
1186
|
-
const result = await findTunnels(backend, registry, {
|
|
1187
|
-
min_projects: params.min_projects,
|
|
1188
|
-
min_occurrences: params.min_occurrences,
|
|
1189
|
-
limit: params.limit
|
|
1190
|
-
});
|
|
1191
|
-
return { content: [{
|
|
1192
|
-
type: "text",
|
|
1193
|
-
text: JSON.stringify(result, null, 2)
|
|
1194
|
-
}] };
|
|
1195
|
-
} catch (e) {
|
|
1196
|
-
return {
|
|
1197
|
-
content: [{
|
|
1198
|
-
type: "text",
|
|
1199
|
-
text: `memory_tunnels error: ${String(e)}`
|
|
1200
|
-
}],
|
|
1201
|
-
isError: true
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
//#endregion
|
|
1207
|
-
//#region src/mcp/tools/feedback.ts
|
|
1208
|
-
const FEEDBACK_ALPHA = .1;
|
|
1209
|
-
/**
|
|
1210
|
-
* Apply relevance feedback to memory chunks and associated KG entities.
|
|
1211
|
-
* Both memory_chunks.relevance_score and kg_entities.feedback_weight go
|
|
1212
|
-
* through the passed StorageBackend, so this works on either backend.
|
|
1213
|
-
*
|
|
1214
|
-
* @param backend StorageBackend for both the chunk and kg_entities portions
|
|
1215
|
-
* @param params Feedback parameters
|
|
1216
|
-
*/
|
|
1217
|
-
async function toolMemoryFeedback(backend, params) {
|
|
1218
|
-
try {
|
|
1219
|
-
if (!Array.isArray(params.chunk_ids) || params.chunk_ids.length === 0) return {
|
|
1220
|
-
content: [{
|
|
1221
|
-
type: "text",
|
|
1222
|
-
text: "memory_feedback error: chunk_ids must be a non-empty array"
|
|
1223
|
-
}],
|
|
1224
|
-
isError: true
|
|
1225
|
-
};
|
|
1226
|
-
const rating = params.rating;
|
|
1227
|
-
if (typeof rating !== "number" || rating < 1 || rating > 5) return {
|
|
1228
|
-
content: [{
|
|
1229
|
-
type: "text",
|
|
1230
|
-
text: "memory_feedback error: rating must be a number between 1 and 5"
|
|
1231
|
-
}],
|
|
1232
|
-
isError: true
|
|
1233
|
-
};
|
|
1234
|
-
const normalizedRating = (rating - 1) / 4;
|
|
1235
|
-
const chunks = await backend.getChunksForFeedback(params.chunk_ids);
|
|
1236
|
-
if (chunks.length === 0) return {
|
|
1237
|
-
content: [{
|
|
1238
|
-
type: "text",
|
|
1239
|
-
text: "memory_feedback: no matching chunks found"
|
|
1240
|
-
}],
|
|
1241
|
-
isError: true
|
|
1242
|
-
};
|
|
1243
|
-
let updatedChunks = 0;
|
|
1244
|
-
const combinedText = [];
|
|
1245
|
-
for (const chunk of chunks) {
|
|
1246
|
-
const oldScore = chunk.relevanceScore ?? .5;
|
|
1247
|
-
const newScore = oldScore + FEEDBACK_ALPHA * (normalizedRating - oldScore);
|
|
1248
|
-
await backend.updateChunkRelevanceScore(chunk.id, newScore);
|
|
1249
|
-
updatedChunks++;
|
|
1250
|
-
combinedText.push(chunk.text);
|
|
1251
|
-
}
|
|
1252
|
-
const tenantId = params.tenant_id ?? "default";
|
|
1253
|
-
const entities = await backend.listKgEntities(tenantId, void 0, 500);
|
|
1254
|
-
const text = combinedText.join("\n").toLowerCase();
|
|
1255
|
-
let updatedEntities = 0;
|
|
1256
|
-
for (const entity of entities) if (text.includes(entity.name.toLowerCase())) {
|
|
1257
|
-
await backend.updateEntityFeedbackWeight(entity.entity_id, normalizedRating, FEEDBACK_ALPHA);
|
|
1258
|
-
updatedEntities++;
|
|
1259
|
-
}
|
|
1260
|
-
return { content: [{
|
|
1261
|
-
type: "text",
|
|
1262
|
-
text: JSON.stringify({
|
|
1263
|
-
updated_chunks: updatedChunks,
|
|
1264
|
-
updated_entities: updatedEntities,
|
|
1265
|
-
rating,
|
|
1266
|
-
normalized_rating: normalizedRating
|
|
1267
|
-
})
|
|
1268
|
-
}] };
|
|
1269
|
-
} catch (e) {
|
|
1270
|
-
return {
|
|
1271
|
-
content: [{
|
|
1272
|
-
type: "text",
|
|
1273
|
-
text: `memory_feedback error: ${String(e)}`
|
|
1274
|
-
}],
|
|
1275
|
-
isError: true
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
//#endregion
|
|
1281
|
-
//#region src/memory/kg-search.ts
|
|
1282
|
-
/**
|
|
1283
|
-
* kg-search.ts — Graph-completion retrieval (MR1).
|
|
1284
|
-
*
|
|
1285
|
-
* Implements graphCompletionSearch: a multi-phase retrieval strategy that
|
|
1286
|
-
* combines vector search with knowledge-graph neighborhood expansion.
|
|
1287
|
-
*
|
|
1288
|
-
* Algorithm:
|
|
1289
|
-
* Phase 1: Wide vector search (50 candidates) using queryVec
|
|
1290
|
-
* Phase 2: Extract entity mentions from retrieved chunks (substring match vs kg_entities)
|
|
1291
|
-
* Phase 3: BFS neighborhood expansion in kg_triples (1-2 hops from matched entities)
|
|
1292
|
-
* Phase 4: Re-rank all collected triples against query embedding using cosine similarity
|
|
1293
|
-
*
|
|
1294
|
-
* The result is a ranked list of KgTriple objects most relevant to the query,
|
|
1295
|
-
* augmented with graph-derived context that pure vector search would miss.
|
|
1296
|
-
*/
|
|
1297
|
-
/**
|
|
1298
|
-
* Find entity names (from the kg_entities table) that appear
|
|
1299
|
-
* as substrings in the given text chunks.
|
|
1300
|
-
*
|
|
1301
|
-
* Returns a deduplicated list of matching entity names.
|
|
1302
|
-
*/
|
|
1303
|
-
async function extractEntityMentions(federation, chunks, tenantId = "default") {
|
|
1304
|
-
const entities = await federation.listKgEntities(tenantId, void 0, 500);
|
|
1305
|
-
if (entities.length === 0) return [];
|
|
1306
|
-
const combinedText = chunks.map((c) => c.snippet).join("\n").toLowerCase();
|
|
1307
|
-
const matched = /* @__PURE__ */ new Set();
|
|
1308
|
-
for (const entity of entities) if (combinedText.includes(entity.name.toLowerCase())) matched.add(entity.name);
|
|
1309
|
-
return Array.from(matched);
|
|
1310
|
-
}
|
|
1311
|
-
/**
|
|
1312
|
-
* Perform BFS over kg_triples to find neighbors of the given entity names.
|
|
1313
|
-
*
|
|
1314
|
-
* For each entity name, fetches triples where the entity is the subject
|
|
1315
|
-
* or the object (bi-directional expansion). Supports 1 or 2 hop depths.
|
|
1316
|
-
*
|
|
1317
|
-
* Returns all unique triples found in the BFS expansion.
|
|
1318
|
-
*/
|
|
1319
|
-
async function bfsExpand(federation, entityNames, hops, projectId) {
|
|
1320
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1321
|
-
const tripleMap = /* @__PURE__ */ new Map();
|
|
1322
|
-
const frontier = new Set(entityNames);
|
|
1323
|
-
for (let hop = 0; hop < hops; hop++) {
|
|
1324
|
-
const nextFrontier = /* @__PURE__ */ new Set();
|
|
1325
|
-
for (const name of frontier) {
|
|
1326
|
-
const asSubject = await federation.queryKgTriples({
|
|
1327
|
-
subject: name,
|
|
1328
|
-
project_id: projectId
|
|
1329
|
-
});
|
|
1330
|
-
const asObject = await federation.queryKgTriples({
|
|
1331
|
-
object: name,
|
|
1332
|
-
project_id: projectId
|
|
1333
|
-
});
|
|
1334
|
-
for (const triple of [...asSubject, ...asObject]) if (!seen.has(triple.id)) {
|
|
1335
|
-
seen.add(triple.id);
|
|
1336
|
-
tripleMap.set(triple.id, triple);
|
|
1337
|
-
nextFrontier.add(triple.subject);
|
|
1338
|
-
nextFrontier.add(triple.object);
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
frontier.clear();
|
|
1342
|
-
for (const name of nextFrontier) if (!entityNames.includes(name)) {
|
|
1343
|
-
frontier.add(name);
|
|
1344
|
-
entityNames.push(name);
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
return Array.from(tripleMap.values());
|
|
1348
|
-
}
|
|
1349
|
-
/**
|
|
1350
|
-
* Score each triple by cosine similarity to the query embedding.
|
|
1351
|
-
*
|
|
1352
|
-
* The triple text representation is: "subject predicate object"
|
|
1353
|
-
* We embed this on-the-fly using generateEmbedding(), which may use a cache.
|
|
1354
|
-
*
|
|
1355
|
-
* Returns triples sorted by relevance score descending.
|
|
1356
|
-
*/
|
|
1357
|
-
async function rerankTriples(triples, queryVec, maxTriples) {
|
|
1358
|
-
if (triples.length === 0) return [];
|
|
1359
|
-
const { generateEmbedding } = await import("./embeddings-CcscYWwk.mjs");
|
|
1360
|
-
return (await Promise.all(triples.map(async (triple) => {
|
|
1361
|
-
const text = `${triple.subject} ${triple.predicate} ${triple.object}`;
|
|
1362
|
-
try {
|
|
1363
|
-
const score = cosineSimilarity(queryVec, await generateEmbedding(text, false));
|
|
1364
|
-
return {
|
|
1365
|
-
...triple,
|
|
1366
|
-
relevanceScore: score
|
|
1367
|
-
};
|
|
1368
|
-
} catch {
|
|
1369
|
-
return {
|
|
1370
|
-
...triple,
|
|
1371
|
-
relevanceScore: 0
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1374
|
-
}))).sort((a, b) => b.relevanceScore - a.relevanceScore).slice(0, maxTriples);
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Graph-completion retrieval combining vector search with KG neighborhood BFS.
|
|
1378
|
-
*
|
|
1379
|
-
* @param federation Active StorageBackend (for chunk/entity/kg_triples lookup)
|
|
1380
|
-
* @param queryVec Pre-computed embedding for the search query
|
|
1381
|
-
* @param opts Configuration options
|
|
1382
|
-
*/
|
|
1383
|
-
async function graphCompletionSearch(federation, queryVec, opts = {}) {
|
|
1384
|
-
const { seedCount = 50, hops = 1, maxTriples = 20, projectId, tenantId = "default", searchOpts } = opts;
|
|
1385
|
-
const seedSearchOpts = {
|
|
1386
|
-
...searchOpts,
|
|
1387
|
-
maxResults: seedCount,
|
|
1388
|
-
projectIds: projectId != null ? [projectId] : searchOpts?.projectIds
|
|
1389
|
-
};
|
|
1390
|
-
let seedChunks = [];
|
|
1391
|
-
try {
|
|
1392
|
-
seedChunks = await federation.searchSemantic(queryVec, seedSearchOpts);
|
|
1393
|
-
} catch (e) {
|
|
1394
|
-
process.stderr.write(`[kg-search] Phase 1 seed search error: ${e}\n`);
|
|
1395
|
-
}
|
|
1396
|
-
if (seedChunks.length === 0) return {
|
|
1397
|
-
triples: [],
|
|
1398
|
-
seedChunks: [],
|
|
1399
|
-
expandedEntities: []
|
|
1400
|
-
};
|
|
1401
|
-
const expandedEntities = await extractEntityMentions(federation, seedChunks, tenantId);
|
|
1402
|
-
if (expandedEntities.length === 0) return {
|
|
1403
|
-
triples: [],
|
|
1404
|
-
seedChunks,
|
|
1405
|
-
expandedEntities: []
|
|
1406
|
-
};
|
|
1407
|
-
let expandedTriples = [];
|
|
1408
|
-
try {
|
|
1409
|
-
expandedTriples = await bfsExpand(federation, [...expandedEntities], hops, projectId);
|
|
1410
|
-
} catch (e) {
|
|
1411
|
-
process.stderr.write(`[kg-search] Phase 3 BFS expansion error: ${e}\n`);
|
|
1412
|
-
}
|
|
1413
|
-
if (expandedTriples.length === 0) return {
|
|
1414
|
-
triples: [],
|
|
1415
|
-
seedChunks,
|
|
1416
|
-
expandedEntities
|
|
1417
|
-
};
|
|
1418
|
-
let rankedTriples = [];
|
|
1419
|
-
try {
|
|
1420
|
-
rankedTriples = await rerankTriples(expandedTriples, queryVec, maxTriples);
|
|
1421
|
-
} catch (e) {
|
|
1422
|
-
process.stderr.write(`[kg-search] Phase 4 rerank error: ${e}\n`);
|
|
1423
|
-
rankedTriples = expandedTriples.slice(0, maxTriples).map((t) => ({
|
|
1424
|
-
...t,
|
|
1425
|
-
relevanceScore: 0
|
|
1426
|
-
}));
|
|
1427
|
-
}
|
|
1428
|
-
return {
|
|
1429
|
-
triples: rankedTriples,
|
|
1430
|
-
seedChunks,
|
|
1431
|
-
expandedEntities
|
|
1432
|
-
};
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
//#endregion
|
|
1436
|
-
//#region src/mcp/tools/kg-search.ts
|
|
1437
|
-
/**
|
|
1438
|
-
* Graph-completion search: vector search + KG neighborhood BFS.
|
|
1439
|
-
*
|
|
1440
|
-
* @param federation Active StorageBackend for chunk/entity/kg_triples operations
|
|
1441
|
-
* @param params Tool parameters
|
|
1442
|
-
*/
|
|
1443
|
-
async function toolMemoryKgSearch(federation, params) {
|
|
1444
|
-
try {
|
|
1445
|
-
if (!params.query || typeof params.query !== "string" || params.query.trim() === "") return {
|
|
1446
|
-
content: [{
|
|
1447
|
-
type: "text",
|
|
1448
|
-
text: "memory_kg_search error: query is required"
|
|
1449
|
-
}],
|
|
1450
|
-
isError: true
|
|
1451
|
-
};
|
|
1452
|
-
const { generateEmbedding } = await import("./embeddings-CcscYWwk.mjs");
|
|
1453
|
-
const result = await graphCompletionSearch(federation, await generateEmbedding(params.query, true), {
|
|
1454
|
-
seedCount: params.wide_k ?? 50,
|
|
1455
|
-
hops: params.neighborhood_depth ?? 1,
|
|
1456
|
-
maxTriples: params.top_k ?? 20,
|
|
1457
|
-
projectId: params.project_id,
|
|
1458
|
-
tenantId: params.tenant_id ?? "default"
|
|
1459
|
-
});
|
|
1460
|
-
if (result.triples.length === 0) return { content: [{
|
|
1461
|
-
type: "text",
|
|
1462
|
-
text: `No KG triples found for query: "${params.query}"\nSeed chunks: ${result.seedChunks.length}, Expanded entities: ${result.expandedEntities.length}`
|
|
1463
|
-
}] };
|
|
1464
|
-
const tripleLines = result.triples.map((t, i) => `[${i + 1}] (score=${t.relevanceScore.toFixed(4)}) ${t.subject} — ${t.predicate} — ${t.object}`).join("\n");
|
|
1465
|
-
const entityList = result.expandedEntities.slice(0, 10).join(", ");
|
|
1466
|
-
return { content: [{
|
|
1467
|
-
type: "text",
|
|
1468
|
-
text: [
|
|
1469
|
-
`Found ${result.triples.length} KG triple(s) for "${params.query}"`,
|
|
1470
|
-
`Seed chunks: ${result.seedChunks.length}`,
|
|
1471
|
-
`Expanded entities (${result.expandedEntities.length}): ${entityList}`,
|
|
1472
|
-
"",
|
|
1473
|
-
tripleLines
|
|
1474
|
-
].join("\n")
|
|
1475
|
-
}] };
|
|
1476
|
-
} catch (e) {
|
|
1477
|
-
return {
|
|
1478
|
-
content: [{
|
|
1479
|
-
type: "text",
|
|
1480
|
-
text: `memory_kg_search error: ${String(e)}`
|
|
1481
|
-
}],
|
|
1482
|
-
isError: true
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
//#endregion
|
|
1488
|
-
export { detectProjectFromPath as A, toolProjectInfo as C, toolMemoryGet as D, combineHybridResults as E, lookupProjectId as M, toolMemoryOutline as O, toolProjectHealth as S, toolProjectTodo as T, toolZettelThemes as _, toolMemoryWakeup as a, toolSessionRoute as b, toolKgInvalidate as c, toolZettelConverse as d, toolZettelExplore as f, toolZettelSurprise as g, toolZettelSuggest as h, toolMemoryTaxonomy as i, formatProject as j, toolMemorySearch as k, toolKgQuery as l, toolZettelHealth as m, toolMemoryFeedback as n, toolKgAdd as o, toolZettelGodNotes as p, toolMemoryTunnels as r, toolKgContradictions as s, toolMemoryKgSearch as t, toolZettelCommunities as u, toolRegistrySearch as v, toolProjectList as w, toolProjectDetect as x, toolSessionList as y };
|
|
1489
|
-
//# sourceMappingURL=tools-BcOKPBtg.mjs.map
|