@graphmemory/server 1.3.1 → 1.3.3

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 (115) hide show
  1. package/dist/api/rest/graph.js +1 -1
  2. package/dist/api/rest/index.js +21 -5
  3. package/dist/api/rest/validation.js +17 -17
  4. package/dist/api/tools/code/get-file-symbols.js +2 -2
  5. package/dist/api/tools/code/get-symbol.js +2 -2
  6. package/dist/api/tools/code/list-files.js +2 -2
  7. package/dist/api/tools/code/search-code.js +2 -1
  8. package/dist/api/tools/code/search-files.js +2 -1
  9. package/dist/api/tools/docs/cross-references.js +2 -1
  10. package/dist/api/tools/docs/explain-symbol.js +2 -1
  11. package/dist/api/tools/docs/find-examples.js +2 -1
  12. package/dist/api/tools/docs/get-node.js +2 -2
  13. package/dist/api/tools/docs/get-toc.js +2 -2
  14. package/dist/api/tools/docs/list-snippets.js +4 -4
  15. package/dist/api/tools/docs/list-topics.js +2 -2
  16. package/dist/api/tools/docs/search-files.js +2 -1
  17. package/dist/api/tools/docs/search-snippets.js +3 -2
  18. package/dist/api/tools/docs/search.js +2 -1
  19. package/dist/api/tools/file-index/get-file-info.js +2 -2
  20. package/dist/api/tools/file-index/list-all-files.js +5 -5
  21. package/dist/api/tools/file-index/search-all-files.js +2 -1
  22. package/dist/api/tools/knowledge/add-attachment.js +26 -4
  23. package/dist/api/tools/knowledge/create-note.js +4 -3
  24. package/dist/api/tools/knowledge/create-relation.js +5 -4
  25. package/dist/api/tools/knowledge/delete-note.js +2 -2
  26. package/dist/api/tools/knowledge/delete-relation.js +4 -3
  27. package/dist/api/tools/knowledge/find-linked-notes.js +4 -3
  28. package/dist/api/tools/knowledge/get-note.js +2 -2
  29. package/dist/api/tools/knowledge/list-notes.js +4 -3
  30. package/dist/api/tools/knowledge/list-relations.js +1 -1
  31. package/dist/api/tools/knowledge/remove-attachment.js +1 -1
  32. package/dist/api/tools/knowledge/search-notes.js +2 -1
  33. package/dist/api/tools/knowledge/update-note.js +6 -5
  34. package/dist/api/tools/skills/add-attachment.js +26 -4
  35. package/dist/api/tools/skills/bump-usage.js +2 -2
  36. package/dist/api/tools/skills/create-skill-link.js +6 -5
  37. package/dist/api/tools/skills/create-skill.js +8 -7
  38. package/dist/api/tools/skills/delete-skill-link.js +4 -3
  39. package/dist/api/tools/skills/delete-skill.js +2 -2
  40. package/dist/api/tools/skills/find-linked-skills.js +4 -3
  41. package/dist/api/tools/skills/get-skill.js +2 -2
  42. package/dist/api/tools/skills/link-skill.js +2 -2
  43. package/dist/api/tools/skills/list-skills.js +4 -3
  44. package/dist/api/tools/skills/recall-skills.js +2 -1
  45. package/dist/api/tools/skills/remove-attachment.js +1 -1
  46. package/dist/api/tools/skills/search-skills.js +2 -1
  47. package/dist/api/tools/skills/update-skill.js +10 -9
  48. package/dist/api/tools/tasks/add-attachment.js +26 -4
  49. package/dist/api/tools/tasks/create-task-link.js +6 -5
  50. package/dist/api/tools/tasks/create-task.js +5 -4
  51. package/dist/api/tools/tasks/delete-task-link.js +4 -3
  52. package/dist/api/tools/tasks/delete-task.js +2 -2
  53. package/dist/api/tools/tasks/find-linked-tasks.js +4 -3
  54. package/dist/api/tools/tasks/get-task.js +2 -2
  55. package/dist/api/tools/tasks/link-task.js +2 -2
  56. package/dist/api/tools/tasks/list-tasks.js +5 -4
  57. package/dist/api/tools/tasks/move-task.js +2 -2
  58. package/dist/api/tools/tasks/remove-attachment.js +1 -1
  59. package/dist/api/tools/tasks/search-tasks.js +2 -1
  60. package/dist/api/tools/tasks/update-task.js +7 -6
  61. package/dist/cli/index.js +3 -1
  62. package/dist/cli/indexer.js +38 -24
  63. package/dist/graphs/code.js +4 -0
  64. package/dist/graphs/docs.js +4 -0
  65. package/dist/graphs/file-index.js +4 -0
  66. package/dist/graphs/knowledge.js +5 -0
  67. package/dist/graphs/skill.js +5 -0
  68. package/dist/graphs/task.js +5 -0
  69. package/dist/lib/defaults.js +1 -1
  70. package/dist/lib/file-import.js +8 -2
  71. package/dist/lib/file-mirror.js +77 -21
  72. package/dist/lib/graph-persistence.js +42 -0
  73. package/dist/lib/jwt.js +6 -5
  74. package/dist/lib/multi-config.js +3 -1
  75. package/dist/lib/parsers/languages/typescript.js +11 -6
  76. package/dist/lib/promise-queue.js +20 -2
  77. package/dist/ui/assets/{NoteForm-aZX9f6-3.js → NoteForm-D0lOYBQq.js} +1 -1
  78. package/dist/ui/assets/{SkillForm-KYa3o92l.js → SkillForm-CfDWe0Nx.js} +1 -1
  79. package/dist/ui/assets/{TaskForm-Bl5nkybO.js → TaskForm-ltmMCEAE.js} +1 -1
  80. package/dist/ui/assets/{_articleId_-DjbCByxM.js → _articleId_-mEqH7YfV.js} +1 -1
  81. package/dist/ui/assets/{_docId_-hdCDjclV.js → _docId_-wAt8n8p4.js} +1 -1
  82. package/dist/ui/assets/{_filePath_-CpG836v4.js → _filePath_-DQMFMLQh.js} +1 -1
  83. package/dist/ui/assets/{_noteId_-C1enaQd1.js → _noteId_-Cqxl6H5q.js} +1 -1
  84. package/dist/ui/assets/{_skillId_-hPoCet7J.js → _skillId_-BlJOfwm_.js} +1 -1
  85. package/dist/ui/assets/{_taskId_-DSB3dLVz.js → _taskId_-Cs8LaIe4.js} +1 -1
  86. package/dist/ui/assets/{_toolName_-3SmCfxZy.js → _toolName_-3CHUDagf.js} +1 -1
  87. package/dist/ui/assets/{attachments-CEQ-2nMo.js → attachments-CMDVqPm_.js} +1 -1
  88. package/dist/ui/assets/{docs-CrXsRcOG.js → docs-BuFjplSR.js} +1 -1
  89. package/dist/ui/assets/{edit-TUIIpUMF.js → edit-7NV817UE.js} +1 -1
  90. package/dist/ui/assets/{edit-BYiy1FZy.js → edit-Bflx3-cK.js} +1 -1
  91. package/dist/ui/assets/{edit-hc-ZWz3y.js → edit-CdmIaFUI.js} +1 -1
  92. package/dist/ui/assets/{esm-BWiKNcBW.js → esm-CqydI1a6.js} +1 -1
  93. package/dist/ui/assets/{files-0bPg6NH9.js → files-BWNbyH1X.js} +1 -1
  94. package/dist/ui/assets/{graph-DXGud_wF.js → graph-B9nFxoXm.js} +1 -1
  95. package/dist/ui/assets/{help-DJ52_fxN.js → help-CqK0hEmf.js} +1 -1
  96. package/dist/ui/assets/{help-CEMQqZUR.js → help-D6XKMuzk.js} +16 -4
  97. package/dist/ui/assets/index-80sqSHwS.js +2 -0
  98. package/dist/ui/assets/{knowledge-DeygeGGH.js → knowledge-g4C4l6uL.js} +1 -1
  99. package/dist/ui/assets/{new-s8c0M75X.js → new-Bqup97cu.js} +1 -1
  100. package/dist/ui/assets/{new-DHTg3Dqq.js → new-DC3lRvxF.js} +1 -1
  101. package/dist/ui/assets/{new-CpD7hOBA.js → new-DbsKrGJ4.js} +1 -1
  102. package/dist/ui/assets/{prompts-BgOmdxgM.js → prompts-DyltFLqJ.js} +1 -1
  103. package/dist/ui/assets/{search-EpJhdP2a.js → search-DtRoWsqW.js} +1 -1
  104. package/dist/ui/assets/{skill-y9pizyqE.js → skill-demt31s6.js} +1 -1
  105. package/dist/ui/assets/{skills-Cga9iUZN.js → skills-DRjYPbZM.js} +1 -1
  106. package/dist/ui/assets/{tasks-CobouTKV.js → tasks-CgsSFz6X.js} +1 -1
  107. package/dist/ui/assets/{tools-JxKH5BDF.js → tools-BDszA6Kh.js} +1 -1
  108. package/dist/ui/assets/{vendor-markdown-CT8ZVEPu.js → vendor-markdown-DngssFHR.js} +27 -27
  109. package/dist/ui/assets/{vendor-md-editor-DmWafJvr.js → vendor-md-editor-DC6xr_29.js} +10 -10
  110. package/dist/ui/assets/{vendor-mui-BPj7d3Sw.js → vendor-mui-DXUYJbRC.js} +1 -1
  111. package/dist/ui/assets/{vendor-mui-icons-B196sG3f.js → vendor-mui-icons-YtgP6dg2.js} +1 -1
  112. package/dist/ui/assets/{vendor-react-CHUjhoxh.js → vendor-react-DxfYAwYK.js} +1 -1
  113. package/dist/ui/index.html +6 -6
  114. package/package.json +1 -1
  115. package/dist/ui/assets/index-BCZDAYZi.js +0 -2
@@ -8,12 +8,12 @@ function register(server, mgr) {
8
8
  'Returns: id, title, description, status, priority, tags, dueDate, estimate, ' +
9
9
  'completedAt, createdAt, updatedAt, subtasks[], blockedBy[], blocks[], related[], crossLinks[].',
10
10
  inputSchema: {
11
- taskId: zod_1.z.string().describe('Task ID to retrieve'),
11
+ taskId: zod_1.z.string().max(500).describe('Task ID to retrieve'),
12
12
  },
13
13
  }, async ({ taskId }) => {
14
14
  const task = mgr.getTask(taskId);
15
15
  if (!task) {
16
- return { content: [{ type: 'text', text: `Task "${taskId}" not found.` }], isError: true };
16
+ return { content: [{ type: 'text', text: 'Task not found' }], isError: true };
17
17
  }
18
18
  return { content: [{ type: 'text', text: JSON.stringify(task, null, 2) }] };
19
19
  });
@@ -9,8 +9,8 @@ function register(server, mgr) {
9
9
  '"blocks": fromId blocks toId. ' +
10
10
  '"related_to": free association between tasks.',
11
11
  inputSchema: {
12
- fromId: zod_1.z.string().describe('Source task ID'),
13
- toId: zod_1.z.string().describe('Target task ID'),
12
+ fromId: zod_1.z.string().max(500).describe('Source task ID'),
13
+ toId: zod_1.z.string().max(500).describe('Target task ID'),
14
14
  kind: zod_1.z.enum(['subtask_of', 'blocks', 'related_to']).describe('Relation type'),
15
15
  },
16
16
  }, async ({ fromId, toId, kind }) => {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.register = register;
4
4
  const zod_1 = require("zod");
5
+ const defaults_1 = require("../../../lib/defaults");
5
6
  function register(server, mgr) {
6
7
  server.registerTool('list_tasks', {
7
8
  description: 'List tasks with optional filters. ' +
@@ -13,10 +14,10 @@ function register(server, mgr) {
13
14
  .describe('Filter by status'),
14
15
  priority: zod_1.z.enum(['critical', 'high', 'medium', 'low']).optional()
15
16
  .describe('Filter by priority'),
16
- tag: zod_1.z.string().optional().describe('Filter by tag (exact match, case-insensitive)'),
17
- filter: zod_1.z.string().optional().describe('Substring match on title or ID'),
18
- assignee: zod_1.z.string().optional().describe('Filter by assignee (team member ID)'),
19
- limit: zod_1.z.number().optional().describe('Max results (default 50)'),
17
+ tag: zod_1.z.string().max(defaults_1.MAX_TAG_LEN).optional().describe('Filter by tag (exact match, case-insensitive)'),
18
+ filter: zod_1.z.string().max(500).optional().describe('Substring match on title or ID'),
19
+ assignee: zod_1.z.string().max(defaults_1.MAX_ASSIGNEE_LEN).optional().describe('Filter by assignee (team member ID)'),
20
+ limit: zod_1.z.number().max(1000).optional().describe('Max results (default 50)'),
20
21
  },
21
22
  }, async ({ status, priority, tag, filter, assignee, limit }) => {
22
23
  const results = mgr.listTasks({ status, priority, tag, filter, assignee, limit });
@@ -10,7 +10,7 @@ function register(server, mgr) {
10
10
  'Returns the updated task summary. ' +
11
11
  'Pass expectedVersion to enable optimistic locking.',
12
12
  inputSchema: {
13
- taskId: zod_1.z.string().describe('Task ID to move'),
13
+ taskId: zod_1.z.string().max(500).describe('Task ID to move'),
14
14
  status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled'])
15
15
  .describe('New status'),
16
16
  expectedVersion: zod_1.z.number().int().positive().optional().describe('Current version for optimistic locking — request fails with version_conflict if the task has been updated since'),
@@ -19,7 +19,7 @@ function register(server, mgr) {
19
19
  try {
20
20
  const moved = mgr.moveTask(taskId, status, expectedVersion);
21
21
  if (!moved) {
22
- return { content: [{ type: 'text', text: `Task "${taskId}" not found.` }], isError: true };
22
+ return { content: [{ type: 'text', text: 'Task not found' }], isError: true };
23
23
  }
24
24
  const task = mgr.getTask(taskId);
25
25
  return { content: [{ type: 'text', text: JSON.stringify({
@@ -6,7 +6,7 @@ function register(server, mgr) {
6
6
  server.registerTool('remove_task_attachment', {
7
7
  description: 'Remove an attachment from a task. The file is deleted from disk.',
8
8
  inputSchema: {
9
- taskId: zod_1.z.string().describe('ID of the task'),
9
+ taskId: zod_1.z.string().max(500).describe('ID of the task'),
10
10
  filename: zod_1.z.string().min(1).max(255)
11
11
  .refine(s => !/[/\\]/.test(s), 'Filename must not contain path separators')
12
12
  .refine(s => !s.includes('..'), 'Filename must not contain ..')
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.register = register;
4
4
  const zod_1 = require("zod");
5
+ const defaults_1 = require("../../../lib/defaults");
5
6
  function register(server, mgr) {
6
7
  server.registerTool('search_tasks', {
7
8
  description: 'Semantic search over the task graph. ' +
@@ -11,7 +12,7 @@ function register(server, mgr) {
11
12
  'Returns an array sorted by relevance score (0–1), each with: ' +
12
13
  'id, title, description, status, priority, tags, score.',
13
14
  inputSchema: {
14
- query: zod_1.z.string().describe('Natural language search query'),
15
+ query: zod_1.z.string().max(defaults_1.MAX_SEARCH_QUERY_LEN).describe('Natural language search query'),
15
16
  topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar tasks to use as seeds (default 5)'),
16
17
  bfsDepth: zod_1.z.number().min(0).max(10).optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
17
18
  maxResults: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 20)'),
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.register = register;
4
4
  const zod_1 = require("zod");
5
5
  const manager_types_1 = require("../../../graphs/manager-types");
6
+ const defaults_1 = require("../../../lib/defaults");
6
7
  function register(server, mgr) {
7
8
  server.registerTool('update_task', {
8
9
  description: 'Update an existing task. Only provided fields are changed. ' +
@@ -11,16 +12,16 @@ function register(server, mgr) {
11
12
  'Use move_task for a simpler status-only change. ' +
12
13
  'Pass expectedVersion to enable optimistic locking.',
13
14
  inputSchema: {
14
- taskId: zod_1.z.string().describe('Task ID to update'),
15
- title: zod_1.z.string().optional().describe('New title'),
16
- description: zod_1.z.string().optional().describe('New description'),
15
+ taskId: zod_1.z.string().max(500).describe('Task ID to update'),
16
+ title: zod_1.z.string().max(defaults_1.MAX_TITLE_LEN).optional().describe('New title'),
17
+ description: zod_1.z.string().max(defaults_1.MAX_DESCRIPTION_LEN).optional().describe('New description'),
17
18
  status: zod_1.z.enum(['backlog', 'todo', 'in_progress', 'review', 'done', 'cancelled']).optional()
18
19
  .describe('New status'),
19
20
  priority: zod_1.z.enum(['critical', 'high', 'medium', 'low']).optional().describe('New priority'),
20
- tags: zod_1.z.array(zod_1.z.string()).optional().describe('Replace tags array'),
21
+ tags: zod_1.z.array(zod_1.z.string().max(defaults_1.MAX_TAG_LEN)).max(defaults_1.MAX_TAGS_COUNT).optional().describe('Replace tags array'),
21
22
  dueDate: zod_1.z.number().nullable().optional().describe('New due date (ms timestamp) or null to clear'),
22
23
  estimate: zod_1.z.number().nullable().optional().describe('New estimate (hours) or null to clear'),
23
- assignee: zod_1.z.string().nullable().optional().describe('Team member ID to assign, or null to unassign'),
24
+ assignee: zod_1.z.string().max(defaults_1.MAX_ASSIGNEE_LEN).nullable().optional().describe('Team member ID to assign, or null to unassign'),
24
25
  expectedVersion: zod_1.z.number().int().positive().optional().describe('Current version for optimistic locking — request fails with version_conflict if the task has been updated since'),
25
26
  },
26
27
  }, async ({ taskId, title, description, status, priority, tags, dueDate, estimate, assignee, expectedVersion }) => {
@@ -44,7 +45,7 @@ function register(server, mgr) {
44
45
  try {
45
46
  const updated = await mgr.updateTask(taskId, patch, expectedVersion);
46
47
  if (!updated) {
47
- return { content: [{ type: 'text', text: `Task "${taskId}" not found.` }], isError: true };
48
+ return { content: [{ type: 'text', text: 'Task not found' }], isError: true };
48
49
  }
49
50
  return { content: [{ type: 'text', text: JSON.stringify({ taskId, updated: true }, null, 2) }] };
50
51
  }
package/dist/cli/index.js CHANGED
@@ -16,10 +16,12 @@ const embedder_1 = require("../lib/embedder");
16
16
  const index_1 = require("../api/index");
17
17
  const defaults_1 = require("../lib/defaults");
18
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;
19
21
  program
20
22
  .name('graphmemory')
21
23
  .description('MCP server for semantic graph memory from markdown docs and source code')
22
- .version('1.3.1');
24
+ .version(pkgVersion);
23
25
  const parseIntArg = (v) => parseInt(v, 10);
24
26
  // ---------------------------------------------------------------------------
25
27
  // Helper: load config from file, or fall back to default (cwd as single project)
@@ -208,7 +208,9 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
208
208
  }
209
209
  function dispatchAdd(absolutePath) {
210
210
  const rel = path_1.default.relative(config.projectDir, absolutePath);
211
- if (config.docsInclude && !isExcluded(rel, docsExclude) && micromatch_1.default.isMatch(rel, config.docsInclude)) {
211
+ if (docGraph && config.docsInclude && !isExcluded(rel, docsExclude) && micromatch_1.default.isMatch(rel, config.docsInclude)) {
212
+ if (rel.endsWith('.md'))
213
+ (0, docs_1.clearWikiIndexCache)(config.projectDir);
212
214
  enqueueDoc(() => indexDocFile(absolutePath));
213
215
  }
214
216
  if (codeGraph && config.codeInclude && !isExcluded(rel, codeExclude) && micromatch_1.default.isMatch(rel, config.codeInclude)) {
@@ -221,33 +223,43 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
221
223
  function dispatchRemove(absolutePath) {
222
224
  const rel = path_1.default.relative(config.projectDir, absolutePath);
223
225
  if (docGraph && config.docsInclude && !isExcluded(rel, docsExclude) && micromatch_1.default.isMatch(rel, config.docsInclude)) {
224
- (0, docs_2.removeFile)(docGraph, rel);
225
- if (knowledgeGraph)
226
- (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'docs', docGraph, config.projectId);
227
- if (taskGraph)
228
- (0, task_1.cleanupProxies)(taskGraph, 'docs', docGraph, config.projectId);
229
- if (skillGraph)
230
- (0, skill_1.cleanupProxies)(skillGraph, 'docs', docGraph, config.projectId);
231
- process.stderr.write(`[indexer] removed doc ${rel}\n`);
226
+ if (rel.endsWith('.md'))
227
+ (0, docs_1.clearWikiIndexCache)(config.projectDir);
228
+ // Enqueue removal to avoid racing with in-flight indexDocFile tasks
229
+ enqueueDoc(async () => {
230
+ (0, docs_2.removeFile)(docGraph, rel);
231
+ if (knowledgeGraph)
232
+ (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'docs', docGraph, config.projectId);
233
+ if (taskGraph)
234
+ (0, task_1.cleanupProxies)(taskGraph, 'docs', docGraph, config.projectId);
235
+ if (skillGraph)
236
+ (0, skill_1.cleanupProxies)(skillGraph, 'docs', docGraph, config.projectId);
237
+ process.stderr.write(`[indexer] removed doc ${rel}\n`);
238
+ });
232
239
  }
233
240
  if (codeGraph && config.codeInclude && !isExcluded(rel, codeExclude) && micromatch_1.default.isMatch(rel, config.codeInclude)) {
234
- (0, code_2.removeCodeFile)(codeGraph, rel);
235
- if (knowledgeGraph)
236
- (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'code', codeGraph, config.projectId);
237
- if (taskGraph)
238
- (0, task_1.cleanupProxies)(taskGraph, 'code', codeGraph, config.projectId);
239
- if (skillGraph)
240
- (0, skill_1.cleanupProxies)(skillGraph, 'code', codeGraph, config.projectId);
241
- process.stderr.write(`[indexer] removed code ${rel}\n`);
241
+ enqueueCode(async () => {
242
+ (0, code_2.removeCodeFile)(codeGraph, rel);
243
+ if (knowledgeGraph)
244
+ (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'code', codeGraph, config.projectId);
245
+ if (taskGraph)
246
+ (0, task_1.cleanupProxies)(taskGraph, 'code', codeGraph, config.projectId);
247
+ if (skillGraph)
248
+ (0, skill_1.cleanupProxies)(skillGraph, 'code', codeGraph, config.projectId);
249
+ process.stderr.write(`[indexer] removed code ${rel}\n`);
250
+ });
242
251
  }
243
252
  if (fileIndexGraph && !isExcluded(rel, filesExclude)) {
244
- (0, file_index_1.removeFileEntry)(fileIndexGraph, rel);
245
- if (knowledgeGraph)
246
- (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'files', fileIndexGraph, config.projectId);
247
- if (taskGraph)
248
- (0, task_1.cleanupProxies)(taskGraph, 'files', fileIndexGraph, config.projectId);
249
- if (skillGraph)
250
- (0, skill_1.cleanupProxies)(skillGraph, 'files', fileIndexGraph, config.projectId);
253
+ enqueueFile(async () => {
254
+ (0, file_index_1.removeFileEntry)(fileIndexGraph, rel);
255
+ if (knowledgeGraph)
256
+ (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'files', fileIndexGraph, config.projectId);
257
+ if (taskGraph)
258
+ (0, task_1.cleanupProxies)(taskGraph, 'files', fileIndexGraph, config.projectId);
259
+ if (skillGraph)
260
+ (0, skill_1.cleanupProxies)(skillGraph, 'files', fileIndexGraph, config.projectId);
261
+ process.stderr.write(`[indexer] removed file ${rel}\n`);
262
+ });
251
263
  }
252
264
  }
253
265
  // ---------------------------------------------------------------------------
@@ -258,6 +270,8 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
258
270
  for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
259
271
  if (entry.name.startsWith('.') || watcher_1.ALWAYS_IGNORED.has(entry.name))
260
272
  continue;
273
+ if (entry.isSymbolicLink())
274
+ continue;
261
275
  const full = path_1.default.join(dir, entry.name);
262
276
  if (entry.isDirectory()) {
263
277
  const relDir = path_1.default.relative(config.projectDir, full);
@@ -217,6 +217,10 @@ function loadCodeGraph(graphMemory, fresh = false, embeddingFingerprint) {
217
217
  process.stderr.write(`[code-graph] Embedding config changed, re-indexing code graph\n`);
218
218
  return graph;
219
219
  }
220
+ if (!(0, graph_persistence_1.validateGraphStructure)(data.graph)) {
221
+ process.stderr.write(`[code-graph] Invalid graph structure in ${file}, starting fresh\n`);
222
+ return graph;
223
+ }
220
224
  (0, embedding_codec_1.decompressEmbeddings)(data.graph);
221
225
  graph.import(data.graph);
222
226
  process.stderr.write(`[code-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
@@ -164,6 +164,10 @@ function loadGraph(graphMemory, fresh = false, embeddingFingerprint) {
164
164
  process.stderr.write(`[graph] Embedding config changed, re-indexing docs graph\n`);
165
165
  return graph;
166
166
  }
167
+ if (!(0, graph_persistence_1.validateGraphStructure)(data.graph)) {
168
+ process.stderr.write(`[graph] Invalid graph structure in ${file}, starting fresh\n`);
169
+ return graph;
170
+ }
167
171
  (0, embedding_codec_1.decompressEmbeddings)(data.graph);
168
172
  graph.import(data.graph);
169
173
  process.stderr.write(`[graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
@@ -272,6 +272,10 @@ function loadFileIndexGraph(graphMemory, fresh = false, embeddingFingerprint) {
272
272
  process.stderr.write(`[file-index] Embedding config changed, re-indexing file index\n`);
273
273
  return graph;
274
274
  }
275
+ if (!(0, graph_persistence_1.validateGraphStructure)(data.graph)) {
276
+ process.stderr.write(`[file-index] Invalid graph structure in ${file}, starting fresh\n`);
277
+ return graph;
278
+ }
275
279
  (0, embedding_codec_1.decompressEmbeddings)(data.graph);
276
280
  graph.import(data.graph);
277
281
  process.stderr.write(`[file-index] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
@@ -360,6 +360,10 @@ function loadKnowledgeGraph(graphMemory, fresh = false, embeddingFingerprint) {
360
360
  process.stderr.write(`[knowledge-graph] Embedding config changed, re-indexing knowledge graph\n`);
361
361
  return graph;
362
362
  }
363
+ if (!(0, graph_persistence_1.validateGraphStructure)(data.graph)) {
364
+ process.stderr.write(`[knowledge-graph] Invalid graph structure in ${file}, starting fresh\n`);
365
+ return graph;
366
+ }
363
367
  (0, embedding_codec_1.decompressEmbeddings)(data.graph);
364
368
  graph.import(data.graph);
365
369
  process.stderr.write(`[knowledge-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
@@ -433,6 +437,7 @@ class KnowledgeGraphManager {
433
437
  mirrorTracker;
434
438
  _bm25Index;
435
439
  get externalGraphs() { return this.ext; }
440
+ get projectDir() { return this.ctx.projectDir; }
436
441
  constructor(_graph, embedFns, ctx, ext = {}) {
437
442
  this._graph = _graph;
438
443
  this.embedFns = embedFns;
@@ -504,6 +504,10 @@ function loadSkillGraph(graphMemory, fresh = false, embeddingFingerprint) {
504
504
  process.stderr.write(`[skill-graph] Embedding config changed, re-indexing skill graph\n`);
505
505
  return graph;
506
506
  }
507
+ if (!(0, graph_persistence_1.validateGraphStructure)(data.graph)) {
508
+ process.stderr.write(`[skill-graph] Invalid graph structure in ${file}, starting fresh\n`);
509
+ return graph;
510
+ }
507
511
  (0, embedding_codec_1.decompressEmbeddings)(data.graph);
508
512
  graph.import(data.graph);
509
513
  process.stderr.write(`[skill-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
@@ -610,6 +614,7 @@ class SkillGraphManager {
610
614
  taskGraph;
611
615
  mirrorTracker;
612
616
  _bm25Index;
617
+ get projectDir() { return this.ctx.projectDir; }
613
618
  constructor(_graph, embedFns, ctx, ext = {}) {
614
619
  this._graph = _graph;
615
620
  this.embedFns = embedFns;
@@ -525,6 +525,10 @@ function loadTaskGraph(graphMemory, fresh = false, embeddingFingerprint) {
525
525
  process.stderr.write(`[task-graph] Embedding config changed, re-indexing task graph\n`);
526
526
  return graph;
527
527
  }
528
+ if (!(0, graph_persistence_1.validateGraphStructure)(data.graph)) {
529
+ process.stderr.write(`[task-graph] Invalid graph structure in ${file}, starting fresh\n`);
530
+ return graph;
531
+ }
528
532
  (0, embedding_codec_1.decompressEmbeddings)(data.graph);
529
533
  graph.import(data.graph);
530
534
  process.stderr.write(`[task-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
@@ -591,6 +595,7 @@ class TaskGraphManager {
591
595
  knowledgeGraph;
592
596
  mirrorTracker;
593
597
  _bm25Index;
598
+ get projectDir() { return this.ctx.projectDir; }
594
599
  constructor(_graph, embedFns, ctx, ext = {}) {
595
600
  this._graph = _graph;
596
601
  this.embedFns = embedFns;
@@ -38,7 +38,7 @@ exports.MAX_TITLE_LEN = 500;
38
38
  exports.MAX_NOTE_CONTENT_LEN = 1_000_000;
39
39
  exports.MAX_TAG_LEN = 100;
40
40
  exports.MAX_TAGS_COUNT = 100;
41
- exports.MAX_SEARCH_QUERY_LEN = 2000;
41
+ exports.MAX_SEARCH_QUERY_LEN = 10_000;
42
42
  exports.MAX_SEARCH_TOP_K = 500;
43
43
  exports.MAX_DESCRIPTION_LEN = 500_000;
44
44
  exports.MAX_ASSIGNEE_LEN = 100;
@@ -96,10 +96,16 @@ function extractTitleAndContent(body) {
96
96
  */
97
97
  function extractId(filePath) {
98
98
  const basename = path.basename(filePath, '.md');
99
+ let id;
99
100
  if (basename === 'note' || basename === 'task' || basename === 'skill') {
100
- return path.basename(path.dirname(filePath));
101
+ id = path.basename(path.dirname(filePath));
101
102
  }
102
- return basename;
103
+ else {
104
+ id = basename;
105
+ }
106
+ if (id === '..' || id === '.' || id.includes('\0'))
107
+ return '';
108
+ return id;
103
109
  }
104
110
  function parseNoteFile(filePath) {
105
111
  try {
@@ -47,6 +47,7 @@ exports.mirrorTaskRelation = mirrorTaskRelation;
47
47
  exports.mirrorSkillRelation = mirrorSkillRelation;
48
48
  exports.mirrorAttachmentEvent = mirrorAttachmentEvent;
49
49
  exports.deleteMirrorDir = deleteMirrorDir;
50
+ exports.sanitizeEntityId = sanitizeEntityId;
50
51
  exports.sanitizeFilename = sanitizeFilename;
51
52
  exports.writeAttachment = writeAttachment;
52
53
  exports.deleteAttachment = deleteAttachment;
@@ -56,6 +57,10 @@ const path = __importStar(require("path"));
56
57
  const crypto_1 = __importDefault(require("crypto"));
57
58
  const frontmatter_1 = require("./frontmatter");
58
59
  const events_log_1 = require("./events-log");
60
+ /** Sanitize a string for safe inclusion in log output. */
61
+ function sanitizeForLog(s) {
62
+ return s.replace(/[\r\n\t]/g, ' ').slice(0, 200);
63
+ }
59
64
  /** Write to a temp file then rename — atomic on same filesystem. */
60
65
  function atomicWriteFileSync(filePath, data, encoding) {
61
66
  const tmp = `${filePath}.${crypto_1.default.randomBytes(4).toString('hex')}.tmp`;
@@ -80,7 +85,12 @@ function buildOutgoingRelations(entityId, relations) {
80
85
  /** Append a 'created' event + write content.md + regenerate note.md snapshot. */
81
86
  function mirrorNoteCreate(notesDir, noteId, attrs, relations) {
82
87
  try {
83
- const entityDir = path.join(notesDir, noteId);
88
+ const safeId = sanitizeEntityId(noteId);
89
+ if (!safeId) {
90
+ process.stderr.write(`[file-mirror] rejected invalid entity ID\n`);
91
+ return;
92
+ }
93
+ const entityDir = path.join(notesDir, safeId);
84
94
  fs.mkdirSync(entityDir, { recursive: true });
85
95
  const eventsPath = path.join(entityDir, 'events.jsonl');
86
96
  if (!fs.existsSync(eventsPath)) {
@@ -101,13 +111,18 @@ function mirrorNoteCreate(notesDir, noteId, attrs, relations) {
101
111
  (0, events_log_1.ensureGitattributes)(notesDir);
102
112
  }
103
113
  catch (err) {
104
- process.stderr.write(`[file-mirror] failed to mirror note create ${noteId}: ${err}\n`);
114
+ process.stderr.write(`[file-mirror] failed to mirror note create ${sanitizeForLog(noteId)}: ${err}\n`);
105
115
  }
106
116
  }
107
117
  /** Append an 'update' event + (if content changed) write content.md + regenerate note.md. */
108
118
  function mirrorNoteUpdate(notesDir, noteId, patch, attrs, relations) {
109
119
  try {
110
- const entityDir = path.join(notesDir, noteId);
120
+ const safeId = sanitizeEntityId(noteId);
121
+ if (!safeId) {
122
+ process.stderr.write(`[file-mirror] rejected invalid entity ID\n`);
123
+ return;
124
+ }
125
+ const entityDir = path.join(notesDir, safeId);
111
126
  fs.mkdirSync(entityDir, { recursive: true });
112
127
  const eventsPath = path.join(entityDir, 'events.jsonl');
113
128
  const delta = { op: 'update' };
@@ -127,7 +142,7 @@ function mirrorNoteUpdate(notesDir, noteId, patch, attrs, relations) {
127
142
  _regenerateNoteSnapshot(notesDir, noteId, attrs, relations);
128
143
  }
129
144
  catch (err) {
130
- process.stderr.write(`[file-mirror] failed to mirror note update ${noteId}: ${err}\n`);
145
+ process.stderr.write(`[file-mirror] failed to mirror note update ${sanitizeForLog(noteId)}: ${err}\n`);
131
146
  }
132
147
  }
133
148
  function _regenerateNoteSnapshot(notesDir, noteId, attrs, relations) {
@@ -153,7 +168,12 @@ function _regenerateNoteSnapshot(notesDir, noteId, attrs, relations) {
153
168
  /** Append a 'created' event + write description.md + regenerate task.md snapshot. */
154
169
  function mirrorTaskCreate(tasksDir, taskId, attrs, relations) {
155
170
  try {
156
- const entityDir = path.join(tasksDir, taskId);
171
+ const safeId = sanitizeEntityId(taskId);
172
+ if (!safeId) {
173
+ process.stderr.write(`[file-mirror] rejected invalid entity ID\n`);
174
+ return;
175
+ }
176
+ const entityDir = path.join(tasksDir, safeId);
157
177
  fs.mkdirSync(entityDir, { recursive: true });
158
178
  const eventsPath = path.join(entityDir, 'events.jsonl');
159
179
  if (!fs.existsSync(eventsPath)) {
@@ -179,13 +199,18 @@ function mirrorTaskCreate(tasksDir, taskId, attrs, relations) {
179
199
  (0, events_log_1.ensureGitattributes)(tasksDir);
180
200
  }
181
201
  catch (err) {
182
- process.stderr.write(`[file-mirror] failed to mirror task create ${taskId}: ${err}\n`);
202
+ process.stderr.write(`[file-mirror] failed to mirror task create ${sanitizeForLog(taskId)}: ${err}\n`);
183
203
  }
184
204
  }
185
205
  /** Append an 'update' event + (if description changed) write description.md + regenerate task.md. */
186
206
  function mirrorTaskUpdate(tasksDir, taskId, patch, attrs, relations) {
187
207
  try {
188
- const entityDir = path.join(tasksDir, taskId);
208
+ const safeId = sanitizeEntityId(taskId);
209
+ if (!safeId) {
210
+ process.stderr.write(`[file-mirror] rejected invalid entity ID\n`);
211
+ return;
212
+ }
213
+ const entityDir = path.join(tasksDir, safeId);
189
214
  fs.mkdirSync(entityDir, { recursive: true });
190
215
  const eventsPath = path.join(entityDir, 'events.jsonl');
191
216
  const delta = { op: 'update' };
@@ -215,7 +240,7 @@ function mirrorTaskUpdate(tasksDir, taskId, patch, attrs, relations) {
215
240
  _regenerateTaskSnapshot(tasksDir, taskId, attrs, relations);
216
241
  }
217
242
  catch (err) {
218
- process.stderr.write(`[file-mirror] failed to mirror task update ${taskId}: ${err}\n`);
243
+ process.stderr.write(`[file-mirror] failed to mirror task update ${sanitizeForLog(taskId)}: ${err}\n`);
219
244
  }
220
245
  }
221
246
  function _regenerateTaskSnapshot(tasksDir, taskId, attrs, relations) {
@@ -247,7 +272,12 @@ function _regenerateTaskSnapshot(tasksDir, taskId, attrs, relations) {
247
272
  /** Append a 'created' event + write description.md + regenerate skill.md snapshot. */
248
273
  function mirrorSkillCreate(skillsDir, skillId, attrs, relations) {
249
274
  try {
250
- const entityDir = path.join(skillsDir, skillId);
275
+ const safeId = sanitizeEntityId(skillId);
276
+ if (!safeId) {
277
+ process.stderr.write(`[file-mirror] rejected invalid entity ID\n`);
278
+ return;
279
+ }
280
+ const entityDir = path.join(skillsDir, safeId);
251
281
  fs.mkdirSync(entityDir, { recursive: true });
252
282
  const eventsPath = path.join(entityDir, 'events.jsonl');
253
283
  if (!fs.existsSync(eventsPath)) {
@@ -276,13 +306,18 @@ function mirrorSkillCreate(skillsDir, skillId, attrs, relations) {
276
306
  (0, events_log_1.ensureGitattributes)(skillsDir);
277
307
  }
278
308
  catch (err) {
279
- process.stderr.write(`[file-mirror] failed to mirror skill create ${skillId}: ${err}\n`);
309
+ process.stderr.write(`[file-mirror] failed to mirror skill create ${sanitizeForLog(skillId)}: ${err}\n`);
280
310
  }
281
311
  }
282
312
  /** Append an 'update' event + (if description changed) write description.md + regenerate skill.md. */
283
313
  function mirrorSkillUpdate(skillsDir, skillId, patch, attrs, relations) {
284
314
  try {
285
- const entityDir = path.join(skillsDir, skillId);
315
+ const safeId = sanitizeEntityId(skillId);
316
+ if (!safeId) {
317
+ process.stderr.write(`[file-mirror] rejected invalid entity ID\n`);
318
+ return;
319
+ }
320
+ const entityDir = path.join(skillsDir, safeId);
286
321
  fs.mkdirSync(entityDir, { recursive: true });
287
322
  const eventsPath = path.join(entityDir, 'events.jsonl');
288
323
  const delta = { op: 'update' };
@@ -318,7 +353,7 @@ function mirrorSkillUpdate(skillsDir, skillId, patch, attrs, relations) {
318
353
  _regenerateSkillSnapshot(skillsDir, skillId, attrs, relations);
319
354
  }
320
355
  catch (err) {
321
- process.stderr.write(`[file-mirror] failed to mirror skill update ${skillId}: ${err}\n`);
356
+ process.stderr.write(`[file-mirror] failed to mirror skill update ${sanitizeForLog(skillId)}: ${err}\n`);
322
357
  }
323
358
  }
324
359
  function _regenerateSkillSnapshot(skillsDir, skillId, attrs, relations) {
@@ -364,7 +399,7 @@ function mirrorNoteRelation(notesDir, noteId, action, kind, to, attrs, relations
364
399
  _regenerateNoteSnapshot(notesDir, noteId, attrs, relations);
365
400
  }
366
401
  catch (err) {
367
- process.stderr.write(`[file-mirror] failed to mirror note relation ${noteId}: ${err}\n`);
402
+ process.stderr.write(`[file-mirror] failed to mirror note relation ${sanitizeForLog(noteId)}: ${err}\n`);
368
403
  }
369
404
  }
370
405
  function mirrorTaskRelation(tasksDir, taskId, action, kind, to, attrs, relations, graph) {
@@ -378,7 +413,7 @@ function mirrorTaskRelation(tasksDir, taskId, action, kind, to, attrs, relations
378
413
  _regenerateTaskSnapshot(tasksDir, taskId, attrs, relations);
379
414
  }
380
415
  catch (err) {
381
- process.stderr.write(`[file-mirror] failed to mirror task relation ${taskId}: ${err}\n`);
416
+ process.stderr.write(`[file-mirror] failed to mirror task relation ${sanitizeForLog(taskId)}: ${err}\n`);
382
417
  }
383
418
  }
384
419
  function mirrorSkillRelation(skillsDir, skillId, action, kind, to, attrs, relations, graph) {
@@ -392,7 +427,7 @@ function mirrorSkillRelation(skillsDir, skillId, action, kind, to, attrs, relati
392
427
  _regenerateSkillSnapshot(skillsDir, skillId, attrs, relations);
393
428
  }
394
429
  catch (err) {
395
- process.stderr.write(`[file-mirror] failed to mirror skill relation ${skillId}: ${err}\n`);
430
+ process.stderr.write(`[file-mirror] failed to mirror skill relation ${sanitizeForLog(skillId)}: ${err}\n`);
396
431
  }
397
432
  }
398
433
  /** Append an attachment add/remove event. */
@@ -410,18 +445,30 @@ function mirrorAttachmentEvent(entityDir, action, file) {
410
445
  // ---------------------------------------------------------------------------
411
446
  /** Delete the entire mirror directory for a note, task or skill (including attachments). */
412
447
  function deleteMirrorDir(dir, id) {
448
+ const safeId = sanitizeEntityId(id);
449
+ if (!safeId) {
450
+ process.stderr.write(`[file-mirror] rejected invalid entity ID\n`);
451
+ return;
452
+ }
413
453
  try {
414
- fs.rmSync(path.join(dir, id), { recursive: true, force: true });
454
+ fs.rmSync(path.join(dir, safeId), { recursive: true, force: true });
415
455
  }
416
456
  catch (err) {
417
457
  if (err.code !== 'ENOENT') {
418
- process.stderr.write(`[file-mirror] failed to delete ${id}/: ${err}\n`);
458
+ process.stderr.write(`[file-mirror] failed to delete ${sanitizeForLog(id)}/: ${err}\n`);
419
459
  }
420
460
  }
421
461
  }
422
462
  // ---------------------------------------------------------------------------
423
463
  // Attachment file helpers (paths now go through attachments/ subdir)
424
464
  // ---------------------------------------------------------------------------
465
+ /** Sanitize an entity ID: extract basename, strip null bytes and path traversal. */
466
+ function sanitizeEntityId(id) {
467
+ const base = path.basename(id.replace(/\0/g, '').replace(/\\/g, '/')).trim();
468
+ if (base === '.' || base === '..')
469
+ return '';
470
+ return base;
471
+ }
425
472
  /** Sanitize a filename: extract basename, strip null bytes and path traversal. */
426
473
  function sanitizeFilename(name) {
427
474
  // Normalize backslashes to forward slashes (path.basename on Unix doesn't treat \ as separator)
@@ -433,29 +480,38 @@ function sanitizeFilename(name) {
433
480
  }
434
481
  /** Write an attachment file to the entity's attachments/ subdirectory. */
435
482
  function writeAttachment(baseDir, entityId, filename, data) {
483
+ const safeEntityId = sanitizeEntityId(entityId);
484
+ if (!safeEntityId)
485
+ throw new Error('Entity ID is empty after sanitization');
436
486
  const safe = sanitizeFilename(filename);
437
487
  if (!safe)
438
488
  throw new Error('Attachment filename is empty after sanitization');
439
- const attachmentsDir = path.join(baseDir, entityId, 'attachments');
489
+ const attachmentsDir = path.join(baseDir, safeEntityId, 'attachments');
440
490
  fs.mkdirSync(attachmentsDir, { recursive: true });
441
491
  fs.writeFileSync(path.join(attachmentsDir, safe), data);
442
492
  }
443
493
  /** Delete an attachment file from attachments/ subdir. Returns true if it existed. */
444
494
  function deleteAttachment(baseDir, entityId, filename) {
445
- const filePath = path.join(baseDir, entityId, 'attachments', sanitizeFilename(filename));
495
+ const safeEntityId = sanitizeEntityId(entityId);
496
+ if (!safeEntityId)
497
+ return false;
498
+ const filePath = path.join(baseDir, safeEntityId, 'attachments', sanitizeFilename(filename));
446
499
  try {
447
500
  fs.unlinkSync(filePath);
448
501
  return true;
449
502
  }
450
503
  catch (err) {
451
504
  if (err.code !== 'ENOENT') {
452
- process.stderr.write(`[file-mirror] failed to delete attachment ${filename}: ${err}\n`);
505
+ process.stderr.write(`[file-mirror] failed to delete attachment ${sanitizeForLog(filename)}: ${err}\n`);
453
506
  }
454
507
  return false;
455
508
  }
456
509
  }
457
510
  /** Get the absolute path of an attachment in attachments/ subdir, or null if not found. */
458
511
  function getAttachmentPath(baseDir, entityId, filename) {
459
- const filePath = path.join(baseDir, entityId, 'attachments', sanitizeFilename(filename));
512
+ const safeEntityId = sanitizeEntityId(entityId);
513
+ if (!safeEntityId)
514
+ return null;
515
+ const filePath = path.join(baseDir, safeEntityId, 'attachments', sanitizeFilename(filename));
460
516
  return fs.existsSync(filePath) ? filePath : null;
461
517
  }