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