@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.
- package/ARCHITECTURE.md +121 -0
- package/FEATURE.md +5 -0
- package/README.md +54 -0
- package/dist/cli/index.mjs +11 -11
- package/dist/daemon/index.mjs +3 -3
- package/dist/{daemon-nXyhvdzz.mjs → daemon-VIFoKc_z.mjs} +31 -6
- package/dist/daemon-VIFoKc_z.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +51 -0
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{factory-Ygqe_bVZ.mjs → factory-e0k1HWuc.mjs} +2 -2
- package/dist/{factory-Ygqe_bVZ.mjs.map → factory-e0k1HWuc.mjs.map} +1 -1
- package/dist/hooks/load-project-context.mjs +276 -89
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/stop-hook.mjs +152 -2
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/{postgres-CKf-EDtS.mjs → postgres-DvEPooLO.mjs} +45 -10
- package/dist/postgres-DvEPooLO.mjs.map +1 -0
- package/dist/query-feedback-Dv43XKHM.mjs +76 -0
- package/dist/query-feedback-Dv43XKHM.mjs.map +1 -0
- package/dist/tools-C4SBZHga.mjs +1731 -0
- package/dist/tools-C4SBZHga.mjs.map +1 -0
- package/dist/{vault-indexer-Bi2cRmn7.mjs → vault-indexer-B-aJpRZC.mjs} +3 -2
- package/dist/{vault-indexer-Bi2cRmn7.mjs.map → vault-indexer-B-aJpRZC.mjs.map} +1 -1
- package/dist/{zettelkasten-cdajbnPr.mjs → zettelkasten-DhBKZQHF.mjs} +358 -3
- package/dist/zettelkasten-DhBKZQHF.mjs.map +1 -0
- package/package.json +1 -1
- package/src/hooks/ts/session-start/load-project-context.ts +36 -0
- package/src/hooks/ts/stop/stop-hook.ts +203 -1
- package/dist/daemon-nXyhvdzz.mjs.map +0 -1
- package/dist/postgres-CKf-EDtS.mjs.map +0 -1
- package/dist/tools-DcaJlYDN.mjs +0 -869
- package/dist/tools-DcaJlYDN.mjs.map +0 -1
- package/dist/zettelkasten-cdajbnPr.mjs.map +0 -1
package/dist/tools-DcaJlYDN.mjs
DELETED
|
@@ -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
|