@graphmemory/server 1.1.0 → 1.2.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/README.md +59 -100
- package/dist/api/index.js +136 -123
- package/dist/api/rest/index.js +1 -1
- package/dist/api/rest/websocket.js +22 -1
- 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/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/cli/index.js +9 -261
- package/dist/cli/indexer.js +1 -1
- package/dist/graphs/file-index.js +3 -3
- package/dist/graphs/manager-types.js +1 -1
- package/dist/lib/file-mirror.js +7 -7
- package/dist/lib/frontmatter.js +3 -2
- package/dist/lib/mirror-watcher.js +5 -4
- package/dist/lib/multi-config.js +54 -0
- package/dist/lib/parsers/languages/registry.js +8 -2
- package/dist/lib/parsers/languages/typescript.js +2 -6
- package/dist/lib/watcher.js +17 -9
- package/dist/ui/assets/{index-D6oxrVF7.js → index-0hRezICt.js} +30 -87
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ function register(server, mgr) {
|
|
|
10
10
|
'Use get_task to fetch full content of a returned task.',
|
|
11
11
|
inputSchema: {
|
|
12
12
|
targetId: zod_1.z.string().describe('Target node ID in the external graph'),
|
|
13
|
-
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge'])
|
|
13
|
+
targetGraph: zod_1.z.enum(['docs', 'code', 'files', 'knowledge', 'skills'])
|
|
14
14
|
.describe('Which graph the target belongs to'),
|
|
15
15
|
kind: zod_1.z.string().optional().describe('Filter by relation kind. If omitted, returns all relations.'),
|
|
16
16
|
projectId: zod_1.z.string().optional().describe('Project ID that the target node belongs to. Defaults to the current project.'),
|
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.2.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;
|
|
@@ -403,58 +204,6 @@ program
|
|
|
403
204
|
initProjects().catch((err) => {
|
|
404
205
|
process.stderr.write(`[serve] Init error: ${err}\n`);
|
|
405
206
|
});
|
|
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
207
|
let shuttingDown = false;
|
|
459
208
|
async function shutdown() {
|
|
460
209
|
if (shuttingDown) {
|
|
@@ -475,7 +224,6 @@ program
|
|
|
475
224
|
socket.destroy();
|
|
476
225
|
}
|
|
477
226
|
openSockets.clear();
|
|
478
|
-
await configWatcher.close();
|
|
479
227
|
await manager.shutdown();
|
|
480
228
|
}
|
|
481
229
|
catch { /* ignore */ }
|
package/dist/cli/indexer.js
CHANGED
|
@@ -226,7 +226,7 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
|
|
|
226
226
|
function scan() {
|
|
227
227
|
function walk(dir) {
|
|
228
228
|
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
229
|
-
if (entry.name.startsWith('.'))
|
|
229
|
+
if (entry.name.startsWith('.') || watcher_1.ALWAYS_IGNORED.has(entry.name))
|
|
230
230
|
continue;
|
|
231
231
|
const full = path_1.default.join(dir, entry.name);
|
|
232
232
|
if (entry.isDirectory()) {
|
|
@@ -121,6 +121,9 @@ function cleanEmptyDirs(graph, dir) {
|
|
|
121
121
|
return;
|
|
122
122
|
if (graph.getNodeAttribute(dir, 'kind') !== 'directory')
|
|
123
123
|
return;
|
|
124
|
+
// Never remove root
|
|
125
|
+
if (dir === '.')
|
|
126
|
+
return;
|
|
124
127
|
// Count outgoing `contains` edges
|
|
125
128
|
const children = graph.outDegree(dir);
|
|
126
129
|
if (children > 0)
|
|
@@ -128,9 +131,6 @@ function cleanEmptyDirs(graph, dir) {
|
|
|
128
131
|
// No children — remove this directory
|
|
129
132
|
const parent = graph.getNodeAttribute(dir, 'directory');
|
|
130
133
|
graph.dropNode(dir);
|
|
131
|
-
// Don't remove root
|
|
132
|
-
if (dir === '.')
|
|
133
|
-
return;
|
|
134
134
|
cleanEmptyDirs(graph, parent);
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
@@ -16,7 +16,7 @@ class VersionConflictError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
exports.VersionConflictError = VersionConflictError;
|
|
19
|
-
/** No-op context for tests
|
|
19
|
+
/** No-op context for tests. */
|
|
20
20
|
function noopContext(projectId = '') {
|
|
21
21
|
return {
|
|
22
22
|
markDirty: () => { },
|
package/dist/lib/file-mirror.js
CHANGED
|
@@ -422,14 +422,14 @@ function deleteMirrorDir(dir, id) {
|
|
|
422
422
|
// ---------------------------------------------------------------------------
|
|
423
423
|
// Attachment file helpers (paths now go through attachments/ subdir)
|
|
424
424
|
// ---------------------------------------------------------------------------
|
|
425
|
-
/** Sanitize a filename: strip
|
|
425
|
+
/** Sanitize a filename: extract basename, strip null bytes and path traversal. */
|
|
426
426
|
function sanitizeFilename(name) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return
|
|
427
|
+
// Normalize backslashes to forward slashes (path.basename on Unix doesn't treat \ as separator)
|
|
428
|
+
const base = path.basename(name.replace(/\0/g, '').replace(/\\/g, '/')).trim();
|
|
429
|
+
// Reject pure traversal names
|
|
430
|
+
if (base === '.' || base === '..')
|
|
431
|
+
return '';
|
|
432
|
+
return base;
|
|
433
433
|
}
|
|
434
434
|
/** Write an attachment file to the entity's attachments/ subdirectory. */
|
|
435
435
|
function writeAttachment(baseDir, entityId, filename, data) {
|
package/dist/lib/frontmatter.js
CHANGED
|
@@ -8,9 +8,10 @@ function serializeMarkdown(frontmatter, body) {
|
|
|
8
8
|
return `---\n${yamlStr}\n---\n\n${body}\n`;
|
|
9
9
|
}
|
|
10
10
|
function parseMarkdown(raw) {
|
|
11
|
-
const
|
|
11
|
+
const normalized = raw.replace(/\r\n/g, '\n');
|
|
12
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/);
|
|
12
13
|
if (!match)
|
|
13
|
-
return { frontmatter: {}, body:
|
|
14
|
+
return { frontmatter: {}, body: normalized };
|
|
14
15
|
const frontmatter = (0, yaml_1.parse)(match[1], { maxAliasCount: 10 }) ?? {};
|
|
15
16
|
const body = match[2].replace(/\n$/, '');
|
|
16
17
|
return { frontmatter, body };
|
|
@@ -51,6 +51,7 @@ const frontmatter_1 = require("./frontmatter");
|
|
|
51
51
|
* this tracker lets us detect our own writes and skip them.
|
|
52
52
|
*/
|
|
53
53
|
class MirrorWriteTracker {
|
|
54
|
+
/** Map from filePath → { mtimeMs (for comparison), recordedAt (for eviction) } */
|
|
54
55
|
recentWrites = new Map();
|
|
55
56
|
static STALE_MS = 10_000; // entries older than 10s are stale
|
|
56
57
|
static MAX_ENTRIES = 10_000;
|
|
@@ -59,7 +60,7 @@ class MirrorWriteTracker {
|
|
|
59
60
|
try {
|
|
60
61
|
const stat = fs.statSync(filePath, { throwIfNoEntry: false });
|
|
61
62
|
if (stat)
|
|
62
|
-
this.recentWrites.set(filePath, stat.mtimeMs);
|
|
63
|
+
this.recentWrites.set(filePath, { mtimeMs: stat.mtimeMs, recordedAt: Date.now() });
|
|
63
64
|
}
|
|
64
65
|
catch { /* ignore */ }
|
|
65
66
|
// Prevent unbounded growth — evict stale entries periodically
|
|
@@ -75,7 +76,7 @@ class MirrorWriteTracker {
|
|
|
75
76
|
const stat = fs.statSync(filePath, { throwIfNoEntry: false });
|
|
76
77
|
if (!stat)
|
|
77
78
|
return false;
|
|
78
|
-
if (Math.abs(stat.mtimeMs - recorded) < 100) {
|
|
79
|
+
if (Math.abs(stat.mtimeMs - recorded.mtimeMs) < 100) {
|
|
79
80
|
this.recentWrites.delete(filePath);
|
|
80
81
|
return true;
|
|
81
82
|
}
|
|
@@ -86,8 +87,8 @@ class MirrorWriteTracker {
|
|
|
86
87
|
}
|
|
87
88
|
evictStale() {
|
|
88
89
|
const now = Date.now();
|
|
89
|
-
for (const [filePath,
|
|
90
|
-
if (now -
|
|
90
|
+
for (const [filePath, entry] of this.recentWrites) {
|
|
91
|
+
if (now - entry.recordedAt > MirrorWriteTracker.STALE_MS)
|
|
91
92
|
this.recentWrites.delete(filePath);
|
|
92
93
|
}
|
|
93
94
|
}
|
package/dist/lib/multi-config.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.GRAPH_NAMES = void 0;
|
|
|
7
7
|
exports.embeddingFingerprint = embeddingFingerprint;
|
|
8
8
|
exports.formatAuthor = formatAuthor;
|
|
9
9
|
exports.loadMultiConfig = loadMultiConfig;
|
|
10
|
+
exports.defaultConfig = defaultConfig;
|
|
10
11
|
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const os_1 = __importDefault(require("os"));
|
|
12
13
|
const path_1 = __importDefault(require("path"));
|
|
@@ -163,6 +164,7 @@ const MODEL_DEFAULTS = {
|
|
|
163
164
|
name: 'Xenova/bge-m3',
|
|
164
165
|
pooling: 'cls',
|
|
165
166
|
normalize: true,
|
|
167
|
+
dtype: 'q8',
|
|
166
168
|
queryPrefix: '',
|
|
167
169
|
documentPrefix: '',
|
|
168
170
|
};
|
|
@@ -391,3 +393,55 @@ function loadMultiConfig(yamlPath) {
|
|
|
391
393
|
}
|
|
392
394
|
return { author: globalAuthor, server, users, projects, workspaces };
|
|
393
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Build a default MultiConfig for a single project rooted at `projectDir`.
|
|
398
|
+
* Used when no config file is found — zero-config startup.
|
|
399
|
+
*/
|
|
400
|
+
function defaultConfig(projectDir) {
|
|
401
|
+
const absDir = path_1.default.resolve(projectDir);
|
|
402
|
+
const id = path_1.default.basename(absDir);
|
|
403
|
+
const server = {
|
|
404
|
+
host: SERVER_DEFAULTS.host,
|
|
405
|
+
port: SERVER_DEFAULTS.port,
|
|
406
|
+
sessionTimeout: SERVER_DEFAULTS.sessionTimeout,
|
|
407
|
+
modelsDir: path_1.default.resolve(SERVER_DEFAULTS.modelsDir),
|
|
408
|
+
model: MODEL_DEFAULTS,
|
|
409
|
+
embedding: EMBEDDING_DEFAULTS,
|
|
410
|
+
defaultAccess: SERVER_DEFAULTS.defaultAccess,
|
|
411
|
+
accessTokenTtl: SERVER_DEFAULTS.accessTokenTtl,
|
|
412
|
+
refreshTokenTtl: SERVER_DEFAULTS.refreshTokenTtl,
|
|
413
|
+
rateLimit: RATE_LIMIT_DEFAULTS,
|
|
414
|
+
maxFileSize: SERVER_DEFAULTS.maxFileSize,
|
|
415
|
+
exclude: [...SERVER_DEFAULTS.exclude],
|
|
416
|
+
};
|
|
417
|
+
const graphConfigs = {};
|
|
418
|
+
for (const gn of exports.GRAPH_NAMES) {
|
|
419
|
+
graphConfigs[gn] = {
|
|
420
|
+
enabled: true,
|
|
421
|
+
include: gn === 'docs' ? PROJECT_DEFAULTS.docsInclude : gn === 'code' ? PROJECT_DEFAULTS.codeInclude : undefined,
|
|
422
|
+
exclude: [...server.exclude],
|
|
423
|
+
model: MODEL_DEFAULTS,
|
|
424
|
+
embedding: EMBEDDING_DEFAULTS,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const project = {
|
|
428
|
+
projectDir: absDir,
|
|
429
|
+
graphMemory: path_1.default.join(absDir, '.graph-memory'),
|
|
430
|
+
exclude: [...server.exclude],
|
|
431
|
+
chunkDepth: PROJECT_DEFAULTS.chunkDepth,
|
|
432
|
+
maxFileSize: server.maxFileSize,
|
|
433
|
+
model: MODEL_DEFAULTS,
|
|
434
|
+
embedding: EMBEDDING_DEFAULTS,
|
|
435
|
+
graphConfigs,
|
|
436
|
+
author: AUTHOR_DEFAULT,
|
|
437
|
+
};
|
|
438
|
+
const projects = new Map();
|
|
439
|
+
projects.set(id, project);
|
|
440
|
+
return {
|
|
441
|
+
author: AUTHOR_DEFAULT,
|
|
442
|
+
server,
|
|
443
|
+
users: {},
|
|
444
|
+
projects,
|
|
445
|
+
workspaces: new Map(),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
@@ -46,6 +46,8 @@ async function loadLanguage(entry) {
|
|
|
46
46
|
function isLanguageSupported(languageName) {
|
|
47
47
|
return languages.has(languageName);
|
|
48
48
|
}
|
|
49
|
+
/** Reusable parser per language (avoids WASM memory leak from creating Parser on every call). */
|
|
50
|
+
const parsers = new Map();
|
|
49
51
|
/** Parse source code with the appropriate language grammar. Returns root node or null. */
|
|
50
52
|
async function parseSource(code, languageName) {
|
|
51
53
|
const entry = languages.get(languageName);
|
|
@@ -53,8 +55,12 @@ async function parseSource(code, languageName) {
|
|
|
53
55
|
return null;
|
|
54
56
|
await initParser();
|
|
55
57
|
const lang = await loadLanguage(entry);
|
|
56
|
-
|
|
57
|
-
parser
|
|
58
|
+
let parser = parsers.get(languageName);
|
|
59
|
+
if (!parser) {
|
|
60
|
+
parser = new _wts.Parser();
|
|
61
|
+
parser.setLanguage(lang);
|
|
62
|
+
parsers.set(languageName, parser);
|
|
63
|
+
}
|
|
58
64
|
const tree = parser.parse(code);
|
|
59
65
|
return tree?.rootNode ?? null;
|
|
60
66
|
}
|
|
@@ -26,12 +26,8 @@ function buildBody(node, docComment) {
|
|
|
26
26
|
}
|
|
27
27
|
return node.text ?? '';
|
|
28
28
|
}
|
|
29
|
-
/** Build a signature
|
|
30
|
-
function buildFullSignature(node,
|
|
31
|
-
if (docComment) {
|
|
32
|
-
const firstDocLine = docComment.split('\n')[0].trim();
|
|
33
|
-
return firstDocLine;
|
|
34
|
-
}
|
|
29
|
+
/** Build a signature: always use the code declaration, not JSDoc. */
|
|
30
|
+
function buildFullSignature(node, _docComment) {
|
|
35
31
|
return buildSignature(node);
|
|
36
32
|
}
|
|
37
33
|
/** Check if a node is inside an export_statement. */
|
package/dist/lib/watcher.js
CHANGED
|
@@ -3,12 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ALWAYS_IGNORED = void 0;
|
|
6
7
|
exports.startWatcher = startWatcher;
|
|
7
8
|
const chokidar_1 = __importDefault(require("chokidar"));
|
|
8
9
|
const micromatch_1 = __importDefault(require("micromatch"));
|
|
9
10
|
const path_1 = __importDefault(require("path"));
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/** Directory basenames that are always excluded from watching and scanning at any nesting level. */
|
|
12
|
+
exports.ALWAYS_IGNORED = new Set([
|
|
13
|
+
'node_modules', '.git', '.hg', '.svn',
|
|
14
|
+
'.next', '.nuxt', '.turbo',
|
|
15
|
+
'dist', 'build',
|
|
16
|
+
'.graph-memory', '.notes', '.tasks', '.skills',
|
|
17
|
+
]);
|
|
12
18
|
// chokidar 5: watch the directory directly — glob patterns don't fire 'add' for existing files
|
|
13
19
|
function startWatcher(dir, handlers, pattern = '**/*.md', excludePatterns) {
|
|
14
20
|
const matches = (filePath) => {
|
|
@@ -17,17 +23,19 @@ function startWatcher(dir, handlers, pattern = '**/*.md', excludePatterns) {
|
|
|
17
23
|
return false;
|
|
18
24
|
return micromatch_1.default.isMatch(rel, pattern);
|
|
19
25
|
};
|
|
20
|
-
// chokidar 5 `ignored` accepts a function — use micromatch for glob exclude patterns
|
|
21
|
-
// plus always skip heavy directories (.git, node_modules, etc.)
|
|
22
|
-
const alwaysIgnoredSet = new Set(ALWAYS_IGNORED.map(d => path_1.default.join(dir, d)));
|
|
23
26
|
const ignored = (filePath) => {
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const basename = path_1.default.basename(filePath);
|
|
28
|
+
// Skip dotfiles and dotdirs (hidden) at any level — except the watched root itself
|
|
29
|
+
if (basename.startsWith('.') && filePath !== dir)
|
|
26
30
|
return true;
|
|
27
|
-
//
|
|
31
|
+
// Always-ignored directories by basename at any nesting level
|
|
32
|
+
if (exports.ALWAYS_IGNORED.has(basename))
|
|
33
|
+
return true;
|
|
34
|
+
// User-defined exclude patterns (glob-based) — only prune directories, not individual files.
|
|
35
|
+
// File-level filtering is handled by matches() + dispatchAdd per-graph excludes.
|
|
28
36
|
if (excludePatterns && excludePatterns.length > 0) {
|
|
29
37
|
const rel = path_1.default.relative(dir, filePath);
|
|
30
|
-
if (micromatch_1.default.isMatch(rel, excludePatterns))
|
|
38
|
+
if (micromatch_1.default.isMatch(rel + '/x', excludePatterns))
|
|
31
39
|
return true;
|
|
32
40
|
}
|
|
33
41
|
return false;
|