@graphmemory/server 1.3.0 → 1.3.2

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 (73) hide show
  1. package/dist/api/index.js +4 -4
  2. package/dist/api/rest/code.js +2 -1
  3. package/dist/api/rest/docs.js +2 -1
  4. package/dist/api/rest/embed.js +8 -1
  5. package/dist/api/rest/index.js +25 -7
  6. package/dist/api/rest/knowledge.js +4 -2
  7. package/dist/api/rest/skills.js +2 -1
  8. package/dist/api/rest/tasks.js +2 -1
  9. package/dist/api/rest/validation.js +41 -40
  10. package/dist/api/rest/websocket.js +24 -7
  11. package/dist/api/tools/knowledge/add-attachment.js +2 -1
  12. package/dist/api/tools/skills/add-attachment.js +2 -1
  13. package/dist/api/tools/tasks/add-attachment.js +2 -1
  14. package/dist/cli/index.js +7 -4
  15. package/dist/cli/indexer.js +38 -25
  16. package/dist/graphs/attachment-types.js +5 -0
  17. package/dist/graphs/code.js +34 -8
  18. package/dist/graphs/docs.js +5 -3
  19. package/dist/graphs/file-index.js +5 -3
  20. package/dist/graphs/knowledge.js +11 -4
  21. package/dist/graphs/skill.js +12 -5
  22. package/dist/graphs/task.js +12 -5
  23. package/dist/lib/defaults.js +78 -0
  24. package/dist/lib/embedder.js +11 -12
  25. package/dist/lib/embedding-codec.js +3 -5
  26. package/dist/lib/graph-persistence.js +68 -0
  27. package/dist/lib/jwt.js +2 -2
  28. package/dist/lib/mirror-watcher.js +4 -3
  29. package/dist/lib/multi-config.js +3 -1
  30. package/dist/lib/parsers/docs.js +2 -1
  31. package/dist/lib/parsers/languages/typescript.js +39 -17
  32. package/dist/lib/project-manager.js +7 -1
  33. package/dist/lib/search/bm25.js +5 -4
  34. package/dist/lib/search/code.js +2 -1
  35. package/dist/lib/search/docs.js +2 -1
  36. package/dist/lib/search/file-index.js +2 -1
  37. package/dist/lib/search/files.js +3 -2
  38. package/dist/lib/search/knowledge.js +2 -1
  39. package/dist/lib/search/skills.js +2 -1
  40. package/dist/lib/search/tasks.js +2 -1
  41. package/dist/ui/assets/{NoteForm-aZX9f6-3.js → NoteForm-SQ0b93i0.js} +1 -1
  42. package/dist/ui/assets/{SkillForm-KYa3o92l.js → SkillForm-BVsGrNPb.js} +1 -1
  43. package/dist/ui/assets/{TaskForm-Bl5nkybO.js → TaskForm-DgPVeiI9.js} +1 -1
  44. package/dist/ui/assets/{_articleId_-DjbCByxM.js → _articleId_-FqdaSeYS.js} +1 -1
  45. package/dist/ui/assets/{_docId_-hdCDjclV.js → _docId_-Q0Wmjtp6.js} +1 -1
  46. package/dist/ui/assets/{_filePath_-CpG836v4.js → _filePath_-BR0gOT_z.js} +1 -1
  47. package/dist/ui/assets/{_noteId_-C1enaQd1.js → _noteId_-BMWd415J.js} +1 -1
  48. package/dist/ui/assets/{_skillId_-hPoCet7J.js → _skillId_-CsHgildJ.js} +1 -1
  49. package/dist/ui/assets/{_taskId_-DSB3dLVz.js → _taskId_-xDHTfbQw.js} +1 -1
  50. package/dist/ui/assets/{_toolName_-3SmCfxZy.js → _toolName_-BSa2uNSu.js} +1 -1
  51. package/dist/ui/assets/{attachments-CEQ-2nMo.js → attachments-NSvN5_0A.js} +1 -1
  52. package/dist/ui/assets/{docs-CrXsRcOG.js → docs-iUK8E40J.js} +1 -1
  53. package/dist/ui/assets/{edit-TUIIpUMF.js → edit-BzIJy_Oo.js} +1 -1
  54. package/dist/ui/assets/{edit-BYiy1FZy.js → edit-Dnc067B2.js} +1 -1
  55. package/dist/ui/assets/{edit-hc-ZWz3y.js → edit-U_UEI361.js} +1 -1
  56. package/dist/ui/assets/{files-0bPg6NH9.js → files-B4svJUZh.js} +1 -1
  57. package/dist/ui/assets/{graph-DXGud_wF.js → graph-CcNP1ckP.js} +1 -1
  58. package/dist/ui/assets/{help-DJ52_fxN.js → help-BJZZtKAR.js} +1 -1
  59. package/dist/ui/assets/{help-CEMQqZUR.js → help-D6XKMuzk.js} +16 -4
  60. package/dist/ui/assets/index-CEweXD9O.js +2 -0
  61. package/dist/ui/assets/{knowledge-DeygeGGH.js → knowledge-CV99ToEV.js} +1 -1
  62. package/dist/ui/assets/{new-CpD7hOBA.js → new-BypesKiP.js} +1 -1
  63. package/dist/ui/assets/{new-s8c0M75X.js → new-Dcx8wlp4.js} +1 -1
  64. package/dist/ui/assets/{new-DHTg3Dqq.js → new-Sq3NY2oa.js} +1 -1
  65. package/dist/ui/assets/{prompts-BgOmdxgM.js → prompts-DbsIe3Pm.js} +1 -1
  66. package/dist/ui/assets/{search-EpJhdP2a.js → search-D87r7lIL.js} +1 -1
  67. package/dist/ui/assets/{skill-y9pizyqE.js → skill-BltAsz7M.js} +1 -1
  68. package/dist/ui/assets/{skills-Cga9iUZN.js → skills-Dtmg2kDA.js} +1 -1
  69. package/dist/ui/assets/{tasks-CobouTKV.js → tasks-BRqIwKCG.js} +1 -1
  70. package/dist/ui/assets/{tools-JxKH5BDF.js → tools-CM3gQ4TK.js} +1 -1
  71. package/dist/ui/index.html +1 -1
  72. package/package.json +5 -2
  73. package/dist/ui/assets/index-BCZDAYZi.js +0 -2
package/dist/api/index.js CHANGED
@@ -47,6 +47,7 @@ const embedder_1 = require("../lib/embedder");
47
47
  const index_1 = require("../api/rest/index");
48
48
  const websocket_1 = require("../api/rest/websocket");
49
49
  const access_1 = require("../lib/access");
50
+ const defaults_1 = require("../lib/defaults");
50
51
  const multi_config_1 = require("../lib/multi-config");
51
52
  const docs_1 = require("../graphs/docs");
52
53
  const code_1 = require("../graphs/code");
@@ -314,13 +315,12 @@ function createMcpServer(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, ta
314
315
  // ---------------------------------------------------------------------------
315
316
  // HTTP transport (Streamable HTTP)
316
317
  // ---------------------------------------------------------------------------
317
- const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB
318
318
  async function collectBody(req) {
319
319
  const chunks = [];
320
320
  let size = 0;
321
321
  for await (const chunk of req) {
322
322
  size += chunk.length;
323
- if (size > MAX_BODY_SIZE)
323
+ if (size > defaults_1.MAX_BODY_SIZE)
324
324
  throw new Error('Request body too large');
325
325
  chunks.push(chunk);
326
326
  }
@@ -338,7 +338,7 @@ async function startHttpServer(host, port, sessionTimeoutMs, docGraph, codeGraph
338
338
  process.stderr.write(`[http] Session ${sid} timed out\n`);
339
339
  }
340
340
  }
341
- }, 60_000);
341
+ }, defaults_1.SESSION_SWEEP_INTERVAL_MS);
342
342
  sweepInterval.unref();
343
343
  const httpServer = http_1.default.createServer(async (req, res) => {
344
344
  try {
@@ -401,7 +401,7 @@ async function startMultiProjectHttpServer(host, port, sessionTimeoutMs, project
401
401
  process.stderr.write(`[http] Session ${sid} (project: ${s.projectId}) timed out\n`);
402
402
  }
403
403
  }
404
- }, 60_000);
404
+ }, defaults_1.SESSION_SWEEP_INTERVAL_MS);
405
405
  sweepInterval.unref();
406
406
  // Express app handles /api/* routes
407
407
  const restApp = (0, index_1.createRestApp)(projectManager, restOptions);
@@ -50,7 +50,8 @@ function createCodeRouter() {
50
50
  const symbol = p.codeManager.getSymbol(symbolId);
51
51
  if (!symbol)
52
52
  return res.status(404).json({ error: 'Symbol not found' });
53
- res.json(symbol);
53
+ const { embedding: _, fileEmbedding: _fe, ...rest } = symbol;
54
+ res.json(rest);
54
55
  }
55
56
  catch (err) {
56
57
  next(err);
@@ -52,7 +52,8 @@ function createDocsRouter() {
52
52
  const node = p.docManager.getNode(nodeId);
53
53
  if (!node)
54
54
  return res.status(404).json({ error: 'Node not found' });
55
- res.json(node);
55
+ const { embedding: _, ...rest } = node;
56
+ res.json(rest);
56
57
  }
57
58
  catch (err) {
58
59
  next(err);
@@ -8,6 +8,7 @@ const crypto_1 = __importDefault(require("crypto"));
8
8
  const express_1 = require("express");
9
9
  const zod_1 = require("zod");
10
10
  const embedder_1 = require("../../lib/embedder");
11
+ const embedding_codec_1 = require("../../lib/embedding-codec");
11
12
  /**
12
13
  * Create an Express router for the embedding API.
13
14
  * POST /api/embed — embed texts using the server's embedding model.
@@ -16,6 +17,7 @@ function createEmbedRouter(apiConfig, modelName) {
16
17
  const router = (0, express_1.Router)();
17
18
  const embedRequestSchema = zod_1.z.object({
18
19
  texts: zod_1.z.array(zod_1.z.string().max(apiConfig.maxTextChars)).min(1).max(apiConfig.maxTexts),
20
+ format: zod_1.z.enum(['json', 'base64']).optional().default('json'),
19
21
  });
20
22
  router.post('/', async (req, res, next) => {
21
23
  try {
@@ -34,7 +36,12 @@ function createEmbedRouter(apiConfig, modelName) {
34
36
  const parsed = embedRequestSchema.parse(req.body);
35
37
  const inputs = parsed.texts.map(text => ({ title: text, content: '' }));
36
38
  const embeddings = await (0, embedder_1.embedBatch)(inputs, modelName);
37
- res.json({ embeddings });
39
+ if (parsed.format === 'base64') {
40
+ res.json({ embeddings: embeddings.map(e => (0, embedding_codec_1.float32ToBase64)(e)), format: 'base64' });
41
+ }
42
+ else {
43
+ res.json({ embeddings });
44
+ }
38
45
  }
39
46
  catch (err) {
40
47
  if (err?.name === 'ZodError') {
@@ -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 } : { 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,9 +73,10 @@ 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;
79
+ const cookieSecure = serverConfig?.cookieSecure;
78
80
  const accessTokenTtl = serverConfig?.accessTokenTtl ?? '15m';
79
81
  const refreshTokenTtl = serverConfig?.refreshTokenTtl ?? '7d';
80
82
  // --- Auth endpoints (before auth middleware — always accessible) ---
@@ -89,7 +91,7 @@ function createRestApp(projectManager, options) {
89
91
  const payload = (0, jwt_1.verifyToken)(accessToken, jwtSecret);
90
92
  if (payload?.type === 'access' && users[payload.userId]) {
91
93
  const user = users[payload.userId];
92
- return res.json({ required: true, authenticated: true, userId: payload.userId, name: user.name, apiKey: user.apiKey });
94
+ return res.json({ required: true, authenticated: true, userId: payload.userId, name: user.name });
93
95
  }
94
96
  }
95
97
  }
@@ -102,6 +104,22 @@ function createRestApp(projectManager, options) {
102
104
  }
103
105
  return res.json({ required: true, authenticated: false });
104
106
  });
107
+ // API key retrieval: requires valid JWT cookie (not exposed in /status)
108
+ app.get('/api/auth/apikey', (req, res) => {
109
+ if (!hasUsers || !jwtSecret) {
110
+ return res.status(400).json({ error: 'Authentication not configured' });
111
+ }
112
+ const accessToken = (0, jwt_1.getAccessToken)(req);
113
+ if (!accessToken) {
114
+ return res.status(401).json({ error: 'Authentication required' });
115
+ }
116
+ const payload = (0, jwt_1.verifyToken)(accessToken, jwtSecret);
117
+ if (!payload || payload.type !== 'access' || !users[payload.userId]) {
118
+ return res.status(401).json({ error: 'Invalid or expired token' });
119
+ }
120
+ const user = users[payload.userId];
121
+ res.json({ apiKey: user.apiKey ?? null });
122
+ });
105
123
  // Login: email + password → set JWT cookies
106
124
  app.post('/api/auth/login', async (req, res) => {
107
125
  if (!hasUsers || !jwtSecret) {
@@ -121,7 +139,7 @@ function createRestApp(projectManager, options) {
121
139
  }
122
140
  const accessToken = (0, jwt_1.signAccessToken)(result.userId, jwtSecret, accessTokenTtl);
123
141
  const refreshToken = (0, jwt_1.signRefreshToken)(result.userId, jwtSecret, refreshTokenTtl);
124
- (0, jwt_1.setAuthCookies)(res, accessToken, refreshToken, accessTokenTtl, refreshTokenTtl);
142
+ (0, jwt_1.setAuthCookies)(res, accessToken, refreshToken, accessTokenTtl, refreshTokenTtl, cookieSecure);
125
143
  res.json({ userId: result.userId, name: result.user.name });
126
144
  });
127
145
  // Refresh: refresh cookie → new access cookie
@@ -144,7 +162,7 @@ function createRestApp(projectManager, options) {
144
162
  }
145
163
  const newAccessToken = (0, jwt_1.signAccessToken)(payload.userId, jwtSecret, accessTokenTtl);
146
164
  const newRefreshToken = (0, jwt_1.signRefreshToken)(payload.userId, jwtSecret, refreshTokenTtl);
147
- (0, jwt_1.setAuthCookies)(res, newAccessToken, newRefreshToken, accessTokenTtl, refreshTokenTtl);
165
+ (0, jwt_1.setAuthCookies)(res, newAccessToken, newRefreshToken, accessTokenTtl, refreshTokenTtl, cookieSecure);
148
166
  res.json({ userId: payload.userId, name: users[payload.userId].name });
149
167
  });
150
168
  // Logout: clear cookies
@@ -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) {
@@ -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
  }
@@ -7,6 +7,7 @@ exports.register = register;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const zod_1 = require("zod");
10
+ const defaults_1 = require("../../../lib/defaults");
10
11
  function register(server, mgr) {
11
12
  server.registerTool('add_note_attachment', {
12
13
  description: 'Attach a file to a note. Provide the absolute path to a local file. ' +
@@ -28,7 +29,7 @@ function register(server, mgr) {
28
29
  if (!stat.isFile()) {
29
30
  return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
30
31
  }
31
- if (stat.size > 50 * 1024 * 1024) {
32
+ if (stat.size > defaults_1.MAX_UPLOAD_SIZE) {
32
33
  return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
33
34
  }
34
35
  const data = fs_1.default.readFileSync(resolved);
@@ -7,6 +7,7 @@ exports.register = register;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const zod_1 = require("zod");
10
+ const defaults_1 = require("../../../lib/defaults");
10
11
  function register(server, mgr) {
11
12
  server.registerTool('add_skill_attachment', {
12
13
  description: 'Attach a file to a skill. Provide the absolute path to a local file. ' +
@@ -28,7 +29,7 @@ function register(server, mgr) {
28
29
  if (!stat.isFile()) {
29
30
  return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
30
31
  }
31
- if (stat.size > 50 * 1024 * 1024) {
32
+ if (stat.size > defaults_1.MAX_UPLOAD_SIZE) {
32
33
  return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
33
34
  }
34
35
  const data = fs_1.default.readFileSync(resolved);
@@ -7,6 +7,7 @@ exports.register = register;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const zod_1 = require("zod");
10
+ const defaults_1 = require("../../../lib/defaults");
10
11
  function register(server, mgr) {
11
12
  server.registerTool('add_task_attachment', {
12
13
  description: 'Attach a file to a task. Provide the absolute path to a local file. ' +
@@ -28,7 +29,7 @@ function register(server, mgr) {
28
29
  if (!stat.isFile()) {
29
30
  return { content: [{ type: 'text', text: JSON.stringify({ error: 'Path is not a regular file' }) }], isError: true };
30
31
  }
31
- if (stat.size > 50 * 1024 * 1024) {
32
+ if (stat.size > defaults_1.MAX_UPLOAD_SIZE) {
32
33
  return { content: [{ type: 'text', text: JSON.stringify({ error: 'File exceeds 50 MB limit' }) }], isError: true };
33
34
  }
34
35
  const data = fs_1.default.readFileSync(resolved);
package/dist/cli/index.js CHANGED
@@ -14,11 +14,14 @@ const jwt_1 = require("../lib/jwt");
14
14
  const project_manager_1 = require("../lib/project-manager");
15
15
  const embedder_1 = require("../lib/embedder");
16
16
  const index_1 = require("../api/index");
17
+ const defaults_1 = require("../lib/defaults");
17
18
  const program = new commander_1.Command();
19
+ const pkgJsonPath = path_1.default.resolve(__dirname, '../../package.json');
20
+ const pkgVersion = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, 'utf-8')).version;
18
21
  program
19
22
  .name('graphmemory')
20
23
  .description('MCP server for semantic graph memory from markdown docs and source code')
21
- .version('1.3.0');
24
+ .version(pkgVersion);
22
25
  const parseIntArg = (v) => parseInt(v, 10);
23
26
  // ---------------------------------------------------------------------------
24
27
  // Helper: load config from file, or fall back to default (cwd as single project)
@@ -211,7 +214,7 @@ program
211
214
  const forceTimer = setTimeout(() => {
212
215
  process.stderr.write('[serve] Shutdown timeout, force exit\n');
213
216
  process.exit(1);
214
- }, 5000);
217
+ }, defaults_1.GRACEFUL_SHUTDOWN_TIMEOUT_MS);
215
218
  try {
216
219
  httpServer.close();
217
220
  // Destroy all open connections (including WebSocket) so the server can close
@@ -324,8 +327,8 @@ usersCmd
324
327
  process.stderr.write('Invalid email format\n');
325
328
  process.exit(1);
326
329
  }
327
- if (password.length > 256) {
328
- process.stderr.write('Password too long (max 256)\n');
330
+ if (password.length > defaults_1.MAX_PASSWORD_LEN) {
331
+ process.stderr.write(`Password too long (max ${defaults_1.MAX_PASSWORD_LEN})\n`);
329
332
  process.exit(1);
330
333
  }
331
334
  const pwHash = await (0, jwt_1.hashPassword)(password);