@graphmemory/server 1.3.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 (39) 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 +4 -3
  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 +5 -4
  15. package/dist/cli/indexer.js +2 -1
  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/mirror-watcher.js +4 -3
  28. package/dist/lib/parsers/docs.js +2 -1
  29. package/dist/lib/parsers/languages/typescript.js +34 -17
  30. package/dist/lib/project-manager.js +7 -1
  31. package/dist/lib/search/bm25.js +5 -4
  32. package/dist/lib/search/code.js +2 -1
  33. package/dist/lib/search/docs.js +2 -1
  34. package/dist/lib/search/file-index.js +2 -1
  35. package/dist/lib/search/files.js +3 -2
  36. package/dist/lib/search/knowledge.js +2 -1
  37. package/dist/lib/search/skills.js +2 -1
  38. package/dist/lib/search/tasks.js +2 -1
  39. package/package.json +5 -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.
@@ -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';
@@ -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,12 @@ 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();
18
19
  program
19
20
  .name('graphmemory')
20
21
  .description('MCP server for semantic graph memory from markdown docs and source code')
21
- .version('1.3.0');
22
+ .version('1.3.1');
22
23
  const parseIntArg = (v) => parseInt(v, 10);
23
24
  // ---------------------------------------------------------------------------
24
25
  // Helper: load config from file, or fall back to default (cwd as single project)
@@ -211,7 +212,7 @@ program
211
212
  const forceTimer = setTimeout(() => {
212
213
  process.stderr.write('[serve] Shutdown timeout, force exit\n');
213
214
  process.exit(1);
214
- }, 5000);
215
+ }, defaults_1.GRACEFUL_SHUTDOWN_TIMEOUT_MS);
215
216
  try {
216
217
  httpServer.close();
217
218
  // Destroy all open connections (including WebSocket) so the server can close
@@ -324,8 +325,8 @@ usersCmd
324
325
  process.stderr.write('Invalid email format\n');
325
326
  process.exit(1);
326
327
  }
327
- if (password.length > 256) {
328
- process.stderr.write('Password too long (max 256)\n');
328
+ if (password.length > defaults_1.MAX_PASSWORD_LEN) {
329
+ process.stderr.write(`Password too long (max ${defaults_1.MAX_PASSWORD_LEN})\n`);
329
330
  process.exit(1);
330
331
  }
331
332
  const pwHash = await (0, jwt_1.hashPassword)(password);
@@ -13,6 +13,7 @@ const docs_2 = require("../graphs/docs");
13
13
  const code_1 = require("../lib/parsers/code");
14
14
  const code_2 = require("../graphs/code");
15
15
  const watcher_1 = require("../lib/watcher");
16
+ const defaults_1 = require("../lib/defaults");
16
17
  const knowledge_1 = require("../graphs/knowledge");
17
18
  const task_1 = require("../graphs/task");
18
19
  const skill_1 = require("../graphs/skill");
@@ -103,7 +104,7 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
103
104
  const rootChunk = chunks.find(c => c.level === 1);
104
105
  const embedText = rootChunk?.title
105
106
  ? `${fileId} ${rootChunk.title}`
106
- : `${fileId} ${rootChunk?.content.slice(0, 200) ?? ''}`;
107
+ : `${fileId} ${rootChunk?.content.slice(0, defaults_1.INDEXER_PREVIEW_LEN) ?? ''}`;
107
108
  batchInputs.push({ title: embedText, content: '' });
108
109
  const embeddings = await (0, embedder_1.embedBatch)(batchInputs, config.docsModelName);
109
110
  for (let i = 0; i < chunks.length; i++) {
@@ -36,10 +36,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.MAX_ATTACHMENTS_PER_ENTITY = exports.MAX_ATTACHMENT_SIZE = void 0;
39
40
  exports.scanAttachments = scanAttachments;
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  const mime_1 = __importDefault(require("mime"));
44
+ /** Maximum size of a single attachment in bytes (10 MB). */
45
+ exports.MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
46
+ /** Maximum number of attachments per entity (note/task/skill). */
47
+ exports.MAX_ATTACHMENTS_PER_ENTITY = 20;
43
48
  /**
44
49
  * Scan the attachments/ subdirectory of an entity directory.
45
50
  * Returns metadata for each file found.
@@ -22,6 +22,8 @@ const code_1 = require("../lib/search/code");
22
22
  const files_1 = require("../lib/search/files");
23
23
  const bm25_1 = require("../lib/search/bm25");
24
24
  const embedding_codec_1 = require("../lib/embedding-codec");
25
+ const graph_persistence_1 = require("../lib/graph-persistence");
26
+ const defaults_1 = require("../lib/defaults");
25
27
  // ---------------------------------------------------------------------------
26
28
  // CRUD
27
29
  // ---------------------------------------------------------------------------
@@ -87,10 +89,10 @@ function resolvePendingImports(graph) {
87
89
  }
88
90
  /**
89
91
  * Resolve pending extends/implements edges after all files have been indexed.
90
- * Searches the entire graph for nodes matching toName (by symbol name).
92
+ * When multiple candidates share the same name, prefers the one whose file
93
+ * is imported by the source file (falls back to first match).
91
94
  */
92
95
  function resolvePendingEdges(graph) {
93
- // Build name → nodeId index for fast lookup
94
96
  const nameIndex = new Map();
95
97
  graph.forEachNode((id, attrs) => {
96
98
  if (attrs.kind === 'class' || attrs.kind === 'interface') {
@@ -99,6 +101,18 @@ function resolvePendingEdges(graph) {
99
101
  nameIndex.set(attrs.name, list);
100
102
  }
101
103
  });
104
+ // Build file → imported file IDs index for disambiguation
105
+ const fileImports = new Map();
106
+ graph.forEachNode((id, attrs) => {
107
+ if (attrs.kind === 'file') {
108
+ const imported = new Set();
109
+ graph.forEachOutEdge(id, (_edge, edgeAttrs, _src, target) => {
110
+ if (edgeAttrs.kind === 'imports')
111
+ imported.add(target);
112
+ });
113
+ fileImports.set(id, imported);
114
+ }
115
+ });
102
116
  let created = 0;
103
117
  graph.forEachNode((id, attrs) => {
104
118
  if (!attrs.pendingEdges || attrs.pendingEdges.length === 0)
@@ -107,8 +121,20 @@ function resolvePendingEdges(graph) {
107
121
  for (const edge of attrs.pendingEdges) {
108
122
  const candidates = nameIndex.get(edge.toName);
109
123
  if (candidates && candidates.length > 0 && graph.hasNode(edge.from)) {
110
- // Use first match (ambiguity is rare in practice)
111
- const toId = candidates[0];
124
+ let toId;
125
+ if (candidates.length === 1) {
126
+ toId = candidates[0];
127
+ }
128
+ else {
129
+ // Disambiguate: prefer candidate whose file is imported by edge.from's file
130
+ const fromFileId = edge.from.split('::')[0];
131
+ const imports = fileImports.get(fromFileId);
132
+ const match = imports && candidates.find(c => {
133
+ const cFileId = c.split('::')[0];
134
+ return imports.has(cFileId);
135
+ });
136
+ toId = match ?? candidates[0];
137
+ }
112
138
  if (toId !== edge.from && !graph.hasEdge(edge.from, toId)) {
113
139
  graph.addEdgeWithKey(`${edge.from}→${toId}`, edge.from, toId, { kind: edge.kind });
114
140
  created++;
@@ -142,7 +168,7 @@ function getCodeFileMtime(graph, fileId) {
142
168
  return graph.getNodeAttribute(nodes[0], 'mtime');
143
169
  }
144
170
  /** List all indexed files with symbol counts. */
145
- function listCodeFiles(graph, filter, limit = 20) {
171
+ function listCodeFiles(graph, filter, limit = defaults_1.LIST_LIMIT_SMALL) {
146
172
  const files = new Map();
147
173
  const lowerFilter = filter?.toLowerCase();
148
174
  graph.forEachNode((_, attrs) => {
@@ -182,10 +208,10 @@ function loadCodeGraph(graphMemory, fresh = false, embeddingFingerprint) {
182
208
  if (fresh)
183
209
  return graph;
184
210
  const file = path_1.default.join(graphMemory, 'code.json');
185
- if (!fs_1.default.existsSync(file))
211
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
212
+ if (!data)
186
213
  return graph;
187
214
  try {
188
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
189
215
  const stored = data.embeddingModel;
190
216
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
191
217
  process.stderr.write(`[code-graph] Embedding config changed, re-indexing code graph\n`);
@@ -207,7 +233,7 @@ class CodeGraphManager {
207
233
  _graph;
208
234
  embedFns;
209
235
  ext;
210
- _bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment} ${attrs.body}`);
236
+ _bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment} ${attrs.body.slice(0, defaults_1.BM25_BODY_MAX_CHARS)}`);
211
237
  constructor(_graph, embedFns, ext = {}) {
212
238
  this._graph = _graph;
213
239
  this.embedFns = embedFns;