@graphmemory/server 1.1.0 → 1.3.0

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 (104) hide show
  1. package/LICENSE +84 -12
  2. package/README.md +66 -101
  3. package/dist/api/index.js +279 -169
  4. package/dist/api/rest/index.js +36 -16
  5. package/dist/api/rest/tools.js +8 -1
  6. package/dist/api/rest/websocket.js +22 -1
  7. package/dist/api/tools/code/search-code.js +12 -9
  8. package/dist/api/tools/code/search-files.js +1 -1
  9. package/dist/api/tools/docs/cross-references.js +3 -2
  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/search-files.js +1 -1
  13. package/dist/api/tools/docs/search-snippets.js +1 -1
  14. package/dist/api/tools/docs/search.js +5 -4
  15. package/dist/api/tools/file-index/search-all-files.js +1 -1
  16. package/dist/api/tools/knowledge/add-attachment.js +14 -3
  17. package/dist/api/tools/knowledge/create-relation.js +2 -2
  18. package/dist/api/tools/knowledge/delete-relation.js +2 -2
  19. package/dist/api/tools/knowledge/find-linked-notes.js +1 -1
  20. package/dist/api/tools/knowledge/remove-attachment.js +5 -1
  21. package/dist/api/tools/knowledge/search-notes.js +5 -4
  22. package/dist/api/tools/skills/add-attachment.js +14 -3
  23. package/dist/api/tools/skills/recall-skills.js +1 -1
  24. package/dist/api/tools/skills/remove-attachment.js +5 -1
  25. package/dist/api/tools/skills/search-skills.js +6 -5
  26. package/dist/api/tools/tasks/add-attachment.js +14 -3
  27. package/dist/api/tools/tasks/create-task-link.js +1 -1
  28. package/dist/api/tools/tasks/delete-task-link.js +1 -1
  29. package/dist/api/tools/tasks/find-linked-tasks.js +1 -1
  30. package/dist/api/tools/tasks/remove-attachment.js +5 -1
  31. package/dist/api/tools/tasks/search-tasks.js +5 -4
  32. package/dist/cli/index.js +69 -311
  33. package/dist/cli/indexer.js +61 -29
  34. package/dist/graphs/code.js +70 -7
  35. package/dist/graphs/docs.js +15 -2
  36. package/dist/graphs/file-index.js +20 -6
  37. package/dist/graphs/file-lang.js +1 -1
  38. package/dist/graphs/knowledge.js +20 -3
  39. package/dist/graphs/manager-types.js +1 -1
  40. package/dist/graphs/skill.js +23 -4
  41. package/dist/graphs/task.js +23 -4
  42. package/dist/lib/embedding-codec.js +65 -0
  43. package/dist/lib/file-mirror.js +7 -7
  44. package/dist/lib/frontmatter.js +3 -2
  45. package/dist/lib/jwt.js +4 -4
  46. package/dist/lib/mirror-watcher.js +5 -4
  47. package/dist/lib/multi-config.js +60 -1
  48. package/dist/lib/parsers/code.js +158 -31
  49. package/dist/lib/parsers/codeblock.js +11 -6
  50. package/dist/lib/parsers/docs.js +59 -31
  51. package/dist/lib/parsers/languages/registry.js +10 -4
  52. package/dist/lib/parsers/languages/typescript.js +195 -48
  53. package/dist/lib/project-manager.js +14 -10
  54. package/dist/lib/search/bm25.js +18 -1
  55. package/dist/lib/search/code.js +12 -3
  56. package/dist/lib/watcher.js +17 -9
  57. package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
  58. package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
  59. package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
  60. package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
  61. package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
  62. package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
  63. package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
  64. package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
  65. package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
  66. package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
  67. package/dist/ui/assets/api-BMnBjMMf.js +1 -0
  68. package/dist/ui/assets/api-BlFF6gX-.js +1 -0
  69. package/dist/ui/assets/api-CrGJOcaN.js +1 -0
  70. package/dist/ui/assets/api-DuX-0a_X.js +1 -0
  71. package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
  72. package/dist/ui/assets/client-Bq88u7gN.js +1 -0
  73. package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
  74. package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
  75. package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
  76. package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
  77. package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
  78. package/dist/ui/assets/files-0bPg6NH9.js +1 -0
  79. package/dist/ui/assets/graph-DXGud_wF.js +1 -0
  80. package/dist/ui/assets/help-CEMQqZUR.js +891 -0
  81. package/dist/ui/assets/help-DJ52_fxN.js +1 -0
  82. package/dist/ui/assets/index-BCZDAYZi.js +2 -0
  83. package/dist/ui/assets/index-D6zSNtzo.css +1 -0
  84. package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
  85. package/dist/ui/assets/new-CpD7hOBA.js +1 -0
  86. package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
  87. package/dist/ui/assets/new-s8c0M75X.js +1 -0
  88. package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
  89. package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
  90. package/dist/ui/assets/search-EpJhdP2a.js +1 -0
  91. package/dist/ui/assets/skill-y9pizyqE.js +1 -0
  92. package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
  93. package/dist/ui/assets/tasks-CobouTKV.js +1 -0
  94. package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
  95. package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
  96. package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
  97. package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
  98. package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
  99. package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
  100. package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
  101. package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
  102. package/dist/ui/index.html +11 -3
  103. package/package.json +2 -2
  104. package/dist/ui/assets/index-D6oxrVF7.js +0 -1759
@@ -5,15 +5,16 @@ const zod_1 = require("zod");
5
5
  function register(server, mgr) {
6
6
  server.registerTool('search_tasks', {
7
7
  description: 'Semantic search over the task graph. ' +
8
- 'Finds the most relevant tasks using vector similarity, then expands results ' +
8
+ 'Supports three modes: hybrid (default, BM25 + vector), vector, keyword. ' +
9
+ 'Finds the most relevant tasks, then expands results ' +
9
10
  'by traversing relations between tasks (graph walk). ' +
10
11
  'Returns an array sorted by relevance score (0–1), each with: ' +
11
12
  'id, title, description, status, priority, tags, score.',
12
13
  inputSchema: {
13
14
  query: zod_1.z.string().describe('Natural language search query'),
14
- topK: zod_1.z.number().optional().describe('How many top similar tasks to use as seeds (default 5)'),
15
- bfsDepth: zod_1.z.number().optional().describe('How many hops to follow relations from each seed (default 1; 0 = no expansion)'),
16
- maxResults: zod_1.z.number().optional().describe('Maximum number of results to return (default 20)'),
15
+ topK: zod_1.z.number().min(1).max(500).optional().describe('How many top similar tasks to use as seeds (default 5)'),
16
+ 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
+ maxResults: zod_1.z.number().min(1).max(500).optional().describe('Maximum number of results to return (default 20)'),
17
18
  minScore: zod_1.z.number().min(0).max(1).optional().describe('Minimum relevance score 0–1 (default 0.5)'),
18
19
  bfsDecay: zod_1.z.number().min(0).max(1).optional().describe('Score multiplier per hop (default 0.8)'),
19
20
  searchMode: zod_1.z.enum(['hybrid', 'vector', 'keyword']).optional().describe('Search mode: hybrid (default, BM25 + vector), vector (embedding only), keyword (BM25 only)'),
package/dist/cli/index.js CHANGED
@@ -13,55 +13,22 @@ const multi_config_1 = require("../lib/multi-config");
13
13
  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
- const docs_1 = require("../graphs/docs");
17
- const code_1 = require("../graphs/code");
18
- const knowledge_1 = require("../graphs/knowledge");
19
- const file_index_1 = require("../graphs/file-index");
20
- const task_1 = require("../graphs/task");
21
- const skill_1 = require("../graphs/skill");
22
16
  const index_1 = require("../api/index");
23
- const indexer_1 = require("../cli/indexer");
24
- const watcher_1 = require("../lib/watcher");
25
17
  const program = new commander_1.Command();
26
18
  program
27
19
  .name('graphmemory')
28
20
  .description('MCP server for semantic graph memory from markdown docs and source code')
29
- .version('1.1.0');
21
+ .version('1.3.0');
30
22
  const parseIntArg = (v) => parseInt(v, 10);
31
23
  // ---------------------------------------------------------------------------
32
- // Helper: resolve a single project from YAML config + --project flag
24
+ // Helper: load config from file, or fall back to default (cwd as single project)
33
25
  // ---------------------------------------------------------------------------
34
- function resolveProject(configPath, projectId) {
35
- const mc = (0, multi_config_1.loadMultiConfig)(configPath);
36
- const ids = Array.from(mc.projects.keys());
37
- if (ids.length === 0) {
38
- process.stderr.write('[cli] No projects defined in config\n');
39
- process.exit(1);
40
- }
41
- const id = projectId ?? ids[0];
42
- const project = mc.projects.get(id);
43
- if (!project) {
44
- process.stderr.write(`[cli] Project "${id}" not found in config. Available: ${ids.join(', ')}\n`);
45
- process.exit(1);
26
+ function loadConfigOrDefault(configPath) {
27
+ if (fs_1.default.existsSync(configPath)) {
28
+ return (0, multi_config_1.loadMultiConfig)(configPath);
46
29
  }
47
- return { id, project, server: mc.server };
48
- }
49
- async function loadAllModels(projectId, config, modelsDir) {
50
- for (const gn of multi_config_1.GRAPH_NAMES) {
51
- if (!config.graphConfigs[gn].enabled)
52
- continue;
53
- await (0, embedder_1.loadModel)(config.graphConfigs[gn].model, config.graphConfigs[gn].embedding, modelsDir, `${projectId}:${gn}`);
54
- }
55
- }
56
- function buildEmbedFns(projectId) {
57
- const pair = (gn) => ({
58
- document: (q) => (0, embedder_1.embed)(q, '', `${projectId}:${gn}`),
59
- query: (q) => (0, embedder_1.embedQuery)(q, `${projectId}:${gn}`),
60
- });
61
- return {
62
- docs: pair('docs'), code: pair('code'), knowledge: pair('knowledge'),
63
- tasks: pair('tasks'), files: pair('files'), skills: pair('skills'),
64
- };
30
+ process.stderr.write(`[cli] Config "${configPath}" not found, using current directory as project\n`);
31
+ return (0, multi_config_1.defaultConfig)(process.cwd());
65
32
  }
66
33
  // ---------------------------------------------------------------------------
67
34
  // Command: index — scan one project and exit
@@ -74,7 +41,7 @@ program
74
41
  .option('--reindex', 'Discard persisted graphs and re-index from scratch')
75
42
  .action((opts) => {
76
43
  (async () => {
77
- const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
44
+ const mc = loadConfigOrDefault(opts.config);
78
45
  const reindex = !!opts.reindex;
79
46
  if (reindex)
80
47
  process.stderr.write('[index] Re-indexing from scratch\n');
@@ -139,172 +106,6 @@ program
139
106
  });
140
107
  });
141
108
  // ---------------------------------------------------------------------------
142
- // Command: mcp — single-project stdio mode
143
- // ---------------------------------------------------------------------------
144
- program
145
- .command('mcp')
146
- .description('Index one project (or workspace), keep watching for changes, and start MCP server on stdio')
147
- .option('--config <path>', 'Path to graph-memory.yaml', 'graph-memory.yaml')
148
- .option('--project <id>', 'Project ID (defaults to first project)')
149
- .option('--workspace <id>', 'Workspace ID (loads all workspace projects with shared graphs)')
150
- .option('--reindex', 'Discard persisted graphs and re-index from scratch')
151
- .action(async (opts) => {
152
- // Workspace mode: load all projects in the workspace with shared graphs
153
- if (opts.workspace) {
154
- const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
155
- const wsConfig = mc.workspaces.get(opts.workspace);
156
- if (!wsConfig) {
157
- process.stderr.write(`[mcp] Workspace "${opts.workspace}" not found in config. Available: ${Array.from(mc.workspaces.keys()).join(', ')}\n`);
158
- process.exit(1);
159
- }
160
- const fresh = !!opts.reindex;
161
- if (fresh)
162
- process.stderr.write(`[mcp] Re-indexing workspace "${opts.workspace}" from scratch\n`);
163
- const manager = new project_manager_1.ProjectManager(mc.server);
164
- await manager.addWorkspace(opts.workspace, wsConfig, fresh);
165
- for (const projId of wsConfig.projects) {
166
- const projConfig = mc.projects.get(projId);
167
- if (!projConfig) {
168
- process.stderr.write(`[mcp] Project "${projId}" referenced by workspace not found\n`);
169
- process.exit(1);
170
- }
171
- await manager.addProject(projId, projConfig, fresh, opts.workspace);
172
- }
173
- // Use specified project (or first) for stdio server
174
- const targetId = opts.project ?? wsConfig.projects[0];
175
- if (!wsConfig.projects.includes(targetId)) {
176
- process.stderr.write(`[mcp] Project "${targetId}" is not part of workspace "${opts.workspace}"\n`);
177
- process.exit(1);
178
- }
179
- const instance = manager.getProject(targetId);
180
- const sessionCtx = {
181
- projectId: targetId,
182
- workspaceId: opts.workspace,
183
- workspaceProjects: wsConfig.projects,
184
- };
185
- await (0, index_1.startStdioServer)(instance.docGraph, instance.codeGraph, instance.knowledgeGraph, instance.fileIndexGraph, instance.taskGraph, instance.embedFns, instance.config.projectDir, instance.skillGraph, sessionCtx);
186
- // Load models and index in background
187
- (async () => {
188
- await manager.loadWorkspaceModels(opts.workspace);
189
- for (const projId of wsConfig.projects) {
190
- await manager.loadModels(projId);
191
- await manager.startIndexing(projId);
192
- }
193
- await manager.startWorkspaceMirror(opts.workspace);
194
- process.stderr.write(`[mcp] Workspace "${opts.workspace}" fully indexed\n`);
195
- })().catch((err) => {
196
- process.stderr.write(`[mcp] Workspace indexer error: ${err}\n`);
197
- });
198
- let shuttingDown = false;
199
- async function shutdown() {
200
- if (shuttingDown) {
201
- process.stderr.write('[mcp] Force exit\n');
202
- process.exit(1);
203
- }
204
- shuttingDown = true;
205
- process.stderr.write('[mcp] Shutting down...\n');
206
- const forceTimer = setTimeout(() => { process.stderr.write('[mcp] Shutdown timeout, force exit\n'); process.exit(1); }, 5000);
207
- try {
208
- await manager.shutdown();
209
- }
210
- catch { /* ignore */ }
211
- clearTimeout(forceTimer);
212
- // Let event loop drain naturally — avoids ONNX global thread pool destructor crash on macOS
213
- }
214
- process.on('SIGINT', () => { void shutdown(); });
215
- process.on('SIGTERM', () => { void shutdown(); });
216
- return;
217
- }
218
- const { id, project, server } = resolveProject(opts.config, opts.project);
219
- const projectDir = path_1.default.resolve(project.projectDir);
220
- const fresh = !!opts.reindex;
221
- if (fresh)
222
- process.stderr.write(`[mcp] Re-indexing project "${id}" from scratch\n`);
223
- const gc = project.graphConfigs;
224
- // Load persisted graphs (or create fresh ones if reindexing / model changed) and start MCP server immediately
225
- const docGraph = gc.docs.enabled ? (0, docs_1.loadGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.docs.model)) : undefined;
226
- const codeGraph = gc.code.enabled ? (0, code_1.loadCodeGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.code.model)) : undefined;
227
- const knowledgeGraph = gc.knowledge.enabled ? (0, knowledge_1.loadKnowledgeGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model)) : undefined;
228
- const fileIndexGraph = gc.files.enabled ? (0, file_index_1.loadFileIndexGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.files.model)) : undefined;
229
- const taskGraph = gc.tasks.enabled ? (0, task_1.loadTaskGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model)) : undefined;
230
- const skillGraph = gc.skills.enabled ? (0, skill_1.loadSkillGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.skills.model)) : undefined;
231
- const embedFns = buildEmbedFns(id);
232
- const sessionCtx = { projectId: id };
233
- await (0, index_1.startStdioServer)(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFns, project.projectDir, skillGraph, sessionCtx);
234
- // Load models and start watcher in the background
235
- let watcher;
236
- let indexer;
237
- async function startIndexing() {
238
- await loadAllModels(id, project, server.modelsDir);
239
- indexer = (0, indexer_1.createProjectIndexer)(docGraph, codeGraph, {
240
- projectId: id,
241
- projectDir,
242
- docsInclude: gc.docs.enabled ? gc.docs.include : undefined,
243
- docsExclude: gc.docs.exclude,
244
- codeInclude: gc.code.enabled ? gc.code.include : undefined,
245
- codeExclude: gc.code.exclude,
246
- filesExclude: gc.files.exclude,
247
- chunkDepth: project.chunkDepth,
248
- maxFileSize: project.maxFileSize,
249
- docsModelName: `${id}:docs`,
250
- codeModelName: `${id}:code`,
251
- filesModelName: `${id}:files`,
252
- }, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph);
253
- watcher = indexer.watch();
254
- await watcher.whenReady;
255
- await indexer.drain();
256
- if (docGraph) {
257
- (0, docs_1.saveGraph)(docGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.docs.model));
258
- process.stderr.write(`[mcp] Docs indexed. ${docGraph.order} nodes, ${docGraph.size} edges.\n`);
259
- }
260
- if (codeGraph) {
261
- (0, code_1.saveCodeGraph)(codeGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.code.model));
262
- process.stderr.write(`[mcp] Code indexed. ${codeGraph.order} nodes, ${codeGraph.size} edges.\n`);
263
- }
264
- if (fileIndexGraph) {
265
- (0, file_index_1.saveFileIndexGraph)(fileIndexGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.files.model));
266
- process.stderr.write(`[mcp] File index done. ${fileIndexGraph.order} nodes, ${fileIndexGraph.size} edges.\n`);
267
- }
268
- }
269
- startIndexing().catch((err) => {
270
- process.stderr.write(`[mcp] Indexer error: ${err}\n`);
271
- });
272
- let shuttingDown = false;
273
- async function shutdown() {
274
- if (shuttingDown) {
275
- process.stderr.write('[mcp] Force exit\n');
276
- process.exit(1);
277
- }
278
- shuttingDown = true;
279
- process.stderr.write('[mcp] Shutting down...\n');
280
- const forceTimer = setTimeout(() => {
281
- process.stderr.write('[mcp] Shutdown timeout, force exit\n');
282
- process.exit(1);
283
- }, 5000);
284
- try {
285
- if (watcher)
286
- await watcher.close();
287
- if (indexer)
288
- await indexer.drain();
289
- if (docGraph)
290
- (0, docs_1.saveGraph)(docGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.docs.model));
291
- if (codeGraph)
292
- (0, code_1.saveCodeGraph)(codeGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.code.model));
293
- if (knowledgeGraph)
294
- (0, knowledge_1.saveKnowledgeGraph)(knowledgeGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model));
295
- if (fileIndexGraph)
296
- (0, file_index_1.saveFileIndexGraph)(fileIndexGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.files.model));
297
- if (taskGraph)
298
- (0, task_1.saveTaskGraph)(taskGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model));
299
- }
300
- catch { /* ignore */ }
301
- clearTimeout(forceTimer);
302
- // Let event loop drain naturally — avoids ONNX global thread pool destructor crash on macOS
303
- }
304
- process.on('SIGINT', () => { void shutdown(); });
305
- process.on('SIGTERM', () => { void shutdown(); });
306
- });
307
- // ---------------------------------------------------------------------------
308
109
  // Command: serve — multi-project HTTP mode
309
110
  // ---------------------------------------------------------------------------
310
111
  program
@@ -315,7 +116,7 @@ program
315
116
  .option('--port <n>', 'HTTP server port', parseIntArg)
316
117
  .option('--reindex', 'Discard persisted graphs and re-index from scratch')
317
118
  .action(async (opts) => {
318
- const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
119
+ const mc = loadConfigOrDefault(opts.config);
319
120
  const host = opts.host ?? mc.server.host;
320
121
  const port = opts.port ?? mc.server.port;
321
122
  const sessionTimeoutMs = mc.server.sessionTimeout * 1000;
@@ -345,7 +146,48 @@ program
345
146
  }
346
147
  // Embedding API model name (loaded in background with other models)
347
148
  const embeddingApiModelName = mc.server.embeddingApi?.enabled ? '__server__' : undefined;
348
- // Start HTTP server immediately (before models are loaded)
149
+ // Load models and index all projects before starting HTTP
150
+ // Load embedding API model if enabled
151
+ if (embeddingApiModelName) {
152
+ try {
153
+ await (0, embedder_1.loadModel)(mc.server.model, mc.server.embedding, mc.server.modelsDir, embeddingApiModelName);
154
+ process.stderr.write(`[serve] Embedding API model ready\n`);
155
+ }
156
+ catch (err) {
157
+ process.stderr.write(`[serve] Failed to load embedding API model: ${err}\n`);
158
+ }
159
+ }
160
+ // Load workspace models
161
+ for (const wsId of manager.listWorkspaces()) {
162
+ try {
163
+ await manager.loadWorkspaceModels(wsId);
164
+ }
165
+ catch (err) {
166
+ process.stderr.write(`[serve] Failed to load workspace "${wsId}" models: ${err}\n`);
167
+ }
168
+ }
169
+ // Load project models and start indexing
170
+ for (const id of manager.listProjects()) {
171
+ try {
172
+ await manager.loadModels(id);
173
+ await manager.startIndexing(id);
174
+ }
175
+ catch (err) {
176
+ process.stderr.write(`[serve] Failed to initialize project "${id}": ${err}\n`);
177
+ }
178
+ }
179
+ // Start workspace mirror watchers (after all projects are indexed)
180
+ for (const wsId of manager.listWorkspaces()) {
181
+ try {
182
+ await manager.startWorkspaceMirror(wsId);
183
+ }
184
+ catch (err) {
185
+ process.stderr.write(`[serve] Failed to start workspace "${wsId}" mirror: ${err}\n`);
186
+ }
187
+ }
188
+ // Start auto-save
189
+ manager.startAutoSave();
190
+ // Start HTTP server (all models loaded, all projects indexed)
349
191
  const httpServer = await (0, index_1.startMultiProjectHttpServer)(host, port, sessionTimeoutMs, manager, {
350
192
  serverConfig: mc.server,
351
193
  users: mc.users,
@@ -357,104 +199,6 @@ program
357
199
  openSockets.add(socket);
358
200
  socket.on('close', () => openSockets.delete(socket));
359
201
  });
360
- // Start auto-save
361
- manager.startAutoSave();
362
- // Load models and start indexing in background (workspaces first, then projects)
363
- async function initProjects() {
364
- // Load embedding API model if enabled
365
- if (embeddingApiModelName) {
366
- try {
367
- await (0, embedder_1.loadModel)(mc.server.model, mc.server.embedding, mc.server.modelsDir, embeddingApiModelName);
368
- process.stderr.write(`[serve] Embedding API model ready\n`);
369
- }
370
- catch (err) {
371
- process.stderr.write(`[serve] Failed to load embedding API model: ${err}\n`);
372
- }
373
- }
374
- // Load workspace models
375
- for (const wsId of manager.listWorkspaces()) {
376
- try {
377
- await manager.loadWorkspaceModels(wsId);
378
- }
379
- catch (err) {
380
- process.stderr.write(`[serve] Failed to load workspace "${wsId}" models: ${err}\n`);
381
- }
382
- }
383
- // Load project models and start indexing
384
- for (const id of manager.listProjects()) {
385
- try {
386
- await manager.loadModels(id);
387
- await manager.startIndexing(id);
388
- }
389
- catch (err) {
390
- process.stderr.write(`[serve] Failed to initialize project "${id}": ${err}\n`);
391
- }
392
- }
393
- // Start workspace mirror watchers (after all projects are indexed)
394
- for (const wsId of manager.listWorkspaces()) {
395
- try {
396
- await manager.startWorkspaceMirror(wsId);
397
- }
398
- catch (err) {
399
- process.stderr.write(`[serve] Failed to start workspace "${wsId}" mirror: ${err}\n`);
400
- }
401
- }
402
- }
403
- initProjects().catch((err) => {
404
- process.stderr.write(`[serve] Init error: ${err}\n`);
405
- });
406
- // Watch YAML config for hot-reload
407
- let reloading = false;
408
- const configWatcher = (0, watcher_1.startWatcher)(path_1.default.dirname(path_1.default.resolve(opts.config)), {
409
- onAdd: () => { },
410
- onChange: async (f) => {
411
- if (path_1.default.resolve(f) !== path_1.default.resolve(opts.config))
412
- return;
413
- if (reloading)
414
- return;
415
- reloading = true;
416
- try {
417
- process.stderr.write('[serve] Config changed, reloading...\n');
418
- const newMc = (0, multi_config_1.loadMultiConfig)(opts.config);
419
- const currentIds = new Set(manager.listProjects());
420
- const newIds = new Set(newMc.projects.keys());
421
- // Remove projects no longer in config
422
- for (const id of currentIds) {
423
- if (!newIds.has(id)) {
424
- await manager.removeProject(id);
425
- }
426
- }
427
- // Add new projects
428
- for (const [id, config] of newMc.projects) {
429
- if (!currentIds.has(id)) {
430
- await manager.addProject(id, config);
431
- await manager.loadModels(id);
432
- await manager.startIndexing(id);
433
- }
434
- }
435
- // Re-add changed projects
436
- for (const [id, config] of newMc.projects) {
437
- if (currentIds.has(id)) {
438
- const existing = manager.getProject(id);
439
- if (existing && JSON.stringify(existing.config) !== JSON.stringify(config)) {
440
- await manager.removeProject(id);
441
- await manager.addProject(id, config);
442
- await manager.loadModels(id);
443
- await manager.startIndexing(id);
444
- }
445
- }
446
- }
447
- process.stderr.write('[serve] Config reload complete\n');
448
- }
449
- catch (err) {
450
- process.stderr.write(`[serve] Config reload error: ${err}\n`);
451
- }
452
- finally {
453
- reloading = false;
454
- }
455
- },
456
- onUnlink: () => { },
457
- }, path_1.default.basename(opts.config));
458
202
  let shuttingDown = false;
459
203
  async function shutdown() {
460
204
  if (shuttingDown) {
@@ -475,7 +219,6 @@ program
475
219
  socket.destroy();
476
220
  }
477
221
  openSockets.clear();
478
- await configWatcher.close();
479
222
  await manager.shutdown();
480
223
  }
481
224
  catch { /* ignore */ }
@@ -572,13 +315,28 @@ usersCmd
572
315
  process.stderr.write('Passwords do not match\n');
573
316
  process.exit(1);
574
317
  }
318
+ // Validate inputs
319
+ if (/[\x00-\x1f\x7f]/.test(name)) {
320
+ process.stderr.write('Name contains invalid characters\n');
321
+ process.exit(1);
322
+ }
323
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
324
+ process.stderr.write('Invalid email format\n');
325
+ process.exit(1);
326
+ }
327
+ if (password.length > 256) {
328
+ process.stderr.write('Password too long (max 256)\n');
329
+ process.exit(1);
330
+ }
575
331
  const pwHash = await (0, jwt_1.hashPassword)(password);
576
332
  const apiKey = `mgm-${crypto_1.default.randomBytes(24).toString('base64url')}`;
577
- // Build YAML block for the new user
333
+ // Build YAML block for the new user — escape quotes to prevent YAML injection
334
+ const safeName = name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
335
+ const safeEmail = email.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
578
336
  const userBlock = [
579
337
  ` ${id}:`,
580
- ` name: "${name}"`,
581
- ` email: "${email}"`,
338
+ ` name: "${safeName}"`,
339
+ ` email: "${safeEmail}"`,
582
340
  ` apiKey: "${apiKey}"`,
583
341
  ` passwordHash: "${pwHash}"`,
584
342
  ].join('\n');
@@ -19,31 +19,51 @@ const skill_1 = require("../graphs/skill");
19
19
  const file_index_1 = require("../graphs/file-index");
20
20
  function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph) {
21
21
  // Three independent serial queues — docs, code, and file index.
22
- let docsQueue = Promise.resolve();
23
- let codeQueue = Promise.resolve();
24
- let fileQueue = Promise.resolve();
25
- // Error tracking
26
- let docErrors = 0;
27
- let codeErrors = 0;
28
- let fileErrors = 0;
29
- function enqueueDoc(fn) {
30
- docsQueue = docsQueue.then(fn).catch((err) => {
31
- docErrors++;
32
- process.stderr.write(`[indexer] Doc error: ${err}\n`);
33
- });
34
- }
35
- function enqueueCode(fn) {
36
- codeQueue = codeQueue.then(fn).catch((err) => {
37
- codeErrors++;
38
- process.stderr.write(`[indexer] Code error: ${err}\n`);
39
- });
40
- }
41
- function enqueueFile(fn) {
42
- fileQueue = fileQueue.then(fn).catch((err) => {
43
- fileErrors++;
44
- process.stderr.write(`[indexer] File index error: ${err}\n`);
45
- });
22
+ // Array-based to avoid promise chain memory accumulation during scan.
23
+ function createSerialQueue(label) {
24
+ const pending = [];
25
+ let running = false;
26
+ let errors = 0;
27
+ let idleResolve = null;
28
+ let idlePromise = Promise.resolve();
29
+ async function pump() {
30
+ running = true;
31
+ while (pending.length > 0) {
32
+ const fn = pending.shift();
33
+ try {
34
+ await fn();
35
+ }
36
+ catch (err) {
37
+ errors++;
38
+ process.stderr.write(`[indexer] ${label} error: ${err}\n`);
39
+ }
40
+ }
41
+ running = false;
42
+ if (idleResolve) {
43
+ idleResolve();
44
+ idleResolve = null;
45
+ }
46
+ }
47
+ return {
48
+ enqueue(fn) {
49
+ pending.push(fn);
50
+ if (!running) {
51
+ idlePromise = new Promise(r => { idleResolve = r; });
52
+ void pump();
53
+ }
54
+ },
55
+ waitIdle() {
56
+ return (pending.length === 0 && !running) ? Promise.resolve() : idlePromise;
57
+ },
58
+ get errors() { return errors; },
59
+ };
46
60
  }
61
+ const docsQueue = createSerialQueue('Doc');
62
+ const codeQueue = createSerialQueue('Code');
63
+ const fileQueue = createSerialQueue('File index');
64
+ function enqueueDoc(fn) { docsQueue.enqueue(fn); }
65
+ function enqueueCode(fn) { codeQueue.enqueue(fn); }
66
+ function enqueueFile(fn) { fileQueue.enqueue(fn); }
47
67
  // ---------------------------------------------------------------------------
48
68
  // Per-file indexing
49
69
  // ---------------------------------------------------------------------------
@@ -126,7 +146,16 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
126
146
  const parsed = await (0, code_1.parseCodeFile)(absolutePath, config.projectDir, mtime);
127
147
  // Batch-embed all symbols + file-level in one forward pass
128
148
  const batchInputs = parsed.nodes.map(({ attrs }) => ({ title: attrs.signature, content: attrs.docComment }));
129
- batchInputs.push({ title: fileId, content: '' });
149
+ // File-level embedding: path + exported symbol names + import summary
150
+ const fileNode = parsed.nodes.find(n => n.attrs.kind === 'file');
151
+ const exportedNames = parsed.nodes
152
+ .filter(n => n.attrs.isExported && n.attrs.kind !== 'file')
153
+ .map(n => n.attrs.name);
154
+ const fileEmbedTitle = exportedNames.length > 0
155
+ ? `${fileId} ${exportedNames.join(' ')}`
156
+ : fileId;
157
+ const fileEmbedContent = fileNode?.attrs.body ?? ''; // body = importSummary for file nodes
158
+ batchInputs.push({ title: fileEmbedTitle, content: fileEmbedContent });
130
159
  const embeddings = await (0, embedder_1.embedBatch)(batchInputs, config.codeModelName);
131
160
  for (let i = 0; i < parsed.nodes.length; i++) {
132
161
  parsed.nodes[i].attrs.embedding = embeddings[i];
@@ -226,7 +255,7 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
226
255
  function scan() {
227
256
  function walk(dir) {
228
257
  for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
229
- if (entry.name.startsWith('.'))
258
+ if (entry.name.startsWith('.') || watcher_1.ALWAYS_IGNORED.has(entry.name))
230
259
  continue;
231
260
  const full = path_1.default.join(dir, entry.name);
232
261
  if (entry.isDirectory()) {
@@ -252,7 +281,7 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
252
281
  }, '**/*', allExcludePatterns.length > 0 ? allExcludePatterns : undefined);
253
282
  }
254
283
  async function drain() {
255
- await Promise.all([docsQueue, codeQueue, fileQueue]);
284
+ await Promise.all([docsQueue.waitIdle(), codeQueue.waitIdle(), fileQueue.waitIdle()]);
256
285
  if (fileIndexGraph)
257
286
  (0, file_index_1.rebuildDirectoryStats)(fileIndexGraph);
258
287
  // Resolve cross-file edges that were deferred during indexing
@@ -265,10 +294,13 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
265
294
  const codeImports = (0, code_2.resolvePendingImports)(codeGraph);
266
295
  if (codeImports > 0)
267
296
  process.stderr.write(`[indexer] Resolved ${codeImports} deferred code import edge(s)\n`);
297
+ const codeEdges = (0, code_2.resolvePendingEdges)(codeGraph);
298
+ if (codeEdges > 0)
299
+ process.stderr.write(`[indexer] Resolved ${codeEdges} deferred code extends/implements edge(s)\n`);
268
300
  }
269
- const totalErrors = docErrors + codeErrors + fileErrors;
301
+ const totalErrors = docsQueue.errors + codeQueue.errors + fileQueue.errors;
270
302
  if (totalErrors > 0) {
271
- process.stderr.write(`[indexer] Completed with ${totalErrors} error(s): docs=${docErrors}, code=${codeErrors}, files=${fileErrors}\n`);
303
+ process.stderr.write(`[indexer] Completed with ${totalErrors} error(s): docs=${docsQueue.errors}, code=${codeQueue.errors}, files=${fileQueue.errors}\n`);
272
304
  }
273
305
  }
274
306
  return { scan, watch, drain };