@graphmemory/server 1.2.0 → 1.3.1
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 +7 -1
- package/dist/api/index.js +151 -54
- package/dist/api/rest/code.js +2 -1
- package/dist/api/rest/docs.js +2 -1
- package/dist/api/rest/embed.js +8 -1
- package/dist/api/rest/index.js +39 -18
- package/dist/api/rest/knowledge.js +4 -2
- package/dist/api/rest/skills.js +2 -1
- package/dist/api/rest/tasks.js +2 -1
- package/dist/api/rest/tools.js +8 -1
- package/dist/api/rest/validation.js +41 -40
- package/dist/api/rest/websocket.js +24 -7
- 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 +15 -3
- 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 +15 -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 +15 -3
- 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 +63 -52
- package/dist/cli/indexer.js +62 -29
- package/dist/graphs/attachment-types.js +5 -0
- package/dist/graphs/code.js +99 -10
- package/dist/graphs/docs.js +20 -5
- package/dist/graphs/file-index.js +22 -6
- package/dist/graphs/file-lang.js +1 -1
- package/dist/graphs/knowledge.js +31 -7
- package/dist/graphs/skill.js +35 -9
- package/dist/graphs/task.js +35 -9
- package/dist/lib/defaults.js +78 -0
- package/dist/lib/embedder.js +11 -12
- package/dist/lib/embedding-codec.js +63 -0
- package/dist/lib/graph-persistence.js +68 -0
- package/dist/lib/jwt.js +4 -4
- package/dist/lib/mirror-watcher.js +4 -3
- package/dist/lib/multi-config.js +6 -1
- package/dist/lib/parsers/code.js +158 -31
- package/dist/lib/parsers/codeblock.js +11 -6
- package/dist/lib/parsers/docs.js +60 -31
- package/dist/lib/parsers/languages/registry.js +2 -2
- package/dist/lib/parsers/languages/typescript.js +214 -46
- package/dist/lib/project-manager.js +21 -11
- package/dist/lib/search/bm25.js +23 -5
- package/dist/lib/search/code.js +13 -3
- package/dist/lib/search/docs.js +2 -1
- package/dist/lib/search/file-index.js +2 -1
- package/dist/lib/search/files.js +3 -2
- package/dist/lib/search/knowledge.js +2 -1
- package/dist/lib/search/skills.js +2 -1
- package/dist/lib/search/tasks.js +2 -1
- 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 +6 -3
- package/dist/ui/assets/index-0hRezICt.js +0 -1702
package/dist/api/rest/index.js
CHANGED
|
@@ -24,6 +24,7 @@ const graph_1 = require("../../api/rest/graph");
|
|
|
24
24
|
const tools_1 = require("../../api/rest/tools");
|
|
25
25
|
const embed_1 = require("../../api/rest/embed");
|
|
26
26
|
const team_1 = require("../../lib/team");
|
|
27
|
+
const defaults_1 = require("../../lib/defaults");
|
|
27
28
|
/**
|
|
28
29
|
* Express middleware: reject if accessLevel (set by requireGraphAccess) is not 'rw'.
|
|
29
30
|
* Use on POST/PUT/DELETE routes inside domain routers.
|
|
@@ -46,7 +47,7 @@ function createRestApp(projectManager, options) {
|
|
|
46
47
|
const users = options?.users ?? {};
|
|
47
48
|
const hasUsers = Object.keys(users).length > 0;
|
|
48
49
|
const corsOrigins = serverConfig?.corsOrigins;
|
|
49
|
-
app.use((0, cors_1.default)(corsOrigins?.length ? { origin: corsOrigins, credentials: true } : {
|
|
50
|
+
app.use((0, cors_1.default)(corsOrigins?.length ? { origin: corsOrigins, credentials: true } : {}));
|
|
50
51
|
app.use(express_1.default.json({ limit: '10mb' }));
|
|
51
52
|
app.use((0, cookie_parser_1.default)());
|
|
52
53
|
// Security headers
|
|
@@ -59,10 +60,10 @@ function createRestApp(projectManager, options) {
|
|
|
59
60
|
const rl = serverConfig?.rateLimit;
|
|
60
61
|
const rateLimitMsg = { error: 'Too many requests, please try again later' };
|
|
61
62
|
if (rl && rl.global > 0) {
|
|
62
|
-
app.use('/api/', (0, express_rate_limit_1.default)({ windowMs:
|
|
63
|
+
app.use('/api/', (0, express_rate_limit_1.default)({ windowMs: defaults_1.RATE_LIMIT_WINDOW_MS, max: rl.global, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg }));
|
|
63
64
|
}
|
|
64
65
|
if (rl && rl.search > 0) {
|
|
65
|
-
const searchLimiter = (0, express_rate_limit_1.default)({ windowMs:
|
|
66
|
+
const searchLimiter = (0, express_rate_limit_1.default)({ windowMs: defaults_1.RATE_LIMIT_WINDOW_MS, max: rl.search, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg });
|
|
66
67
|
app.use('/api/projects/:projectId/knowledge/search', searchLimiter);
|
|
67
68
|
app.use('/api/projects/:projectId/tasks/search', searchLimiter);
|
|
68
69
|
app.use('/api/projects/:projectId/skills/search', searchLimiter);
|
|
@@ -72,7 +73,7 @@ function createRestApp(projectManager, options) {
|
|
|
72
73
|
app.use('/api/embed', searchLimiter);
|
|
73
74
|
}
|
|
74
75
|
if (rl && rl.auth > 0) {
|
|
75
|
-
app.use('/api/auth/login', (0, express_rate_limit_1.default)({ windowMs:
|
|
76
|
+
app.use('/api/auth/login', (0, express_rate_limit_1.default)({ windowMs: defaults_1.RATE_LIMIT_WINDOW_MS, max: rl.auth, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg }));
|
|
76
77
|
}
|
|
77
78
|
const jwtSecret = serverConfig?.jwtSecret;
|
|
78
79
|
const accessTokenTtl = serverConfig?.accessTokenTtl ?? '15m';
|
|
@@ -89,7 +90,7 @@ function createRestApp(projectManager, options) {
|
|
|
89
90
|
const payload = (0, jwt_1.verifyToken)(accessToken, jwtSecret);
|
|
90
91
|
if (payload?.type === 'access' && users[payload.userId]) {
|
|
91
92
|
const user = users[payload.userId];
|
|
92
|
-
return res.json({ required: true, authenticated: true, userId: payload.userId, name: user.name });
|
|
93
|
+
return res.json({ required: true, authenticated: true, userId: payload.userId, name: user.name, apiKey: user.apiKey });
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
}
|
|
@@ -170,7 +171,7 @@ function createRestApp(projectManager, options) {
|
|
|
170
171
|
}
|
|
171
172
|
// 2. Bearer apiKey (from MCP/API clients)
|
|
172
173
|
const auth = req.headers.authorization;
|
|
173
|
-
if (auth?.startsWith('Bearer ')) {
|
|
174
|
+
if (auth?.startsWith('Bearer ') && auth.length > 7) {
|
|
174
175
|
const apiKey = auth.slice(7);
|
|
175
176
|
const result = (0, access_1.resolveUserFromApiKey)(apiKey, users);
|
|
176
177
|
if (result) {
|
|
@@ -178,6 +179,7 @@ function createRestApp(projectManager, options) {
|
|
|
178
179
|
req.user = result.user;
|
|
179
180
|
return next();
|
|
180
181
|
}
|
|
182
|
+
// Invalid Bearer token — reject (explicit auth attempt should not fall through)
|
|
181
183
|
return _res.status(401).json({ error: 'Invalid API key' });
|
|
182
184
|
}
|
|
183
185
|
// 3. No auth = anonymous (uses defaultAccess)
|
|
@@ -200,13 +202,16 @@ function createRestApp(projectManager, options) {
|
|
|
200
202
|
const p = projectManager.getProject(id);
|
|
201
203
|
const gc = p.config.graphConfigs;
|
|
202
204
|
const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
|
|
203
|
-
// Per-graph info: enabled
|
|
205
|
+
// Per-graph info: enabled, readonly, access level for current user
|
|
204
206
|
const graphs = {};
|
|
205
207
|
for (const gn of multi_config_1.GRAPH_NAMES) {
|
|
206
|
-
|
|
208
|
+
let access = serverConfig
|
|
207
209
|
? (0, access_1.resolveAccess)(userId, gn, p.config, serverConfig, ws?.config)
|
|
208
210
|
: 'rw';
|
|
209
|
-
|
|
211
|
+
// Cap access for readonly graphs
|
|
212
|
+
if (access === 'rw' && gc[gn].readonly)
|
|
213
|
+
access = 'r';
|
|
214
|
+
graphs[gn] = { enabled: gc[gn].enabled, readonly: gc[gn].readonly, access: gc[gn].enabled ? access : null };
|
|
210
215
|
}
|
|
211
216
|
return {
|
|
212
217
|
id,
|
|
@@ -280,14 +285,23 @@ function createRestApp(projectManager, options) {
|
|
|
280
285
|
// Middleware: check access level for a graph (read or read-write)
|
|
281
286
|
function requireGraphAccess(graphName, level) {
|
|
282
287
|
return (req, _res, next) => {
|
|
288
|
+
const p = req.project;
|
|
289
|
+
// Graph-level readonly: enforce even without auth config
|
|
290
|
+
const isReadonly = p?.config.graphConfigs[graphName]?.readonly;
|
|
291
|
+
if (isReadonly) {
|
|
292
|
+
req.accessLevel = 'r';
|
|
293
|
+
}
|
|
283
294
|
if (!serverConfig)
|
|
284
295
|
return next(); // no config = no auth enforcement
|
|
285
|
-
const p = req.project;
|
|
286
296
|
if (!p)
|
|
287
297
|
return next();
|
|
288
298
|
const userId = req.userId;
|
|
289
299
|
const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
|
|
290
|
-
|
|
300
|
+
let access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
|
|
301
|
+
// Graph-level readonly: cap to 'r' regardless of user permissions
|
|
302
|
+
if (access === 'rw' && p.config.graphConfigs[graphName]?.readonly) {
|
|
303
|
+
access = 'r';
|
|
304
|
+
}
|
|
291
305
|
if (!(0, access_1.canRead)(access)) {
|
|
292
306
|
return _res.status(403).json({ error: 'Access denied' });
|
|
293
307
|
}
|
|
@@ -329,7 +343,9 @@ function createRestApp(projectManager, options) {
|
|
|
329
343
|
return true;
|
|
330
344
|
const userId = req.userId;
|
|
331
345
|
const ws = p.workspaceId ? projectManager.getWorkspace(p.workspaceId) : undefined;
|
|
332
|
-
|
|
346
|
+
let access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
|
|
347
|
+
if (access === 'rw' && p.config.graphConfigs[graphName]?.readonly)
|
|
348
|
+
access = 'r';
|
|
333
349
|
if (level === 'rw')
|
|
334
350
|
return (0, access_1.canWrite)(access);
|
|
335
351
|
return (0, access_1.canRead)(access);
|
|
@@ -338,16 +354,21 @@ function createRestApp(projectManager, options) {
|
|
|
338
354
|
if (serverConfig?.embeddingApi?.enabled && options?.embeddingApiModelName) {
|
|
339
355
|
app.use('/api/embed', (0, embed_1.createEmbedRouter)(serverConfig.embeddingApi, options.embeddingApiModelName));
|
|
340
356
|
}
|
|
341
|
-
// Serve UI
|
|
357
|
+
// Serve UI at /ui/ path — check dist/ui/ (npm package) then ui/dist/ (dev)
|
|
342
358
|
const uiDistPkg = path_1.default.resolve(__dirname, '../../ui');
|
|
343
359
|
const uiDistDev = path_1.default.resolve(__dirname, '../../../ui/dist');
|
|
344
360
|
const uiDist = fs_1.default.existsSync(uiDistPkg) ? uiDistPkg : uiDistDev;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
361
|
+
// Redirect root to /ui/
|
|
362
|
+
app.get('/', (_req, res) => { res.redirect('/ui/'); });
|
|
363
|
+
// Static files under /ui/
|
|
364
|
+
app.use('/ui', express_1.default.static(uiDist, { redirect: false, index: false }));
|
|
365
|
+
// SPA fallback: serve index.html for all /ui/* routes
|
|
366
|
+
const indexHtml = path_1.default.join(uiDist, 'index.html');
|
|
367
|
+
app.use('/ui', (_req, res, next) => {
|
|
368
|
+
// Skip requests for actual files (assets with extensions like .js, .css, .png)
|
|
369
|
+
if (_req.path.includes('.') && !_req.path.endsWith('.html'))
|
|
349
370
|
return next();
|
|
350
|
-
res.sendFile(
|
|
371
|
+
res.sendFile(indexHtml, (err) => {
|
|
351
372
|
if (err)
|
|
352
373
|
next();
|
|
353
374
|
});
|
|
@@ -11,7 +11,8 @@ const multer_1 = __importDefault(require("multer"));
|
|
|
11
11
|
const validation_1 = require("../../api/rest/validation");
|
|
12
12
|
const index_1 = require("../../api/rest/index");
|
|
13
13
|
const manager_types_1 = require("../../graphs/manager-types");
|
|
14
|
-
const
|
|
14
|
+
const defaults_1 = require("../../lib/defaults");
|
|
15
|
+
const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: defaults_1.MAX_UPLOAD_SIZE } });
|
|
15
16
|
function createKnowledgeRouter() {
|
|
16
17
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
17
18
|
function getProject(req) {
|
|
@@ -52,7 +53,8 @@ function createKnowledgeRouter() {
|
|
|
52
53
|
const note = p.knowledgeManager.getNote(req.params.noteId);
|
|
53
54
|
if (!note)
|
|
54
55
|
return res.status(404).json({ error: 'Note not found' });
|
|
55
|
-
|
|
56
|
+
const { embedding: _, ...rest } = note;
|
|
57
|
+
res.json(rest);
|
|
56
58
|
}
|
|
57
59
|
catch (err) {
|
|
58
60
|
next(err);
|
package/dist/api/rest/skills.js
CHANGED
|
@@ -11,7 +11,8 @@ const multer_1 = __importDefault(require("multer"));
|
|
|
11
11
|
const validation_1 = require("../../api/rest/validation");
|
|
12
12
|
const index_1 = require("../../api/rest/index");
|
|
13
13
|
const manager_types_1 = require("../../graphs/manager-types");
|
|
14
|
-
const
|
|
14
|
+
const defaults_1 = require("../../lib/defaults");
|
|
15
|
+
const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: defaults_1.MAX_UPLOAD_SIZE } });
|
|
15
16
|
function createSkillsRouter() {
|
|
16
17
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
17
18
|
function getProject(req) {
|
package/dist/api/rest/tasks.js
CHANGED
|
@@ -11,7 +11,8 @@ const multer_1 = __importDefault(require("multer"));
|
|
|
11
11
|
const validation_1 = require("../../api/rest/validation");
|
|
12
12
|
const index_1 = require("../../api/rest/index");
|
|
13
13
|
const manager_types_1 = require("../../graphs/manager-types");
|
|
14
|
-
const
|
|
14
|
+
const defaults_1 = require("../../lib/defaults");
|
|
15
|
+
const upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: defaults_1.MAX_UPLOAD_SIZE } });
|
|
15
16
|
function createTasksRouter() {
|
|
16
17
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
17
18
|
function getProject(req) {
|
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);
|
|
@@ -4,6 +4,7 @@ exports.attachmentFilenameSchema = exports.linkedQuerySchema = exports.graphExpo
|
|
|
4
4
|
exports.validateBody = validateBody;
|
|
5
5
|
exports.validateQuery = validateQuery;
|
|
6
6
|
const zod_1 = require("zod");
|
|
7
|
+
const defaults_1 = require("../../lib/defaults");
|
|
7
8
|
function validateBody(schema) {
|
|
8
9
|
return (req, _res, next) => {
|
|
9
10
|
req.body = schema.parse(req.body);
|
|
@@ -20,14 +21,14 @@ function validateQuery(schema) {
|
|
|
20
21
|
// Knowledge schemas
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
exports.createNoteSchema = zod_1.z.object({
|
|
23
|
-
title: zod_1.z.string().min(1).max(
|
|
24
|
-
content: zod_1.z.string().max(
|
|
25
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
24
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN),
|
|
25
|
+
content: zod_1.z.string().max(defaults_1.MAX_NOTE_CONTENT_LEN),
|
|
26
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional().default([]),
|
|
26
27
|
});
|
|
27
28
|
exports.updateNoteSchema = zod_1.z.object({
|
|
28
|
-
title: zod_1.z.string().min(1).max(
|
|
29
|
-
content: zod_1.z.string().max(
|
|
30
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
29
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN).optional(),
|
|
30
|
+
content: zod_1.z.string().max(defaults_1.MAX_NOTE_CONTENT_LEN).optional(),
|
|
31
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional(),
|
|
31
32
|
version: zod_1.z.number().int().positive().optional(),
|
|
32
33
|
});
|
|
33
34
|
exports.createRelationSchema = zod_1.z.object({
|
|
@@ -38,8 +39,8 @@ exports.createRelationSchema = zod_1.z.object({
|
|
|
38
39
|
projectId: zod_1.z.string().min(1).optional(),
|
|
39
40
|
});
|
|
40
41
|
exports.noteSearchSchema = zod_1.z.object({
|
|
41
|
-
q: zod_1.z.string().min(1).max(
|
|
42
|
-
topK: zod_1.z.coerce.number().int().positive().max(
|
|
42
|
+
q: zod_1.z.string().min(1).max(defaults_1.MAX_SEARCH_QUERY_LEN),
|
|
43
|
+
topK: zod_1.z.coerce.number().int().positive().max(defaults_1.MAX_SEARCH_TOP_K).optional(),
|
|
43
44
|
minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
|
|
44
45
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
|
|
45
46
|
});
|
|
@@ -52,24 +53,24 @@ exports.noteListSchema = zod_1.z.object({
|
|
|
52
53
|
// Task schemas
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
54
55
|
exports.createTaskSchema = zod_1.z.object({
|
|
55
|
-
title: zod_1.z.string().min(1).max(
|
|
56
|
-
description: zod_1.z.string().max(
|
|
56
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN),
|
|
57
|
+
description: zod_1.z.string().max(defaults_1.MAX_DESCRIPTION_LEN).default(''),
|
|
57
58
|
status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']).default('todo'),
|
|
58
59
|
priority: zod_1.z.enum(['critical', 'high', 'medium', 'low']).default('medium'),
|
|
59
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
60
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional().default([]),
|
|
60
61
|
dueDate: zod_1.z.number().nullable().optional(),
|
|
61
62
|
estimate: zod_1.z.number().nullable().optional(),
|
|
62
|
-
assignee: zod_1.z.string().max(
|
|
63
|
+
assignee: zod_1.z.string().max(defaults_1.MAX_ASSIGNEE_LEN).nullable().optional(),
|
|
63
64
|
});
|
|
64
65
|
exports.updateTaskSchema = zod_1.z.object({
|
|
65
|
-
title: zod_1.z.string().min(1).max(
|
|
66
|
-
description: zod_1.z.string().max(
|
|
66
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN).optional(),
|
|
67
|
+
description: zod_1.z.string().max(defaults_1.MAX_DESCRIPTION_LEN).optional(),
|
|
67
68
|
status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']).optional(),
|
|
68
69
|
priority: zod_1.z.enum(['critical', 'high', 'medium', 'low']).optional(),
|
|
69
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
70
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional(),
|
|
70
71
|
dueDate: zod_1.z.number().nullable().optional(),
|
|
71
72
|
estimate: zod_1.z.number().nullable().optional(),
|
|
72
|
-
assignee: zod_1.z.string().max(
|
|
73
|
+
assignee: zod_1.z.string().max(defaults_1.MAX_ASSIGNEE_LEN).nullable().optional(),
|
|
73
74
|
version: zod_1.z.number().int().positive().optional(),
|
|
74
75
|
});
|
|
75
76
|
exports.moveTaskSchema = zod_1.z.object({
|
|
@@ -84,8 +85,8 @@ exports.createTaskLinkSchema = zod_1.z.object({
|
|
|
84
85
|
projectId: zod_1.z.string().min(1).optional(),
|
|
85
86
|
});
|
|
86
87
|
exports.taskSearchSchema = zod_1.z.object({
|
|
87
|
-
q: zod_1.z.string().min(1).max(
|
|
88
|
-
topK: zod_1.z.coerce.number().int().positive().max(
|
|
88
|
+
q: zod_1.z.string().min(1).max(defaults_1.MAX_SEARCH_QUERY_LEN),
|
|
89
|
+
topK: zod_1.z.coerce.number().int().positive().max(defaults_1.MAX_SEARCH_TOP_K).optional(),
|
|
89
90
|
minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
|
|
90
91
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
|
|
91
92
|
});
|
|
@@ -101,8 +102,8 @@ exports.taskListSchema = zod_1.z.object({
|
|
|
101
102
|
// Search schemas (docs, code, files)
|
|
102
103
|
// ---------------------------------------------------------------------------
|
|
103
104
|
exports.searchQuerySchema = zod_1.z.object({
|
|
104
|
-
q: zod_1.z.string().min(1).max(
|
|
105
|
-
topK: zod_1.z.coerce.number().int().positive().max(
|
|
105
|
+
q: zod_1.z.string().min(1).max(defaults_1.MAX_SEARCH_QUERY_LEN),
|
|
106
|
+
topK: zod_1.z.coerce.number().int().positive().max(defaults_1.MAX_SEARCH_TOP_K).optional(),
|
|
106
107
|
minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
|
|
107
108
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
|
|
108
109
|
});
|
|
@@ -127,24 +128,24 @@ exports.fileListSchema = zod_1.z.object({
|
|
|
127
128
|
// Skill schemas
|
|
128
129
|
// ---------------------------------------------------------------------------
|
|
129
130
|
exports.createSkillSchema = zod_1.z.object({
|
|
130
|
-
title: zod_1.z.string().min(1).max(
|
|
131
|
-
description: zod_1.z.string().max(
|
|
132
|
-
steps: zod_1.z.array(zod_1.z.string().max(
|
|
133
|
-
triggers: zod_1.z.array(zod_1.z.string().max(
|
|
134
|
-
inputHints: zod_1.z.array(zod_1.z.string().max(
|
|
135
|
-
filePatterns: zod_1.z.array(zod_1.z.string().max(
|
|
136
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
131
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN),
|
|
132
|
+
description: zod_1.z.string().max(defaults_1.MAX_DESCRIPTION_LEN).default(''),
|
|
133
|
+
steps: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_STEP_LEN)).max(defaults_1.MAX_SKILL_STEPS_COUNT).optional().default([]),
|
|
134
|
+
triggers: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional().default([]),
|
|
135
|
+
inputHints: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional().default([]),
|
|
136
|
+
filePatterns: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional().default([]),
|
|
137
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional().default([]),
|
|
137
138
|
source: zod_1.z.enum(['user', 'learned']).default('user'),
|
|
138
139
|
confidence: zod_1.z.number().min(0).max(1).default(1),
|
|
139
140
|
});
|
|
140
141
|
exports.updateSkillSchema = zod_1.z.object({
|
|
141
|
-
title: zod_1.z.string().min(1).max(
|
|
142
|
-
description: zod_1.z.string().max(
|
|
143
|
-
steps: zod_1.z.array(zod_1.z.string().max(
|
|
144
|
-
triggers: zod_1.z.array(zod_1.z.string().max(
|
|
145
|
-
inputHints: zod_1.z.array(zod_1.z.string().max(
|
|
146
|
-
filePatterns: zod_1.z.array(zod_1.z.string().max(
|
|
147
|
-
tags: zod_1.z.array(zod_1.z.string().max(
|
|
142
|
+
title: zod_1.z.string().min(1).max(defaults_1.MAX_TITLE_LEN).optional(),
|
|
143
|
+
description: zod_1.z.string().max(defaults_1.MAX_DESCRIPTION_LEN).optional(),
|
|
144
|
+
steps: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_STEP_LEN)).max(defaults_1.MAX_SKILL_STEPS_COUNT).optional(),
|
|
145
|
+
triggers: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional(),
|
|
146
|
+
inputHints: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional(),
|
|
147
|
+
filePatterns: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_SKILL_TRIGGER_LEN)).max(defaults_1.MAX_SKILL_TRIGGERS_COUNT).optional(),
|
|
148
|
+
tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional(),
|
|
148
149
|
source: zod_1.z.enum(['user', 'learned']).optional(),
|
|
149
150
|
confidence: zod_1.z.number().min(0).max(1).optional(),
|
|
150
151
|
version: zod_1.z.number().int().positive().optional(),
|
|
@@ -157,8 +158,8 @@ exports.createSkillLinkSchema = zod_1.z.object({
|
|
|
157
158
|
projectId: zod_1.z.string().min(1).optional(),
|
|
158
159
|
});
|
|
159
160
|
exports.skillSearchSchema = zod_1.z.object({
|
|
160
|
-
q: zod_1.z.string().min(1).max(
|
|
161
|
-
topK: zod_1.z.coerce.number().int().positive().max(
|
|
161
|
+
q: zod_1.z.string().min(1).max(defaults_1.MAX_SEARCH_QUERY_LEN),
|
|
162
|
+
topK: zod_1.z.coerce.number().int().positive().max(defaults_1.MAX_SEARCH_TOP_K).optional(),
|
|
162
163
|
minScore: zod_1.z.coerce.number().min(0).max(1).optional(),
|
|
163
164
|
searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional(),
|
|
164
165
|
});
|
|
@@ -179,9 +180,9 @@ exports.graphExportSchema = zod_1.z.object({
|
|
|
179
180
|
// ---------------------------------------------------------------------------
|
|
180
181
|
exports.linkedQuerySchema = zod_1.z.object({
|
|
181
182
|
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'tasks', 'skills']),
|
|
182
|
-
targetNodeId: zod_1.z.string().min(1).max(
|
|
183
|
-
kind: zod_1.z.string().max(
|
|
184
|
-
projectId: zod_1.z.string().max(
|
|
183
|
+
targetNodeId: zod_1.z.string().min(1).max(defaults_1.MAX_TARGET_NODE_ID_LEN),
|
|
184
|
+
kind: zod_1.z.string().max(defaults_1.MAX_LINK_KIND_LEN).optional(),
|
|
185
|
+
projectId: zod_1.z.string().max(defaults_1.MAX_PROJECT_ID_LEN).optional(),
|
|
185
186
|
});
|
|
186
187
|
// ---------------------------------------------------------------------------
|
|
187
188
|
// Attachment schemas
|
|
@@ -189,7 +190,7 @@ exports.linkedQuerySchema = zod_1.z.object({
|
|
|
189
190
|
/** Validates an attachment filename (path param). No path separators, no .., no dangerous chars. */
|
|
190
191
|
exports.attachmentFilenameSchema = zod_1.z.string()
|
|
191
192
|
.min(1)
|
|
192
|
-
.max(
|
|
193
|
+
.max(defaults_1.MAX_ATTACHMENT_FILENAME_LEN)
|
|
193
194
|
.refine(s => !/[/\\]/.test(s), 'Filename must not contain path separators')
|
|
194
195
|
.refine(s => !s.includes('..'), 'Filename must not contain ..')
|
|
195
196
|
.refine(s => !s.includes('\0'), 'Filename must not contain null bytes')
|
|
@@ -3,10 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.attachWebSocket = attachWebSocket;
|
|
4
4
|
const ws_1 = require("ws");
|
|
5
5
|
const jwt_1 = require("../../lib/jwt");
|
|
6
|
+
const defaults_1 = require("../../lib/defaults");
|
|
6
7
|
/**
|
|
7
8
|
* Attach a WebSocket server to the HTTP server at /api/ws.
|
|
8
9
|
* Broadcasts all ProjectManager events to connected clients.
|
|
9
10
|
* Each event includes projectId — clients filter on their side.
|
|
11
|
+
* Returns a handle with a cleanup function to remove all listeners.
|
|
10
12
|
*/
|
|
11
13
|
function attachWebSocket(httpServer, projectManager, options) {
|
|
12
14
|
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
@@ -55,7 +57,8 @@ function attachWebSocket(httpServer, projectManager, options) {
|
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
|
-
// Subscribe to ProjectManager events
|
|
60
|
+
// Subscribe to ProjectManager events (track handlers for cleanup)
|
|
61
|
+
const listeners = [];
|
|
59
62
|
const events = [
|
|
60
63
|
'note:created', 'note:updated', 'note:deleted',
|
|
61
64
|
'note:attachment:added', 'note:attachment:deleted',
|
|
@@ -66,14 +69,16 @@ function attachWebSocket(httpServer, projectManager, options) {
|
|
|
66
69
|
'project:indexed',
|
|
67
70
|
];
|
|
68
71
|
for (const eventType of events) {
|
|
69
|
-
|
|
72
|
+
const handler = (data) => {
|
|
70
73
|
broadcast({ projectId: data.projectId, type: eventType, data });
|
|
71
|
-
}
|
|
74
|
+
};
|
|
75
|
+
projectManager.on(eventType, handler);
|
|
76
|
+
listeners.push([eventType, handler]);
|
|
72
77
|
}
|
|
73
78
|
// Debounced graph:updated from indexer
|
|
74
79
|
let graphUpdateTimer;
|
|
75
80
|
let pendingGraphUpdates = new Map();
|
|
76
|
-
|
|
81
|
+
const graphHandler = (data) => {
|
|
77
82
|
const key = data.projectId;
|
|
78
83
|
if (!pendingGraphUpdates.has(key))
|
|
79
84
|
pendingGraphUpdates.set(key, []);
|
|
@@ -85,8 +90,20 @@ function attachWebSocket(httpServer, projectManager, options) {
|
|
|
85
90
|
}
|
|
86
91
|
pendingGraphUpdates = new Map();
|
|
87
92
|
graphUpdateTimer = undefined;
|
|
88
|
-
},
|
|
93
|
+
}, defaults_1.WS_DEBOUNCE_MS);
|
|
89
94
|
}
|
|
90
|
-
}
|
|
91
|
-
|
|
95
|
+
};
|
|
96
|
+
projectManager.on('graph:updated', graphHandler);
|
|
97
|
+
listeners.push(['graph:updated', graphHandler]);
|
|
98
|
+
function cleanup() {
|
|
99
|
+
for (const [event, handler] of listeners) {
|
|
100
|
+
projectManager.removeListener(event, handler);
|
|
101
|
+
}
|
|
102
|
+
if (graphUpdateTimer) {
|
|
103
|
+
clearTimeout(graphUpdateTimer);
|
|
104
|
+
graphUpdateTimer = undefined;
|
|
105
|
+
}
|
|
106
|
+
pendingGraphUpdates.clear();
|
|
107
|
+
}
|
|
108
|
+
return { wss, cleanup };
|
|
92
109
|
}
|
|
@@ -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)'),
|