@graphmemory/server 1.1.0 → 1.3.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/LICENSE +84 -12
- package/README.md +66 -101
- package/dist/api/index.js +279 -169
- package/dist/api/rest/index.js +36 -16
- package/dist/api/rest/tools.js +8 -1
- package/dist/api/rest/websocket.js +22 -1
- package/dist/api/tools/code/search-code.js +12 -9
- package/dist/api/tools/code/search-files.js +1 -1
- package/dist/api/tools/docs/cross-references.js +3 -2
- package/dist/api/tools/docs/explain-symbol.js +2 -1
- package/dist/api/tools/docs/find-examples.js +2 -1
- package/dist/api/tools/docs/search-files.js +1 -1
- package/dist/api/tools/docs/search-snippets.js +1 -1
- package/dist/api/tools/docs/search.js +5 -4
- package/dist/api/tools/file-index/search-all-files.js +1 -1
- package/dist/api/tools/knowledge/add-attachment.js +14 -3
- package/dist/api/tools/knowledge/create-relation.js +2 -2
- package/dist/api/tools/knowledge/delete-relation.js +2 -2
- package/dist/api/tools/knowledge/find-linked-notes.js +1 -1
- package/dist/api/tools/knowledge/remove-attachment.js +5 -1
- package/dist/api/tools/knowledge/search-notes.js +5 -4
- package/dist/api/tools/skills/add-attachment.js +14 -3
- package/dist/api/tools/skills/recall-skills.js +1 -1
- package/dist/api/tools/skills/remove-attachment.js +5 -1
- package/dist/api/tools/skills/search-skills.js +6 -5
- package/dist/api/tools/tasks/add-attachment.js +14 -3
- package/dist/api/tools/tasks/create-task-link.js +1 -1
- package/dist/api/tools/tasks/delete-task-link.js +1 -1
- package/dist/api/tools/tasks/find-linked-tasks.js +1 -1
- package/dist/api/tools/tasks/remove-attachment.js +5 -1
- package/dist/api/tools/tasks/search-tasks.js +5 -4
- package/dist/cli/index.js +69 -311
- package/dist/cli/indexer.js +61 -29
- package/dist/graphs/code.js +70 -7
- package/dist/graphs/docs.js +15 -2
- package/dist/graphs/file-index.js +20 -6
- package/dist/graphs/file-lang.js +1 -1
- package/dist/graphs/knowledge.js +20 -3
- package/dist/graphs/manager-types.js +1 -1
- package/dist/graphs/skill.js +23 -4
- package/dist/graphs/task.js +23 -4
- package/dist/lib/embedding-codec.js +65 -0
- package/dist/lib/file-mirror.js +7 -7
- package/dist/lib/frontmatter.js +3 -2
- package/dist/lib/jwt.js +4 -4
- package/dist/lib/mirror-watcher.js +5 -4
- package/dist/lib/multi-config.js +60 -1
- package/dist/lib/parsers/code.js +158 -31
- package/dist/lib/parsers/codeblock.js +11 -6
- package/dist/lib/parsers/docs.js +59 -31
- package/dist/lib/parsers/languages/registry.js +10 -4
- package/dist/lib/parsers/languages/typescript.js +195 -48
- package/dist/lib/project-manager.js +14 -10
- package/dist/lib/search/bm25.js +18 -1
- package/dist/lib/search/code.js +12 -3
- package/dist/lib/watcher.js +17 -9
- package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
- package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
- package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
- package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
- package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
- package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
- package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
- package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
- package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
- package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
- package/dist/ui/assets/api-BMnBjMMf.js +1 -0
- package/dist/ui/assets/api-BlFF6gX-.js +1 -0
- package/dist/ui/assets/api-CrGJOcaN.js +1 -0
- package/dist/ui/assets/api-DuX-0a_X.js +1 -0
- package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
- package/dist/ui/assets/client-Bq88u7gN.js +1 -0
- package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
- package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
- package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
- package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
- package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
- package/dist/ui/assets/files-0bPg6NH9.js +1 -0
- package/dist/ui/assets/graph-DXGud_wF.js +1 -0
- package/dist/ui/assets/help-CEMQqZUR.js +891 -0
- package/dist/ui/assets/help-DJ52_fxN.js +1 -0
- package/dist/ui/assets/index-BCZDAYZi.js +2 -0
- package/dist/ui/assets/index-D6zSNtzo.css +1 -0
- package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
- package/dist/ui/assets/new-CpD7hOBA.js +1 -0
- package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
- package/dist/ui/assets/new-s8c0M75X.js +1 -0
- package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
- package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
- package/dist/ui/assets/search-EpJhdP2a.js +1 -0
- package/dist/ui/assets/skill-y9pizyqE.js +1 -0
- package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
- package/dist/ui/assets/tasks-CobouTKV.js +1 -0
- package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
- package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
- package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
- package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
- package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
- package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
- package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
- package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
- package/dist/ui/index.html +11 -3
- package/package.json +2 -2
- package/dist/ui/assets/index-D6oxrVF7.js +0 -1759
package/dist/api/rest/index.js
CHANGED
|
@@ -46,7 +46,7 @@ function createRestApp(projectManager, options) {
|
|
|
46
46
|
const users = options?.users ?? {};
|
|
47
47
|
const hasUsers = Object.keys(users).length > 0;
|
|
48
48
|
const corsOrigins = serverConfig?.corsOrigins;
|
|
49
|
-
app.use((0, cors_1.default)(corsOrigins?.length ? { origin: corsOrigins, credentials: true } : {
|
|
49
|
+
app.use((0, cors_1.default)(corsOrigins?.length ? { origin: corsOrigins, credentials: true } : {}));
|
|
50
50
|
app.use(express_1.default.json({ limit: '10mb' }));
|
|
51
51
|
app.use((0, cookie_parser_1.default)());
|
|
52
52
|
// Security headers
|
|
@@ -63,7 +63,7 @@ function createRestApp(projectManager, options) {
|
|
|
63
63
|
}
|
|
64
64
|
if (rl && rl.search > 0) {
|
|
65
65
|
const searchLimiter = (0, express_rate_limit_1.default)({ windowMs: 60_000, max: rl.search, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg });
|
|
66
|
-
app.use('/api/projects/:projectId/knowledge/
|
|
66
|
+
app.use('/api/projects/:projectId/knowledge/search', searchLimiter);
|
|
67
67
|
app.use('/api/projects/:projectId/tasks/search', searchLimiter);
|
|
68
68
|
app.use('/api/projects/:projectId/skills/search', searchLimiter);
|
|
69
69
|
app.use('/api/projects/:projectId/docs/search', searchLimiter);
|
|
@@ -89,7 +89,7 @@ function createRestApp(projectManager, options) {
|
|
|
89
89
|
const payload = (0, jwt_1.verifyToken)(accessToken, jwtSecret);
|
|
90
90
|
if (payload?.type === 'access' && users[payload.userId]) {
|
|
91
91
|
const user = users[payload.userId];
|
|
92
|
-
return res.json({ required: true, authenticated: true, userId: payload.userId, name: user.name });
|
|
92
|
+
return res.json({ required: true, authenticated: true, userId: payload.userId, name: user.name, apiKey: user.apiKey });
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -170,7 +170,7 @@ function createRestApp(projectManager, options) {
|
|
|
170
170
|
}
|
|
171
171
|
// 2. Bearer apiKey (from MCP/API clients)
|
|
172
172
|
const auth = req.headers.authorization;
|
|
173
|
-
if (auth?.startsWith('Bearer ')) {
|
|
173
|
+
if (auth?.startsWith('Bearer ') && auth.length > 7) {
|
|
174
174
|
const apiKey = auth.slice(7);
|
|
175
175
|
const result = (0, access_1.resolveUserFromApiKey)(apiKey, users);
|
|
176
176
|
if (result) {
|
|
@@ -178,6 +178,7 @@ function createRestApp(projectManager, options) {
|
|
|
178
178
|
req.user = result.user;
|
|
179
179
|
return next();
|
|
180
180
|
}
|
|
181
|
+
// Invalid Bearer token — reject (explicit auth attempt should not fall through)
|
|
181
182
|
return _res.status(401).json({ error: 'Invalid API key' });
|
|
182
183
|
}
|
|
183
184
|
// 3. No auth = anonymous (uses defaultAccess)
|
|
@@ -200,13 +201,16 @@ function createRestApp(projectManager, options) {
|
|
|
200
201
|
const p = projectManager.getProject(id);
|
|
201
202
|
const gc = p.config.graphConfigs;
|
|
202
203
|
const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
|
|
203
|
-
// Per-graph info: enabled
|
|
204
|
+
// Per-graph info: enabled, readonly, access level for current user
|
|
204
205
|
const graphs = {};
|
|
205
206
|
for (const gn of multi_config_1.GRAPH_NAMES) {
|
|
206
|
-
|
|
207
|
+
let access = serverConfig
|
|
207
208
|
? (0, access_1.resolveAccess)(userId, gn, p.config, serverConfig, ws?.config)
|
|
208
209
|
: 'rw';
|
|
209
|
-
|
|
210
|
+
// Cap access for readonly graphs
|
|
211
|
+
if (access === 'rw' && gc[gn].readonly)
|
|
212
|
+
access = 'r';
|
|
213
|
+
graphs[gn] = { enabled: gc[gn].enabled, readonly: gc[gn].readonly, access: gc[gn].enabled ? access : null };
|
|
210
214
|
}
|
|
211
215
|
return {
|
|
212
216
|
id,
|
|
@@ -280,14 +284,23 @@ function createRestApp(projectManager, options) {
|
|
|
280
284
|
// Middleware: check access level for a graph (read or read-write)
|
|
281
285
|
function requireGraphAccess(graphName, level) {
|
|
282
286
|
return (req, _res, next) => {
|
|
287
|
+
const p = req.project;
|
|
288
|
+
// Graph-level readonly: enforce even without auth config
|
|
289
|
+
const isReadonly = p?.config.graphConfigs[graphName]?.readonly;
|
|
290
|
+
if (isReadonly) {
|
|
291
|
+
req.accessLevel = 'r';
|
|
292
|
+
}
|
|
283
293
|
if (!serverConfig)
|
|
284
294
|
return next(); // no config = no auth enforcement
|
|
285
|
-
const p = req.project;
|
|
286
295
|
if (!p)
|
|
287
296
|
return next();
|
|
288
297
|
const userId = req.userId;
|
|
289
298
|
const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
|
|
290
|
-
|
|
299
|
+
let access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
|
|
300
|
+
// Graph-level readonly: cap to 'r' regardless of user permissions
|
|
301
|
+
if (access === 'rw' && p.config.graphConfigs[graphName]?.readonly) {
|
|
302
|
+
access = 'r';
|
|
303
|
+
}
|
|
291
304
|
if (!(0, access_1.canRead)(access)) {
|
|
292
305
|
return _res.status(403).json({ error: 'Access denied' });
|
|
293
306
|
}
|
|
@@ -329,7 +342,9 @@ function createRestApp(projectManager, options) {
|
|
|
329
342
|
return true;
|
|
330
343
|
const userId = req.userId;
|
|
331
344
|
const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
|
|
332
|
-
|
|
345
|
+
let access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
|
|
346
|
+
if (access === 'rw' && p.config.graphConfigs[graphName]?.readonly)
|
|
347
|
+
access = 'r';
|
|
333
348
|
if (level === 'rw')
|
|
334
349
|
return (0, access_1.canWrite)(access);
|
|
335
350
|
return (0, access_1.canRead)(access);
|
|
@@ -338,16 +353,21 @@ function createRestApp(projectManager, options) {
|
|
|
338
353
|
if (serverConfig?.embeddingApi?.enabled && options?.embeddingApiModelName) {
|
|
339
354
|
app.use('/api/embed', (0, embed_1.createEmbedRouter)(serverConfig.embeddingApi, options.embeddingApiModelName));
|
|
340
355
|
}
|
|
341
|
-
// Serve UI
|
|
356
|
+
// Serve UI at /ui/ path — check dist/ui/ (npm package) then ui/dist/ (dev)
|
|
342
357
|
const uiDistPkg = path_1.default.resolve(__dirname, '../../ui');
|
|
343
358
|
const uiDistDev = path_1.default.resolve(__dirname, '../../../ui/dist');
|
|
344
359
|
const uiDist = fs_1.default.existsSync(uiDistPkg) ? uiDistPkg : uiDistDev;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
360
|
+
// Redirect root to /ui/
|
|
361
|
+
app.get('/', (_req, res) => { res.redirect('/ui/'); });
|
|
362
|
+
// Static files under /ui/
|
|
363
|
+
app.use('/ui', express_1.default.static(uiDist, { redirect: false, index: false }));
|
|
364
|
+
// SPA fallback: serve index.html for all /ui/* routes
|
|
365
|
+
const indexHtml = path_1.default.join(uiDist, 'index.html');
|
|
366
|
+
app.use('/ui', (_req, res, next) => {
|
|
367
|
+
// Skip requests for actual files (assets with extensions like .js, .css, .png)
|
|
368
|
+
if (_req.path.includes('.') && !_req.path.endsWith('.html'))
|
|
349
369
|
return next();
|
|
350
|
-
res.sendFile(
|
|
370
|
+
res.sendFile(indexHtml, (err) => {
|
|
351
371
|
if (err)
|
|
352
372
|
next();
|
|
353
373
|
});
|
package/dist/api/rest/tools.js
CHANGED
|
@@ -5,6 +5,7 @@ const express_1 = require("express");
|
|
|
5
5
|
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
6
6
|
const inMemory_js_1 = require("@modelcontextprotocol/sdk/inMemory.js");
|
|
7
7
|
const index_1 = require("../../api/index");
|
|
8
|
+
const multi_config_1 = require("../../lib/multi-config");
|
|
8
9
|
// Tool category detection based on tool name
|
|
9
10
|
const TOOL_CATEGORIES = {
|
|
10
11
|
get_context: 'context',
|
|
@@ -49,8 +50,14 @@ async function getClient(p, pm) {
|
|
|
49
50
|
workspaceId: ws?.id,
|
|
50
51
|
workspaceProjects: ws?.config.projects,
|
|
51
52
|
};
|
|
53
|
+
// Build readonly set from config for defense-in-depth
|
|
54
|
+
const readonlyGraphs = new Set();
|
|
55
|
+
for (const gn of multi_config_1.GRAPH_NAMES) {
|
|
56
|
+
if (p.config.graphConfigs[gn].readonly)
|
|
57
|
+
readonlyGraphs.add(gn);
|
|
58
|
+
}
|
|
52
59
|
const [serverTransport, clientTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
|
|
53
|
-
const server = (0, index_1.createMcpServer)(p.docGraph, p.codeGraph, p.knowledgeGraph, p.fileIndexGraph, p.taskGraph, p.embedFns, p.mutationQueue, p.config.projectDir, p.skillGraph, sessionCtx);
|
|
60
|
+
const server = (0, index_1.createMcpServer)(p.docGraph, p.codeGraph, p.knowledgeGraph, p.fileIndexGraph, p.taskGraph, p.embedFns, p.mutationQueue, p.config.projectDir, p.skillGraph, sessionCtx, readonlyGraphs.size > 0 ? readonlyGraphs : undefined);
|
|
54
61
|
await server.connect(serverTransport);
|
|
55
62
|
const client = new index_js_1.Client({ name: 'tools-explorer', version: '1.0.0' });
|
|
56
63
|
await client.connect(clientTransport);
|
|
@@ -2,19 +2,40 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.attachWebSocket = attachWebSocket;
|
|
4
4
|
const ws_1 = require("ws");
|
|
5
|
+
const jwt_1 = require("../../lib/jwt");
|
|
5
6
|
/**
|
|
6
7
|
* Attach a WebSocket server to the HTTP server at /api/ws.
|
|
7
8
|
* Broadcasts all ProjectManager events to connected clients.
|
|
8
9
|
* Each event includes projectId — clients filter on their side.
|
|
9
10
|
*/
|
|
10
|
-
function attachWebSocket(httpServer, projectManager) {
|
|
11
|
+
function attachWebSocket(httpServer, projectManager, options) {
|
|
11
12
|
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
13
|
+
const jwtSecret = options?.jwtSecret;
|
|
14
|
+
const users = options?.users ?? {};
|
|
15
|
+
const hasUsers = Object.keys(users).length > 0;
|
|
12
16
|
// Handle upgrade requests for /api/ws
|
|
13
17
|
httpServer.on('upgrade', (req, socket, head) => {
|
|
14
18
|
if (req.url !== '/api/ws') {
|
|
15
19
|
socket.destroy();
|
|
16
20
|
return;
|
|
17
21
|
}
|
|
22
|
+
// Auth: if users are configured, require valid JWT cookie or reject
|
|
23
|
+
if (hasUsers && jwtSecret) {
|
|
24
|
+
const cookie = req.headers.cookie ?? '';
|
|
25
|
+
const match = cookie.match(/(?:^|;\s*)mgm_access=([^;]+)/);
|
|
26
|
+
const token = match?.[1];
|
|
27
|
+
if (!token) {
|
|
28
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
29
|
+
socket.destroy();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const payload = (0, jwt_1.verifyToken)(token, jwtSecret);
|
|
33
|
+
if (!payload || payload.type !== 'access' || !users[payload.userId]) {
|
|
34
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
35
|
+
socket.destroy();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
18
39
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
19
40
|
ws.on('error', (err) => {
|
|
20
41
|
process.stderr.write(`[ws] Client error: ${err}\n`);
|
|
@@ -5,23 +5,26 @@ const zod_1 = require("zod");
|
|
|
5
5
|
function register(server, mgr) {
|
|
6
6
|
server.registerTool('search_code', {
|
|
7
7
|
description: 'Semantic search over the indexed source code. ' +
|
|
8
|
-
'
|
|
9
|
-
'
|
|
8
|
+
'Supports three modes: hybrid (default, combines BM25 keyword + vector similarity), ' +
|
|
9
|
+
'vector (embedding only), keyword (BM25 text matching only). ' +
|
|
10
|
+
'Finds the most relevant symbols (functions, classes, constructors, types) by matching ' +
|
|
11
|
+
'against name, signature, doc comments, and body text, ' +
|
|
10
12
|
'then expands results by following graph edges (imports, contains, extends). ' +
|
|
11
13
|
'Returns an array sorted by relevance score (0–1), each with: ' +
|
|
12
14
|
'id, fileId, kind, name, signature, docComment, startLine, endLine, score. ' +
|
|
13
|
-
'
|
|
15
|
+
'Set includeBody=true to include full source code in results (avoids extra get_symbol calls).',
|
|
14
16
|
inputSchema: {
|
|
15
17
|
query: zod_1.z.string().describe('Natural language or code search query, e.g. "function that loads the graph from disk"'),
|
|
16
|
-
topK: zod_1.z.number().optional().describe('How many top similar symbols to use as seeds (default 5)'),
|
|
17
|
-
bfsDepth: zod_1.z.number().optional().describe('How many hops to follow graph edges from each seed (default 1; 0 = no expansion)'),
|
|
18
|
-
maxResults: zod_1.z.number().optional().describe('Maximum number of results to return (default 20)'),
|
|
19
|
-
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1; lower values return more results (default 0.
|
|
18
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar symbols to use as seeds (default 5)'),
|
|
19
|
+
bfsDepth: zod_1.z.number().min(0).max(10).optional().describe('How many hops to follow graph edges from each seed (default 1; 0 = no expansion)'),
|
|
20
|
+
maxResults: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 20)'),
|
|
21
|
+
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1; lower values return more results (default 0.3)'),
|
|
20
22
|
bfsDecay: zod_1.z.number().min(0).max(1).optional().describe('Score multiplier per graph hop (default 0.8)'),
|
|
21
23
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional().describe('Search mode: hybrid (default, BM25 + vector), vector (embedding only), keyword (BM25 only)'),
|
|
24
|
+
includeBody: zod_1.z.boolean().optional().describe('Include full source code body in results (default false)'),
|
|
22
25
|
},
|
|
23
|
-
}, async ({ query, topK, bfsDepth, maxResults, minScore, bfsDecay, searchMode }) => {
|
|
24
|
-
const results = await mgr.search(query, { topK, bfsDepth, maxResults, minScore, bfsDecay, searchMode });
|
|
26
|
+
}, async ({ query, topK, bfsDepth, maxResults, minScore, bfsDecay, searchMode, includeBody }) => {
|
|
27
|
+
const results = await mgr.search(query, { topK, bfsDepth, maxResults, minScore, bfsDecay, searchMode, includeBody });
|
|
25
28
|
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
26
29
|
});
|
|
27
30
|
}
|
|
@@ -12,7 +12,7 @@ function register(server, mgr) {
|
|
|
12
12
|
'Use this to discover which source files are relevant before diving into symbols with get_file_symbols or search_code.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
query: zod_1.z.string().describe('Natural language or path search query, e.g. "graph persistence" or "search module"'),
|
|
15
|
-
topK: zod_1.z.number().optional().describe('Maximum number of results to return (default 10)'),
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 10)'),
|
|
16
16
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.3)'),
|
|
17
17
|
},
|
|
18
18
|
}, async ({ query, topK, minScore }) => {
|
|
@@ -17,8 +17,9 @@ function register(server, docMgr, codeMgr) {
|
|
|
17
17
|
}, async ({ symbol }) => {
|
|
18
18
|
// 1. Search CodeGraph for definitions
|
|
19
19
|
const definitions = [];
|
|
20
|
+
const symbolLower = symbol.toLowerCase();
|
|
20
21
|
codeGraph.forEachNode((id, attrs) => {
|
|
21
|
-
if (attrs.name === symbol) {
|
|
22
|
+
if (attrs.name === symbol || attrs.name.toLowerCase() === symbolLower) {
|
|
22
23
|
definitions.push({
|
|
23
24
|
id,
|
|
24
25
|
fileId: attrs.fileId,
|
|
@@ -38,7 +39,7 @@ function register(server, docMgr, codeMgr) {
|
|
|
38
39
|
docGraph.forEachNode((id, attrs) => {
|
|
39
40
|
if (attrs.symbols.length === 0)
|
|
40
41
|
return;
|
|
41
|
-
if (!attrs.symbols.
|
|
42
|
+
if (!attrs.symbols.some(s => s === symbol || s.toLowerCase() === symbolLower))
|
|
42
43
|
return;
|
|
43
44
|
examples.push({
|
|
44
45
|
id,
|
|
@@ -14,13 +14,14 @@ function register(server, mgr) {
|
|
|
14
14
|
limit: zod_1.z.number().optional().describe('Max results to return (default 10)'),
|
|
15
15
|
},
|
|
16
16
|
}, async ({ symbol, limit = 10 }) => {
|
|
17
|
+
const symbolLower = symbol.toLowerCase();
|
|
17
18
|
const results = [];
|
|
18
19
|
graph.forEachNode((id, attrs) => {
|
|
19
20
|
if (results.length >= limit)
|
|
20
21
|
return;
|
|
21
22
|
if (attrs.symbols.length === 0)
|
|
22
23
|
return;
|
|
23
|
-
if (!attrs.symbols.
|
|
24
|
+
if (!attrs.symbols.some(s => s === symbol || s.toLowerCase() === symbolLower))
|
|
24
25
|
return;
|
|
25
26
|
// Find the parent text section
|
|
26
27
|
const parent = findParentTextSection(graph, id, attrs);
|
|
@@ -14,13 +14,14 @@ function register(server, mgr) {
|
|
|
14
14
|
limit: zod_1.z.number().optional().describe('Max results to return (default 20)'),
|
|
15
15
|
},
|
|
16
16
|
}, async ({ symbol, limit = 20 }) => {
|
|
17
|
+
const symbolLower = symbol.toLowerCase();
|
|
17
18
|
const results = [];
|
|
18
19
|
graph.forEachNode((id, attrs) => {
|
|
19
20
|
if (results.length >= limit)
|
|
20
21
|
return;
|
|
21
22
|
if (attrs.symbols.length === 0)
|
|
22
23
|
return;
|
|
23
|
-
if (!attrs.symbols.
|
|
24
|
+
if (!attrs.symbols.some(s => s === symbol || s.toLowerCase() === symbolLower))
|
|
24
25
|
return;
|
|
25
26
|
// Find parent text section (previous node with lower level and no language)
|
|
26
27
|
const parentId = findParentSection(graph, id, attrs);
|
|
@@ -12,7 +12,7 @@ function register(server, mgr) {
|
|
|
12
12
|
'Use this to discover which doc files are relevant before diving into content with search or get_toc.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
query: zod_1.z.string().describe('Natural language search query, e.g. "authentication setup" or "API endpoints"'),
|
|
15
|
-
topK: zod_1.z.number().optional().describe('Maximum number of results to return (default 10)'),
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 10)'),
|
|
16
16
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.3)'),
|
|
17
17
|
},
|
|
18
18
|
}, async ({ query, topK, minScore }) => {
|
|
@@ -12,7 +12,7 @@ function register(server, mgr) {
|
|
|
12
12
|
'Returns code block nodes sorted by relevance score.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
query: zod_1.z.string().describe('Natural language search query'),
|
|
15
|
-
topK: zod_1.z.number().optional().describe('Max results to return (default 10)'),
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('Max results to return (default 10)'),
|
|
16
16
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.3)'),
|
|
17
17
|
language: zod_1.z.string().optional().describe('Filter by language, e.g. "typescript", "python"'),
|
|
18
18
|
},
|
|
@@ -5,7 +5,8 @@ const zod_1 = require("zod");
|
|
|
5
5
|
function register(server, mgr) {
|
|
6
6
|
server.registerTool('search', {
|
|
7
7
|
description: 'Semantic search over the indexed documentation. ' +
|
|
8
|
-
'
|
|
8
|
+
'Supports three modes: hybrid (default, BM25 + vector), vector, keyword. ' +
|
|
9
|
+
'Finds the most relevant sections, then expands results ' +
|
|
9
10
|
'by traversing links between documents (graph walk). ' +
|
|
10
11
|
'Returns an array of chunks sorted by relevance score (0–1), each with: ' +
|
|
11
12
|
'id, fileId, title, content, level, score. ' +
|
|
@@ -13,9 +14,9 @@ function register(server, mgr) {
|
|
|
13
14
|
'Prefer this tool when looking for information without knowing which file contains it.',
|
|
14
15
|
inputSchema: {
|
|
15
16
|
query: zod_1.z.string().describe('Natural language search query'),
|
|
16
|
-
topK: zod_1.z.number().optional().describe('How many top similar sections to use as seeds for graph expansion (default 5)'),
|
|
17
|
-
bfsDepth: zod_1.z.number().optional().describe('How many hops to follow cross-document links from each seed (default 1; 0 = no expansion)'),
|
|
18
|
-
maxResults: zod_1.z.number().optional().describe('Maximum number of results to return (default 20)'),
|
|
17
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar sections to use as seeds for graph expansion (default 5)'),
|
|
18
|
+
bfsDepth: zod_1.z.number().min(0).max(10).optional().describe('How many hops to follow cross-document links from each seed (default 1; 0 = no expansion)'),
|
|
19
|
+
maxResults: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 20)'),
|
|
19
20
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score threshold 0–1; lower values return more results (default 0.5)'),
|
|
20
21
|
bfsDecay: zod_1.z.number().min(0).max(1).optional().describe('Score multiplier applied per graph hop; controls how quickly relevance fades with distance (default 0.8)'),
|
|
21
22
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional().describe('Search mode: hybrid (default, BM25 + vector), vector (embedding only), keyword (BM25 only)'),
|
|
@@ -12,7 +12,7 @@ function register(server, mgr) {
|
|
|
12
12
|
'Use this to discover which project files are relevant to a topic.',
|
|
13
13
|
inputSchema: {
|
|
14
14
|
query: zod_1.z.string().describe('Search query'),
|
|
15
|
-
topK: zod_1.z.number().optional().default(10)
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().default(10)
|
|
16
16
|
.describe('Max results (default 10)'),
|
|
17
17
|
minScore: zod_1.z.number().optional().default(0.3)
|
|
18
18
|
.describe('Minimum cosine similarity score (default 0.3)'),
|
|
@@ -17,11 +17,22 @@ function register(server, mgr) {
|
|
|
17
17
|
filePath: zod_1.z.string().describe('Absolute path to the file on disk'),
|
|
18
18
|
},
|
|
19
19
|
}, async ({ noteId, filePath }) => {
|
|
20
|
-
|
|
20
|
+
const resolved = path_1.default.resolve(filePath);
|
|
21
|
+
let stat;
|
|
22
|
+
try {
|
|
23
|
+
stat = fs_1.default.statSync(resolved);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
21
26
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File not found' }) }], isError: true };
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
if (!stat.isFile()) {
|
|
29
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
32
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
const data = fs_1.default.readFileSync(resolved);
|
|
35
|
+
const filename = path_1.default.basename(resolved);
|
|
25
36
|
const meta = mgr.addAttachment(noteId, filename, data);
|
|
26
37
|
if (!meta) {
|
|
27
38
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Note not found or no project dir' }) }], isError: true };
|
|
@@ -12,8 +12,8 @@ function register(server, mgr) {
|
|
|
12
12
|
fromId: zod_1.z.string().describe('Source note ID'),
|
|
13
13
|
toId: zod_1.z.string().describe('Target note ID, or target node ID in docs/code/files/tasks graph'),
|
|
14
14
|
kind: zod_1.z.string().describe('Relation type, e.g. "depends_on", "references"'),
|
|
15
|
-
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks']).optional()
|
|
16
|
-
.describe('Set to "docs", "code", "files", or "
|
|
15
|
+
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks', 'skills']).optional()
|
|
16
|
+
.describe('Set to "docs", "code", "files", "tasks", or "skills" to create a cross-graph link instead of note-to-note'),
|
|
17
17
|
projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
|
|
18
18
|
},
|
|
19
19
|
}, async ({ fromId, toId, kind, targetGraph, projectId }) => {
|
|
@@ -9,8 +9,8 @@ function register(server, mgr) {
|
|
|
9
9
|
inputSchema: {
|
|
10
10
|
fromId: zod_1.z.string().describe('Source note ID'),
|
|
11
11
|
toId: zod_1.z.string().describe('Target note ID, or target node ID in docs/code/files/tasks graph'),
|
|
12
|
-
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks']).optional()
|
|
13
|
-
.describe('Set to "docs", "code", "files", or "
|
|
12
|
+
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks', 'skills']).optional()
|
|
13
|
+
.describe('Set to "docs", "code", "files", "tasks", or "skills" when deleting a cross-graph link'),
|
|
14
14
|
projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
|
|
15
15
|
},
|
|
16
16
|
}, async ({ fromId, toId, targetGraph, projectId }) => {
|
|
@@ -11,7 +11,7 @@ function register(server, mgr) {
|
|
|
11
11
|
'Use get_note to fetch full content of a returned note.',
|
|
12
12
|
inputSchema: {
|
|
13
13
|
targetId: zod_1.z.string().describe('Target node ID in the external graph (e.g. "src/config.ts", "src/auth.ts::login", "docs/api.md::Setup")'),
|
|
14
|
-
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks']).describe('Which graph the target belongs to
|
|
14
|
+
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'tasks', 'skills']).describe('Which graph the target belongs to'),
|
|
15
15
|
kind: zod_1.z.string().optional().describe('Filter by relation kind (e.g. "references", "depends_on"). If omitted, returns all relations.'),
|
|
16
16
|
projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
|
|
17
17
|
},
|
|
@@ -7,7 +7,11 @@ function register(server, mgr) {
|
|
|
7
7
|
description: 'Remove an attachment from a note. The file is deleted from disk.',
|
|
8
8
|
inputSchema: {
|
|
9
9
|
noteId: zod_1.z.string().describe('ID of the note'),
|
|
10
|
-
filename: zod_1.z.string().
|
|
10
|
+
filename: zod_1.z.string().min(1).max(255)
|
|
11
|
+
.refine(s => !/[/\\]/.test(s), 'Filename must not contain path separators')
|
|
12
|
+
.refine(s => !s.includes('..'), 'Filename must not contain ..')
|
|
13
|
+
.refine(s => !s.includes('\0'), 'Filename must not contain null bytes')
|
|
14
|
+
.describe('Filename of the attachment to remove'),
|
|
11
15
|
},
|
|
12
16
|
}, async ({ noteId, filename }) => {
|
|
13
17
|
const ok = mgr.removeAttachment(noteId, filename);
|
|
@@ -5,15 +5,16 @@ const zod_1 = require("zod");
|
|
|
5
5
|
function register(server, mgr) {
|
|
6
6
|
server.registerTool('search_notes', {
|
|
7
7
|
description: 'Semantic search over the knowledge graph (facts and notes). ' +
|
|
8
|
-
'
|
|
8
|
+
'Supports three modes: hybrid (default, BM25 + vector), vector, keyword. ' +
|
|
9
|
+
'Finds the most relevant notes, then expands results ' +
|
|
9
10
|
'by traversing relations between notes (graph walk). ' +
|
|
10
11
|
'Returns an array sorted by relevance score (0–1), each with: ' +
|
|
11
12
|
'id, title, content, tags, score.',
|
|
12
13
|
inputSchema: {
|
|
13
14
|
query: zod_1.z.string().describe('Natural language search query'),
|
|
14
|
-
topK: zod_1.z.number().optional().describe('How many top similar notes to use as seeds (default 5)'),
|
|
15
|
-
bfsDepth: zod_1.z.number().optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
|
|
16
|
-
maxResults: zod_1.z.number().optional().describe('Maximum number of results to return (default 20)'),
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar notes to use as seeds (default 5)'),
|
|
16
|
+
bfsDepth: zod_1.z.number().min(0).max(10).optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
|
|
17
|
+
maxResults: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 20)'),
|
|
17
18
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.5)'),
|
|
18
19
|
bfsDecay: zod_1.z.number().min(0).max(1).optional().describe('Score multiplier per hop (default 0.8)'),
|
|
19
20
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional().describe('Search mode: hybrid (default, BM25 + vector), vector (embedding only), keyword (BM25 only)'),
|
|
@@ -17,11 +17,22 @@ function register(server, mgr) {
|
|
|
17
17
|
filePath: zod_1.z.string().describe('Absolute path to the file on disk'),
|
|
18
18
|
},
|
|
19
19
|
}, async ({ skillId, filePath }) => {
|
|
20
|
-
|
|
20
|
+
const resolved = path_1.default.resolve(filePath);
|
|
21
|
+
let stat;
|
|
22
|
+
try {
|
|
23
|
+
stat = fs_1.default.statSync(resolved);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
21
26
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File not found' }) }], isError: true };
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
if (!stat.isFile()) {
|
|
29
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
32
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
const data = fs_1.default.readFileSync(resolved);
|
|
35
|
+
const filename = path_1.default.basename(resolved);
|
|
25
36
|
const meta = mgr.addAttachment(skillId, filename, data);
|
|
26
37
|
if (!meta) {
|
|
27
38
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Skill not found or no project dir' }) }], isError: true };
|
|
@@ -8,7 +8,7 @@ function register(server, mgr) {
|
|
|
8
8
|
'minScore default (0.3) for higher recall. Use at the start of a task to find applicable recipes.',
|
|
9
9
|
inputSchema: {
|
|
10
10
|
context: zod_1.z.string().describe('Description of the current task or context to match skills against'),
|
|
11
|
-
topK: zod_1.z.number().optional().describe('How many top similar skills to use as seeds (default 5)'),
|
|
11
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar skills to use as seeds (default 5)'),
|
|
12
12
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.3)'),
|
|
13
13
|
},
|
|
14
14
|
}, async ({ context, topK, minScore }) => {
|
|
@@ -7,7 +7,11 @@ function register(server, mgr) {
|
|
|
7
7
|
description: 'Remove an attachment from a skill. The file is deleted from disk.',
|
|
8
8
|
inputSchema: {
|
|
9
9
|
skillId: zod_1.z.string().describe('ID of the skill'),
|
|
10
|
-
filename: zod_1.z.string().
|
|
10
|
+
filename: zod_1.z.string().min(1).max(255)
|
|
11
|
+
.refine(s => !/[/\\]/.test(s), 'Filename must not contain path separators')
|
|
12
|
+
.refine(s => !s.includes('..'), 'Filename must not contain ..')
|
|
13
|
+
.refine(s => !s.includes('\0'), 'Filename must not contain null bytes')
|
|
14
|
+
.describe('Filename of the attachment to remove'),
|
|
11
15
|
},
|
|
12
16
|
}, async ({ skillId, filename }) => {
|
|
13
17
|
const ok = mgr.removeAttachment(skillId, filename);
|
|
@@ -5,15 +5,16 @@ const zod_1 = require("zod");
|
|
|
5
5
|
function register(server, mgr) {
|
|
6
6
|
server.registerTool('search_skills', {
|
|
7
7
|
description: 'Semantic search over the skill graph. ' +
|
|
8
|
-
'
|
|
8
|
+
'Supports three modes: hybrid (default, BM25 + vector), vector, keyword. ' +
|
|
9
|
+
'Finds the most relevant skills, then expands results ' +
|
|
9
10
|
'by traversing relations between skills (graph walk). ' +
|
|
10
11
|
'Returns an array sorted by relevance score (0–1), each with: ' +
|
|
11
|
-
'id, title, description, tags, source, confidence, score.',
|
|
12
|
+
'id, title, description, tags, source, confidence, usageCount, score.',
|
|
12
13
|
inputSchema: {
|
|
13
14
|
query: zod_1.z.string().describe('Natural language search query'),
|
|
14
|
-
topK: zod_1.z.number().optional().describe('How many top similar skills to use as seeds (default 5)'),
|
|
15
|
-
bfsDepth: zod_1.z.number().optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
|
|
16
|
-
maxResults: zod_1.z.number().optional().describe('Maximum number of results to return (default 20)'),
|
|
15
|
+
topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar skills to use as seeds (default 5)'),
|
|
16
|
+
bfsDepth: zod_1.z.number().min(0).max(10).optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
|
|
17
|
+
maxResults: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 20)'),
|
|
17
18
|
minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.5)'),
|
|
18
19
|
bfsDecay: zod_1.z.number().min(0).max(1).optional().describe('Score multiplier per hop (default 0.8)'),
|
|
19
20
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional().describe('Search mode: hybrid (default, BM25 + vector), vector (embedding only), keyword (BM25 only)'),
|
|
@@ -17,11 +17,22 @@ function register(server, mgr) {
|
|
|
17
17
|
filePath: zod_1.z.string().describe('Absolute path to the file on disk'),
|
|
18
18
|
},
|
|
19
19
|
}, async ({ taskId, filePath }) => {
|
|
20
|
-
|
|
20
|
+
const resolved = path_1.default.resolve(filePath);
|
|
21
|
+
let stat;
|
|
22
|
+
try {
|
|
23
|
+
stat = fs_1.default.statSync(resolved);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
21
26
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File not found' }) }], isError: true };
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
if (!stat.isFile()) {
|
|
29
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
32
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
const data = fs_1.default.readFileSync(resolved);
|
|
35
|
+
const filename = path_1.default.basename(resolved);
|
|
25
36
|
const meta = mgr.addAttachment(taskId, filename, data);
|
|
26
37
|
if (!meta) {
|
|
27
38
|
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Task not found or no project dir' }) }], isError: true };
|
|
@@ -10,7 +10,7 @@ function register(server, mgr) {
|
|
|
10
10
|
inputSchema: {
|
|
11
11
|
taskId: zod_1.z.string().describe('Source task ID'),
|
|
12
12
|
targetId: zod_1.z.string().describe('Target node ID in the external graph (e.g. "src/auth.ts::login", "api.md::Setup", "my-note")'),
|
|
13
|
-
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge'])
|
|
13
|
+
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'skills'])
|
|
14
14
|
.describe('Which graph the target belongs to'),
|
|
15
15
|
kind: zod_1.z.string().describe('Relation type, e.g. "references", "fixes", "implements"'),
|
|
16
16
|
projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
|
|
@@ -9,7 +9,7 @@ function register(server, mgr) {
|
|
|
9
9
|
inputSchema: {
|
|
10
10
|
taskId: zod_1.z.string().describe('Source task ID'),
|
|
11
11
|
targetId: zod_1.z.string().describe('Target node ID in the external graph'),
|
|
12
|
-
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge'])
|
|
12
|
+
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'skills'])
|
|
13
13
|
.describe('Which graph the target belongs to'),
|
|
14
14
|
projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
|
|
15
15
|
},
|
|
@@ -10,7 +10,7 @@ function register(server, mgr) {
|
|
|
10
10
|
'Use get_task to fetch full content of a returned task.',
|
|
11
11
|
inputSchema: {
|
|
12
12
|
targetId: zod_1.z.string().describe('Target node ID in the external graph'),
|
|
13
|
-
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge'])
|
|
13
|
+
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'skills'])
|
|
14
14
|
.describe('Which graph the target belongs to'),
|
|
15
15
|
kind: zod_1.z.string().optional().describe('Filter by relation kind. If omitted, returns all relations.'),
|
|
16
16
|
projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
|
|
@@ -7,7 +7,11 @@ function register(server, mgr) {
|
|
|
7
7
|
description: 'Remove an attachment from a task. The file is deleted from disk.',
|
|
8
8
|
inputSchema: {
|
|
9
9
|
taskId: zod_1.z.string().describe('ID of the task'),
|
|
10
|
-
filename: zod_1.z.string().
|
|
10
|
+
filename: zod_1.z.string().min(1).max(255)
|
|
11
|
+
.refine(s => !/[/\\]/.test(s), 'Filename must not contain path separators')
|
|
12
|
+
.refine(s => !s.includes('..'), 'Filename must not contain ..')
|
|
13
|
+
.refine(s => !s.includes('\0'), 'Filename must not contain null bytes')
|
|
14
|
+
.describe('Filename of the attachment to remove'),
|
|
11
15
|
},
|
|
12
16
|
}, async ({ taskId, filename }) => {
|
|
13
17
|
const ok = mgr.removeAttachment(taskId, filename);
|