@graphmemory/server 1.1.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 (123) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +216 -0
  3. package/dist/api/index.js +473 -0
  4. package/dist/api/rest/code.js +78 -0
  5. package/dist/api/rest/docs.js +80 -0
  6. package/dist/api/rest/embed.js +47 -0
  7. package/dist/api/rest/files.js +64 -0
  8. package/dist/api/rest/graph.js +71 -0
  9. package/dist/api/rest/index.js +371 -0
  10. package/dist/api/rest/knowledge.js +239 -0
  11. package/dist/api/rest/skills.js +285 -0
  12. package/dist/api/rest/tasks.js +273 -0
  13. package/dist/api/rest/tools.js +157 -0
  14. package/dist/api/rest/validation.js +196 -0
  15. package/dist/api/rest/websocket.js +71 -0
  16. package/dist/api/tools/code/get-file-symbols.js +30 -0
  17. package/dist/api/tools/code/get-symbol.js +22 -0
  18. package/dist/api/tools/code/list-files.js +18 -0
  19. package/dist/api/tools/code/search-code.js +27 -0
  20. package/dist/api/tools/code/search-files.js +22 -0
  21. package/dist/api/tools/context/get-context.js +19 -0
  22. package/dist/api/tools/docs/cross-references.js +76 -0
  23. package/dist/api/tools/docs/explain-symbol.js +55 -0
  24. package/dist/api/tools/docs/find-examples.js +52 -0
  25. package/dist/api/tools/docs/get-node.js +24 -0
  26. package/dist/api/tools/docs/get-toc.js +22 -0
  27. package/dist/api/tools/docs/list-snippets.js +46 -0
  28. package/dist/api/tools/docs/list-topics.js +18 -0
  29. package/dist/api/tools/docs/search-files.js +22 -0
  30. package/dist/api/tools/docs/search-snippets.js +43 -0
  31. package/dist/api/tools/docs/search.js +27 -0
  32. package/dist/api/tools/file-index/get-file-info.js +21 -0
  33. package/dist/api/tools/file-index/list-all-files.js +28 -0
  34. package/dist/api/tools/file-index/search-all-files.js +24 -0
  35. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  36. package/dist/api/tools/knowledge/create-note.js +20 -0
  37. package/dist/api/tools/knowledge/create-relation.js +29 -0
  38. package/dist/api/tools/knowledge/delete-note.js +19 -0
  39. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  40. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  41. package/dist/api/tools/knowledge/get-note.js +20 -0
  42. package/dist/api/tools/knowledge/list-notes.js +18 -0
  43. package/dist/api/tools/knowledge/list-relations.js +17 -0
  44. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  45. package/dist/api/tools/knowledge/search-notes.js +25 -0
  46. package/dist/api/tools/knowledge/update-note.js +34 -0
  47. package/dist/api/tools/skills/add-attachment.js +31 -0
  48. package/dist/api/tools/skills/bump-usage.js +19 -0
  49. package/dist/api/tools/skills/create-skill-link.js +25 -0
  50. package/dist/api/tools/skills/create-skill.js +26 -0
  51. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  52. package/dist/api/tools/skills/delete-skill.js +20 -0
  53. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  54. package/dist/api/tools/skills/get-skill.js +21 -0
  55. package/dist/api/tools/skills/link-skill.js +23 -0
  56. package/dist/api/tools/skills/list-skills.js +20 -0
  57. package/dist/api/tools/skills/recall-skills.js +18 -0
  58. package/dist/api/tools/skills/remove-attachment.js +19 -0
  59. package/dist/api/tools/skills/search-skills.js +25 -0
  60. package/dist/api/tools/skills/update-skill.js +58 -0
  61. package/dist/api/tools/tasks/add-attachment.js +31 -0
  62. package/dist/api/tools/tasks/create-task-link.js +25 -0
  63. package/dist/api/tools/tasks/create-task.js +26 -0
  64. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  65. package/dist/api/tools/tasks/delete-task.js +20 -0
  66. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  67. package/dist/api/tools/tasks/get-task.js +20 -0
  68. package/dist/api/tools/tasks/link-task.js +23 -0
  69. package/dist/api/tools/tasks/list-tasks.js +25 -0
  70. package/dist/api/tools/tasks/move-task.js +38 -0
  71. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  72. package/dist/api/tools/tasks/search-tasks.js +25 -0
  73. package/dist/api/tools/tasks/update-task.js +58 -0
  74. package/dist/cli/index.js +617 -0
  75. package/dist/cli/indexer.js +275 -0
  76. package/dist/graphs/attachment-types.js +74 -0
  77. package/dist/graphs/code-types.js +10 -0
  78. package/dist/graphs/code.js +204 -0
  79. package/dist/graphs/docs.js +231 -0
  80. package/dist/graphs/file-index-types.js +10 -0
  81. package/dist/graphs/file-index.js +310 -0
  82. package/dist/graphs/file-lang.js +119 -0
  83. package/dist/graphs/knowledge-types.js +32 -0
  84. package/dist/graphs/knowledge.js +768 -0
  85. package/dist/graphs/manager-types.js +87 -0
  86. package/dist/graphs/skill-types.js +10 -0
  87. package/dist/graphs/skill.js +1016 -0
  88. package/dist/graphs/task-types.js +17 -0
  89. package/dist/graphs/task.js +972 -0
  90. package/dist/lib/access.js +67 -0
  91. package/dist/lib/embedder.js +235 -0
  92. package/dist/lib/events-log.js +401 -0
  93. package/dist/lib/file-import.js +328 -0
  94. package/dist/lib/file-mirror.js +461 -0
  95. package/dist/lib/frontmatter.js +17 -0
  96. package/dist/lib/jwt.js +146 -0
  97. package/dist/lib/mirror-watcher.js +637 -0
  98. package/dist/lib/multi-config.js +393 -0
  99. package/dist/lib/parsers/code.js +214 -0
  100. package/dist/lib/parsers/codeblock.js +33 -0
  101. package/dist/lib/parsers/docs.js +199 -0
  102. package/dist/lib/parsers/languages/index.js +15 -0
  103. package/dist/lib/parsers/languages/registry.js +68 -0
  104. package/dist/lib/parsers/languages/types.js +2 -0
  105. package/dist/lib/parsers/languages/typescript.js +306 -0
  106. package/dist/lib/project-manager.js +458 -0
  107. package/dist/lib/promise-queue.js +22 -0
  108. package/dist/lib/search/bm25.js +167 -0
  109. package/dist/lib/search/code.js +103 -0
  110. package/dist/lib/search/docs.js +106 -0
  111. package/dist/lib/search/file-index.js +31 -0
  112. package/dist/lib/search/files.js +61 -0
  113. package/dist/lib/search/knowledge.js +101 -0
  114. package/dist/lib/search/skills.js +104 -0
  115. package/dist/lib/search/tasks.js +103 -0
  116. package/dist/lib/team.js +89 -0
  117. package/dist/lib/watcher.js +67 -0
  118. package/dist/ui/assets/index-D6oxrVF7.js +1759 -0
  119. package/dist/ui/assets/index-kKd4mVrh.css +1 -0
  120. package/dist/ui/favicon.svg +1 -0
  121. package/dist/ui/icons.svg +24 -0
  122. package/dist/ui/index.html +14 -0
  123. package/package.json +89 -0
@@ -0,0 +1,617 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ const readline_1 = __importDefault(require("readline"));
12
+ const multi_config_1 = require("../lib/multi-config");
13
+ const jwt_1 = require("../lib/jwt");
14
+ const project_manager_1 = require("../lib/project-manager");
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
+ const index_1 = require("../api/index");
23
+ const indexer_1 = require("../cli/indexer");
24
+ const watcher_1 = require("../lib/watcher");
25
+ const program = new commander_1.Command();
26
+ program
27
+ .name('graphmemory')
28
+ .description('MCP server for semantic graph memory from markdown docs and source code')
29
+ .version('1.1.0');
30
+ const parseIntArg = (v) => parseInt(v, 10);
31
+ // ---------------------------------------------------------------------------
32
+ // Helper: resolve a single project from YAML config + --project flag
33
+ // ---------------------------------------------------------------------------
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);
46
+ }
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
+ };
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Command: index — scan one project and exit
68
+ // ---------------------------------------------------------------------------
69
+ program
70
+ .command('index')
71
+ .description('Scan and embed all matching files, then exit (all projects or one with --project)')
72
+ .option('--config <path>', 'Path to graph-memory.yaml', 'graph-memory.yaml')
73
+ .option('--project <id>', 'Project ID to index (omit to index all)')
74
+ .option('--reindex', 'Discard persisted graphs and re-index from scratch')
75
+ .action((opts) => {
76
+ (async () => {
77
+ const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
78
+ const reindex = !!opts.reindex;
79
+ if (reindex)
80
+ process.stderr.write('[index] Re-indexing from scratch\n');
81
+ const manager = new project_manager_1.ProjectManager(mc.server);
82
+ // Build workspace membership lookup
83
+ const projectWorkspace = new Map();
84
+ for (const [wsId, wsConfig] of mc.workspaces) {
85
+ for (const projId of wsConfig.projects) {
86
+ projectWorkspace.set(projId, wsId);
87
+ }
88
+ }
89
+ // Add workspaces first
90
+ for (const [wsId, wsConfig] of mc.workspaces) {
91
+ await manager.addWorkspace(wsId, wsConfig, reindex);
92
+ }
93
+ // Add projects (workspace projects share knowledge/task/skill graphs)
94
+ const ids = opts.project ? [opts.project] : Array.from(mc.projects.keys());
95
+ if (ids.length === 0) {
96
+ process.stderr.write('[index] No projects defined in config\n');
97
+ process.exit(1);
98
+ }
99
+ for (const id of ids) {
100
+ const project = mc.projects.get(id);
101
+ if (!project) {
102
+ process.stderr.write(`[index] Project "${id}" not found in config. Available: ${Array.from(mc.projects.keys()).join(', ')}\n`);
103
+ process.exit(1);
104
+ }
105
+ await manager.addProject(id, project, reindex, projectWorkspace.get(id));
106
+ }
107
+ // Load models (workspaces first, then projects)
108
+ for (const wsId of manager.listWorkspaces()) {
109
+ await manager.loadWorkspaceModels(wsId);
110
+ }
111
+ for (const id of ids) {
112
+ await manager.loadModels(id);
113
+ }
114
+ // Index all projects
115
+ for (const id of ids) {
116
+ process.stderr.write(`[index] Indexing project "${id}"...\n`);
117
+ await manager.startIndexing(id);
118
+ const instance = manager.getProject(id);
119
+ if (instance.docGraph) {
120
+ process.stderr.write(`[index] "${id}" docs: ${instance.docGraph.order} nodes, ${instance.docGraph.size} edges\n`);
121
+ }
122
+ if (instance.codeGraph) {
123
+ process.stderr.write(`[index] "${id}" code: ${instance.codeGraph.order} nodes, ${instance.codeGraph.size} edges\n`);
124
+ }
125
+ if (instance.fileIndexGraph) {
126
+ process.stderr.write(`[index] "${id}" files: ${instance.fileIndexGraph.order} nodes, ${instance.fileIndexGraph.size} edges\n`);
127
+ }
128
+ }
129
+ // Save workspaces
130
+ for (const wsId of manager.listWorkspaces()) {
131
+ const ws = manager.getWorkspace(wsId);
132
+ process.stderr.write(`[index] Workspace "${wsId}" knowledge: ${ws.knowledgeGraph.order} nodes, tasks: ${ws.taskGraph.order} nodes, skills: ${ws.skillGraph.order} nodes\n`);
133
+ }
134
+ await manager.shutdown();
135
+ process.stderr.write(`[index] Done. Indexed ${ids.length} project${ids.length > 1 ? 's' : ''}.\n`);
136
+ })().catch((err) => {
137
+ process.stderr.write(`[index] Fatal: ${err}\n`);
138
+ process.exit(1);
139
+ });
140
+ });
141
+ // ---------------------------------------------------------------------------
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
+ // Command: serve — multi-project HTTP mode
309
+ // ---------------------------------------------------------------------------
310
+ program
311
+ .command('serve')
312
+ .description('Start multi-project MCP server over HTTP')
313
+ .option('--config <path>', 'Path to graph-memory.yaml', 'graph-memory.yaml')
314
+ .option('--host <addr>', 'HTTP server bind address')
315
+ .option('--port <n>', 'HTTP server port', parseIntArg)
316
+ .option('--reindex', 'Discard persisted graphs and re-index from scratch')
317
+ .action(async (opts) => {
318
+ const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
319
+ const host = opts.host ?? mc.server.host;
320
+ const port = opts.port ?? mc.server.port;
321
+ const sessionTimeoutMs = mc.server.sessionTimeout * 1000;
322
+ // Validate jwtSecret when users are defined
323
+ const hasUsers = Object.keys(mc.users).length > 0;
324
+ if (hasUsers && !mc.server.jwtSecret) {
325
+ process.stderr.write('[serve] Warning: users are defined but server.jwtSecret is not set. UI password login will not work (API key auth still works).\n');
326
+ }
327
+ const reindex = !!opts.reindex;
328
+ if (reindex)
329
+ process.stderr.write('[serve] Re-indexing all projects from scratch\n');
330
+ const manager = new project_manager_1.ProjectManager(mc.server);
331
+ // Build workspace membership lookup
332
+ const projectWorkspace = new Map();
333
+ for (const [wsId, wsConfig] of mc.workspaces) {
334
+ for (const projId of wsConfig.projects) {
335
+ projectWorkspace.set(projId, wsId);
336
+ }
337
+ }
338
+ // Add workspaces first (loads shared knowledge/task/skill graphs)
339
+ for (const [wsId, wsConfig] of mc.workspaces) {
340
+ await manager.addWorkspace(wsId, wsConfig, reindex);
341
+ }
342
+ // Add all projects (workspace projects share knowledge/task/skill graphs)
343
+ for (const [id, config] of mc.projects) {
344
+ await manager.addProject(id, config, reindex, projectWorkspace.get(id));
345
+ }
346
+ // Embedding API model name (loaded in background with other models)
347
+ const embeddingApiModelName = mc.server.embeddingApi?.enabled ? '__server__' : undefined;
348
+ // Start HTTP server immediately (before models are loaded)
349
+ const httpServer = await (0, index_1.startMultiProjectHttpServer)(host, port, sessionTimeoutMs, manager, {
350
+ serverConfig: mc.server,
351
+ users: mc.users,
352
+ embeddingApiModelName,
353
+ });
354
+ // Track open connections for graceful shutdown
355
+ const openSockets = new Set();
356
+ httpServer.on('connection', (socket) => {
357
+ openSockets.add(socket);
358
+ socket.on('close', () => openSockets.delete(socket));
359
+ });
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
+ let shuttingDown = false;
459
+ async function shutdown() {
460
+ if (shuttingDown) {
461
+ process.stderr.write('[serve] Force exit\n');
462
+ process.exit(1);
463
+ }
464
+ shuttingDown = true;
465
+ process.stderr.write('[serve] Shutting down...\n');
466
+ // Force exit after 5s if graceful shutdown hangs
467
+ const forceTimer = setTimeout(() => {
468
+ process.stderr.write('[serve] Shutdown timeout, force exit\n');
469
+ process.exit(1);
470
+ }, 5000);
471
+ try {
472
+ httpServer.close();
473
+ // Destroy all open connections (including WebSocket) so the server can close
474
+ for (const socket of openSockets) {
475
+ socket.destroy();
476
+ }
477
+ openSockets.clear();
478
+ await configWatcher.close();
479
+ await manager.shutdown();
480
+ }
481
+ catch { /* ignore */ }
482
+ clearTimeout(forceTimer);
483
+ // Let event loop drain naturally — avoids ONNX global thread pool destructor crash on macOS
484
+ }
485
+ process.on('SIGINT', () => { void shutdown(); });
486
+ process.on('SIGTERM', () => { void shutdown(); });
487
+ });
488
+ // ---------------------------------------------------------------------------
489
+ // Command: users — manage users in config
490
+ // ---------------------------------------------------------------------------
491
+ const usersCmd = program
492
+ .command('users')
493
+ .description('Manage users in graph-memory.yaml');
494
+ usersCmd
495
+ .command('add')
496
+ .description('Add a new user interactively')
497
+ .option('--config <path>', 'Path to graph-memory.yaml', 'graph-memory.yaml')
498
+ .action(async (opts) => {
499
+ const configPath = path_1.default.resolve(opts.config);
500
+ // Read raw YAML to preserve formatting
501
+ let yamlContent;
502
+ try {
503
+ yamlContent = fs_1.default.readFileSync(configPath, 'utf-8');
504
+ }
505
+ catch {
506
+ process.stderr.write(`[users] Cannot read config: ${configPath}\n`);
507
+ process.exit(1);
508
+ }
509
+ // Validate config loads
510
+ const mc = (0, multi_config_1.loadMultiConfig)(configPath);
511
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stderr });
512
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
513
+ const askHidden = (q) => new Promise(resolve => {
514
+ process.stderr.write(q);
515
+ const stdin = process.stdin;
516
+ const wasRaw = stdin.isRaw;
517
+ if (stdin.isTTY)
518
+ stdin.setRawMode(true);
519
+ let input = '';
520
+ const onData = (ch) => {
521
+ const c = ch.toString();
522
+ if (c === '\n' || c === '\r') {
523
+ if (stdin.isTTY)
524
+ stdin.setRawMode(wasRaw ?? false);
525
+ stdin.removeListener('data', onData);
526
+ process.stderr.write('\n');
527
+ resolve(input);
528
+ }
529
+ else if (c === '\u0003') {
530
+ // Ctrl+C
531
+ if (stdin.isTTY)
532
+ stdin.setRawMode(wasRaw ?? false);
533
+ process.exit(0);
534
+ }
535
+ else if (c === '\u007f' || c === '\b') {
536
+ input = input.slice(0, -1);
537
+ }
538
+ else {
539
+ input += c;
540
+ }
541
+ };
542
+ stdin.on('data', onData);
543
+ });
544
+ try {
545
+ const userId = await ask('User ID (e.g. "prih"): ');
546
+ if (!userId.trim()) {
547
+ process.stderr.write('User ID is required\n');
548
+ process.exit(1);
549
+ }
550
+ const id = userId.trim();
551
+ if (mc.users[id]) {
552
+ process.stderr.write(`[users] User "${id}" already exists in config\n`);
553
+ process.exit(1);
554
+ }
555
+ const name = (await ask('Name: ')).trim();
556
+ if (!name) {
557
+ process.stderr.write('Name is required\n');
558
+ process.exit(1);
559
+ }
560
+ const email = (await ask('Email: ')).trim();
561
+ if (!email) {
562
+ process.stderr.write('Email is required\n');
563
+ process.exit(1);
564
+ }
565
+ const password = await askHidden('Password: ');
566
+ if (!password) {
567
+ process.stderr.write('Password is required\n');
568
+ process.exit(1);
569
+ }
570
+ const password2 = await askHidden('Confirm password: ');
571
+ if (password !== password2) {
572
+ process.stderr.write('Passwords do not match\n');
573
+ process.exit(1);
574
+ }
575
+ const pwHash = await (0, jwt_1.hashPassword)(password);
576
+ const apiKey = `mgm-${crypto_1.default.randomBytes(24).toString('base64url')}`;
577
+ // Build YAML block for the new user
578
+ const userBlock = [
579
+ ` ${id}:`,
580
+ ` name: "${name}"`,
581
+ ` email: "${email}"`,
582
+ ` apiKey: "${apiKey}"`,
583
+ ` passwordHash: "${pwHash}"`,
584
+ ].join('\n');
585
+ // Insert into YAML — find existing `users:` section or add one
586
+ if (yamlContent.includes('\nusers:')) {
587
+ // Append under existing users: section
588
+ yamlContent = yamlContent.replace(/\nusers:\s*\n/, (match) => {
589
+ return match + userBlock + '\n';
590
+ });
591
+ }
592
+ else if (yamlContent.startsWith('users:')) {
593
+ yamlContent = yamlContent.replace(/^users:\s*\n/, (match) => {
594
+ return match + userBlock + '\n';
595
+ });
596
+ }
597
+ else {
598
+ // Add users section at the end
599
+ yamlContent = yamlContent.trimEnd() + '\n\nusers:\n' + userBlock + '\n';
600
+ }
601
+ fs_1.default.writeFileSync(configPath, yamlContent, 'utf-8');
602
+ // Validate the result
603
+ try {
604
+ (0, multi_config_1.loadMultiConfig)(configPath);
605
+ }
606
+ catch (err) {
607
+ process.stderr.write(`[users] Warning: config validation failed after edit: ${err}\n`);
608
+ }
609
+ process.stderr.write(`\nUser "${id}" added successfully.\n`);
610
+ process.stderr.write(` API Key: ${apiKey}\n`);
611
+ process.stderr.write(` (save this key — it cannot be recovered)\n`);
612
+ }
613
+ finally {
614
+ rl.close();
615
+ }
616
+ });
617
+ program.parse();