@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.
Files changed (111) hide show
  1. package/LICENSE +84 -12
  2. package/README.md +7 -1
  3. package/dist/api/index.js +151 -54
  4. package/dist/api/rest/code.js +2 -1
  5. package/dist/api/rest/docs.js +2 -1
  6. package/dist/api/rest/embed.js +8 -1
  7. package/dist/api/rest/index.js +39 -18
  8. package/dist/api/rest/knowledge.js +4 -2
  9. package/dist/api/rest/skills.js +2 -1
  10. package/dist/api/rest/tasks.js +2 -1
  11. package/dist/api/rest/tools.js +8 -1
  12. package/dist/api/rest/validation.js +41 -40
  13. package/dist/api/rest/websocket.js +24 -7
  14. package/dist/api/tools/code/search-code.js +12 -9
  15. package/dist/api/tools/code/search-files.js +1 -1
  16. package/dist/api/tools/docs/cross-references.js +3 -2
  17. package/dist/api/tools/docs/explain-symbol.js +2 -1
  18. package/dist/api/tools/docs/find-examples.js +2 -1
  19. package/dist/api/tools/docs/search-files.js +1 -1
  20. package/dist/api/tools/docs/search-snippets.js +1 -1
  21. package/dist/api/tools/docs/search.js +5 -4
  22. package/dist/api/tools/file-index/search-all-files.js +1 -1
  23. package/dist/api/tools/knowledge/add-attachment.js +15 -3
  24. package/dist/api/tools/knowledge/remove-attachment.js +5 -1
  25. package/dist/api/tools/knowledge/search-notes.js +5 -4
  26. package/dist/api/tools/skills/add-attachment.js +15 -3
  27. package/dist/api/tools/skills/recall-skills.js +1 -1
  28. package/dist/api/tools/skills/remove-attachment.js +5 -1
  29. package/dist/api/tools/skills/search-skills.js +6 -5
  30. package/dist/api/tools/tasks/add-attachment.js +15 -3
  31. package/dist/api/tools/tasks/remove-attachment.js +5 -1
  32. package/dist/api/tools/tasks/search-tasks.js +5 -4
  33. package/dist/cli/index.js +63 -52
  34. package/dist/cli/indexer.js +62 -29
  35. package/dist/graphs/attachment-types.js +5 -0
  36. package/dist/graphs/code.js +99 -10
  37. package/dist/graphs/docs.js +20 -5
  38. package/dist/graphs/file-index.js +22 -6
  39. package/dist/graphs/file-lang.js +1 -1
  40. package/dist/graphs/knowledge.js +31 -7
  41. package/dist/graphs/skill.js +35 -9
  42. package/dist/graphs/task.js +35 -9
  43. package/dist/lib/defaults.js +78 -0
  44. package/dist/lib/embedder.js +11 -12
  45. package/dist/lib/embedding-codec.js +63 -0
  46. package/dist/lib/graph-persistence.js +68 -0
  47. package/dist/lib/jwt.js +4 -4
  48. package/dist/lib/mirror-watcher.js +4 -3
  49. package/dist/lib/multi-config.js +6 -1
  50. package/dist/lib/parsers/code.js +158 -31
  51. package/dist/lib/parsers/codeblock.js +11 -6
  52. package/dist/lib/parsers/docs.js +60 -31
  53. package/dist/lib/parsers/languages/registry.js +2 -2
  54. package/dist/lib/parsers/languages/typescript.js +214 -46
  55. package/dist/lib/project-manager.js +21 -11
  56. package/dist/lib/search/bm25.js +23 -5
  57. package/dist/lib/search/code.js +13 -3
  58. package/dist/lib/search/docs.js +2 -1
  59. package/dist/lib/search/file-index.js +2 -1
  60. package/dist/lib/search/files.js +3 -2
  61. package/dist/lib/search/knowledge.js +2 -1
  62. package/dist/lib/search/skills.js +2 -1
  63. package/dist/lib/search/tasks.js +2 -1
  64. package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
  65. package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
  66. package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
  67. package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
  68. package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
  69. package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
  70. package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
  71. package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
  72. package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
  73. package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
  74. package/dist/ui/assets/api-BMnBjMMf.js +1 -0
  75. package/dist/ui/assets/api-BlFF6gX-.js +1 -0
  76. package/dist/ui/assets/api-CrGJOcaN.js +1 -0
  77. package/dist/ui/assets/api-DuX-0a_X.js +1 -0
  78. package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
  79. package/dist/ui/assets/client-Bq88u7gN.js +1 -0
  80. package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
  81. package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
  82. package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
  83. package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
  84. package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
  85. package/dist/ui/assets/files-0bPg6NH9.js +1 -0
  86. package/dist/ui/assets/graph-DXGud_wF.js +1 -0
  87. package/dist/ui/assets/help-CEMQqZUR.js +891 -0
  88. package/dist/ui/assets/help-DJ52_fxN.js +1 -0
  89. package/dist/ui/assets/index-BCZDAYZi.js +2 -0
  90. package/dist/ui/assets/index-D6zSNtzo.css +1 -0
  91. package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
  92. package/dist/ui/assets/new-CpD7hOBA.js +1 -0
  93. package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
  94. package/dist/ui/assets/new-s8c0M75X.js +1 -0
  95. package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
  96. package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
  97. package/dist/ui/assets/search-EpJhdP2a.js +1 -0
  98. package/dist/ui/assets/skill-y9pizyqE.js +1 -0
  99. package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
  100. package/dist/ui/assets/tasks-CobouTKV.js +1 -0
  101. package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
  102. package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
  103. package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
  104. package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
  105. package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
  106. package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
  107. package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
  108. package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
  109. package/dist/ui/index.html +11 -3
  110. package/package.json +6 -3
  111. package/dist/ui/assets/index-0hRezICt.js +0 -1702
@@ -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 } : { 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: 60_000, max: rl.global, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg }));
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: 60_000, max: rl.search, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg });
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: 60_000, max: rl.auth, standardHeaders: true, legacyHeaders: false, message: rateLimitMsg }));
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 + access level for current user
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
- const access = serverConfig
208
+ let access = serverConfig
207
209
  ? (0, access_1.resolveAccess)(userId, gn, p.config, serverConfig, ws?.config)
208
210
  : 'rw';
209
- graphs[gn] = { enabled: gc[gn].enabled, access: gc[gn].enabled ? access : null };
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
- const access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
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
- const access = (0, access_1.resolveAccess)(userId, graphName, p.config, serverConfig, ws?.config);
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 static files — check dist/ui/ (npm package) then ui/dist/ (dev)
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
- app.use(express_1.default.static(uiDist));
346
- // SPA fallback: serve index.html for non-API routes
347
- app.get('/{*splat}', (_req, res, next) => {
348
- if (_req.path.startsWith('/api/'))
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(path_1.default.join(uiDist, 'index.html'), (err) => {
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 upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
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
- res.json(note);
56
+ const { embedding: _, ...rest } = note;
57
+ res.json(rest);
56
58
  }
57
59
  catch (err) {
58
60
  next(err);
@@ -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 upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
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) {
@@ -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 upload = (0, multer_1.default)({ storage: multer_1.default.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
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) {
@@ -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(500),
24
- content: zod_1.z.string().max(1_000_000),
25
- tags: zod_1.z.array(zod_1.z.string().max(100)).max(100).optional().default([]),
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(500).optional(),
29
- content: zod_1.z.string().max(1_000_000).optional(),
30
- tags: zod_1.z.array(zod_1.z.string().max(100)).max(100).optional(),
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(2000),
42
- topK: zod_1.z.coerce.number().int().positive().max(500).optional(),
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(500),
56
- description: zod_1.z.string().max(500_000).default(''),
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(100)).max(100).optional().default([]),
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(100).nullable().optional(),
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(500).optional(),
66
- description: zod_1.z.string().max(500_000).optional(),
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(100)).max(100).optional(),
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(100).nullable().optional(),
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(2000),
88
- topK: zod_1.z.coerce.number().int().positive().max(500).optional(),
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(2000),
105
- topK: zod_1.z.coerce.number().int().positive().max(500).optional(),
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(500),
131
- description: zod_1.z.string().max(500_000).default(''),
132
- steps: zod_1.z.array(zod_1.z.string().max(10_000)).max(100).optional().default([]),
133
- triggers: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional().default([]),
134
- inputHints: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional().default([]),
135
- filePatterns: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional().default([]),
136
- tags: zod_1.z.array(zod_1.z.string().max(100)).max(100).optional().default([]),
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(500).optional(),
142
- description: zod_1.z.string().max(500_000).optional(),
143
- steps: zod_1.z.array(zod_1.z.string().max(10_000)).max(100).optional(),
144
- triggers: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional(),
145
- inputHints: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional(),
146
- filePatterns: zod_1.z.array(zod_1.z.string().max(500)).max(50).optional(),
147
- tags: zod_1.z.array(zod_1.z.string().max(100)).max(100).optional(),
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(2000),
161
- topK: zod_1.z.coerce.number().int().positive().max(500).optional(),
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(500),
183
- kind: zod_1.z.string().max(100).optional(),
184
- projectId: zod_1.z.string().max(200).optional(),
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(255)
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
- projectManager.on(eventType, (data) => {
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
- projectManager.on('graph:updated', (data) => {
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
- }, 1000);
93
+ }, defaults_1.WS_DEBOUNCE_MS);
89
94
  }
90
- });
91
- return wss;
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
- 'Finds the most relevant symbols (functions, classes, types) by matching ' +
9
- 'against their signatures and doc comments using vector similarity, ' +
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
- 'Pass an id to get_symbol to read the full implementation.',
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.5)'),
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.includes(symbol))
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.includes(symbol))
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.includes(symbol))
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
- 'Finds the most relevant sections using vector similarity, then expands results ' +
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)'),