@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.
- package/LICENSE +84 -12
- package/README.md +66 -101
- package/dist/api/index.js +279 -169
- package/dist/api/rest/index.js +36 -16
- package/dist/api/rest/tools.js +8 -1
- package/dist/api/rest/websocket.js +22 -1
- package/dist/api/tools/code/search-code.js +12 -9
- package/dist/api/tools/code/search-files.js +1 -1
- package/dist/api/tools/docs/cross-references.js +3 -2
- package/dist/api/tools/docs/explain-symbol.js +2 -1
- package/dist/api/tools/docs/find-examples.js +2 -1
- package/dist/api/tools/docs/search-files.js +1 -1
- package/dist/api/tools/docs/search-snippets.js +1 -1
- package/dist/api/tools/docs/search.js +5 -4
- package/dist/api/tools/file-index/search-all-files.js +1 -1
- package/dist/api/tools/knowledge/add-attachment.js +14 -3
- package/dist/api/tools/knowledge/create-relation.js +2 -2
- package/dist/api/tools/knowledge/delete-relation.js +2 -2
- package/dist/api/tools/knowledge/find-linked-notes.js +1 -1
- package/dist/api/tools/knowledge/remove-attachment.js +5 -1
- package/dist/api/tools/knowledge/search-notes.js +5 -4
- package/dist/api/tools/skills/add-attachment.js +14 -3
- package/dist/api/tools/skills/recall-skills.js +1 -1
- package/dist/api/tools/skills/remove-attachment.js +5 -1
- package/dist/api/tools/skills/search-skills.js +6 -5
- package/dist/api/tools/tasks/add-attachment.js +14 -3
- package/dist/api/tools/tasks/create-task-link.js +1 -1
- package/dist/api/tools/tasks/delete-task-link.js +1 -1
- package/dist/api/tools/tasks/find-linked-tasks.js +1 -1
- package/dist/api/tools/tasks/remove-attachment.js +5 -1
- package/dist/api/tools/tasks/search-tasks.js +5 -4
- package/dist/cli/index.js +69 -311
- package/dist/cli/indexer.js +61 -29
- package/dist/graphs/code.js +70 -7
- package/dist/graphs/docs.js +15 -2
- package/dist/graphs/file-index.js +20 -6
- package/dist/graphs/file-lang.js +1 -1
- package/dist/graphs/knowledge.js +20 -3
- package/dist/graphs/manager-types.js +1 -1
- package/dist/graphs/skill.js +23 -4
- package/dist/graphs/task.js +23 -4
- package/dist/lib/embedding-codec.js +65 -0
- package/dist/lib/file-mirror.js +7 -7
- package/dist/lib/frontmatter.js +3 -2
- package/dist/lib/jwt.js +4 -4
- package/dist/lib/mirror-watcher.js +5 -4
- package/dist/lib/multi-config.js +60 -1
- package/dist/lib/parsers/code.js +158 -31
- package/dist/lib/parsers/codeblock.js +11 -6
- package/dist/lib/parsers/docs.js +59 -31
- package/dist/lib/parsers/languages/registry.js +10 -4
- package/dist/lib/parsers/languages/typescript.js +195 -48
- package/dist/lib/project-manager.js +14 -10
- package/dist/lib/search/bm25.js +18 -1
- package/dist/lib/search/code.js +12 -3
- package/dist/lib/watcher.js +17 -9
- package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
- package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
- package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
- package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
- package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
- package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
- package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
- package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
- package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
- package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
- package/dist/ui/assets/api-BMnBjMMf.js +1 -0
- package/dist/ui/assets/api-BlFF6gX-.js +1 -0
- package/dist/ui/assets/api-CrGJOcaN.js +1 -0
- package/dist/ui/assets/api-DuX-0a_X.js +1 -0
- package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
- package/dist/ui/assets/client-Bq88u7gN.js +1 -0
- package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
- package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
- package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
- package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
- package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
- package/dist/ui/assets/files-0bPg6NH9.js +1 -0
- package/dist/ui/assets/graph-DXGud_wF.js +1 -0
- package/dist/ui/assets/help-CEMQqZUR.js +891 -0
- package/dist/ui/assets/help-DJ52_fxN.js +1 -0
- package/dist/ui/assets/index-BCZDAYZi.js +2 -0
- package/dist/ui/assets/index-D6zSNtzo.css +1 -0
- package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
- package/dist/ui/assets/new-CpD7hOBA.js +1 -0
- package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
- package/dist/ui/assets/new-s8c0M75X.js +1 -0
- package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
- package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
- package/dist/ui/assets/search-EpJhdP2a.js +1 -0
- package/dist/ui/assets/skill-y9pizyqE.js +1 -0
- package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
- package/dist/ui/assets/tasks-CobouTKV.js +1 -0
- package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
- package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
- package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
- package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
- package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
- package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
- package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
- package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
- package/dist/ui/index.html +11 -3
- package/package.json +2 -2
- 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
|
-
'
|
|
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.
|
|
21
|
+
.version('1.3.0');
|
|
30
22
|
const parseIntArg = (v) => parseInt(v, 10);
|
|
31
23
|
// ---------------------------------------------------------------------------
|
|
32
|
-
// Helper:
|
|
24
|
+
// Helper: load config from file, or fall back to default (cwd as single project)
|
|
33
25
|
// ---------------------------------------------------------------------------
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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 = (
|
|
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 = (
|
|
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
|
-
//
|
|
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: "${
|
|
581
|
-
` email: "${
|
|
338
|
+
` name: "${safeName}"`,
|
|
339
|
+
` email: "${safeEmail}"`,
|
|
582
340
|
` apiKey: "${apiKey}"`,
|
|
583
341
|
` passwordHash: "${pwHash}"`,
|
|
584
342
|
].join('\n');
|
package/dist/cli/indexer.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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 =
|
|
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=${
|
|
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 };
|