@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.
@@ -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.1.0');
21
+ .version('1.2.0');
30
22
  const parseIntArg = (v) => parseInt(v, 10);
31
23
  // ---------------------------------------------------------------------------
32
- // Helper: resolve a single project from YAML config + --project flag
24
+ // Helper: load config from file, or fall back to default (cwd as single project)
33
25
  // ---------------------------------------------------------------------------
34
- function resolveProject(configPath, projectId) {
35
- const mc = (0, multi_config_1.loadMultiConfig)(configPath);
36
- const ids = Array.from(mc.projects.keys());
37
- if (ids.length === 0) {
38
- process.stderr.write('[cli] No projects defined in config\n');
39
- process.exit(1);
40
- }
41
- const id = projectId ?? ids[0];
42
- const project = mc.projects.get(id);
43
- if (!project) {
44
- process.stderr.write(`[cli] Project "${id}" not found in config. Available: ${ids.join(', ')}\n`);
45
- process.exit(1);
26
+ function loadConfigOrDefault(configPath) {
27
+ if (fs_1.default.existsSync(configPath)) {
28
+ return (0, multi_config_1.loadMultiConfig)(configPath);
46
29
  }
47
- return { id, project, server: mc.server };
48
- }
49
- async function loadAllModels(projectId, config, modelsDir) {
50
- for (const gn of multi_config_1.GRAPH_NAMES) {
51
- if (!config.graphConfigs[gn].enabled)
52
- continue;
53
- await (0, embedder_1.loadModel)(config.graphConfigs[gn].model, config.graphConfigs[gn].embedding, modelsDir, `${projectId}:${gn}`);
54
- }
55
- }
56
- function buildEmbedFns(projectId) {
57
- const pair = (gn) => ({
58
- document: (q) => (0, embedder_1.embed)(q, '', `${projectId}:${gn}`),
59
- query: (q) => (0, embedder_1.embedQuery)(q, `${projectId}:${gn}`),
60
- });
61
- return {
62
- docs: pair('docs'), code: pair('code'), knowledge: pair('knowledge'),
63
- tasks: pair('tasks'), files: pair('files'), skills: pair('skills'),
64
- };
30
+ process.stderr.write(`[cli] Config "${configPath}" not found, using current directory as project\n`);
31
+ return (0, multi_config_1.defaultConfig)(process.cwd());
65
32
  }
66
33
  // ---------------------------------------------------------------------------
67
34
  // Command: index — scan one project and exit
@@ -74,7 +41,7 @@ program
74
41
  .option('--reindex', 'Discard persisted graphs and re-index from scratch')
75
42
  .action((opts) => {
76
43
  (async () => {
77
- const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
44
+ const mc = loadConfigOrDefault(opts.config);
78
45
  const reindex = !!opts.reindex;
79
46
  if (reindex)
80
47
  process.stderr.write('[index] Re-indexing from scratch\n');
@@ -139,172 +106,6 @@ program
139
106
  });
140
107
  });
141
108
  // ---------------------------------------------------------------------------
142
- // Command: mcp — single-project stdio mode
143
- // ---------------------------------------------------------------------------
144
- program
145
- .command('mcp')
146
- .description('Index one project (or workspace), keep watching for changes, and start MCP server on stdio')
147
- .option('--config <path>', 'Path to graph-memory.yaml', 'graph-memory.yaml')
148
- .option('--project <id>', 'Project ID (defaults to first project)')
149
- .option('--workspace <id>', 'Workspace ID (loads all workspace projects with shared graphs)')
150
- .option('--reindex', 'Discard persisted graphs and re-index from scratch')
151
- .action(async (opts) => {
152
- // Workspace mode: load all projects in the workspace with shared graphs
153
- if (opts.workspace) {
154
- const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
155
- const wsConfig = mc.workspaces.get(opts.workspace);
156
- if (!wsConfig) {
157
- process.stderr.write(`[mcp] Workspace "${opts.workspace}" not found in config. Available: ${Array.from(mc.workspaces.keys()).join(', ')}\n`);
158
- process.exit(1);
159
- }
160
- const fresh = !!opts.reindex;
161
- if (fresh)
162
- process.stderr.write(`[mcp] Re-indexing workspace "${opts.workspace}" from scratch\n`);
163
- const manager = new project_manager_1.ProjectManager(mc.server);
164
- await manager.addWorkspace(opts.workspace, wsConfig, fresh);
165
- for (const projId of wsConfig.projects) {
166
- const projConfig = mc.projects.get(projId);
167
- if (!projConfig) {
168
- process.stderr.write(`[mcp] Project "${projId}" referenced by workspace not found\n`);
169
- process.exit(1);
170
- }
171
- await manager.addProject(projId, projConfig, fresh, opts.workspace);
172
- }
173
- // Use specified project (or first) for stdio server
174
- const targetId = opts.project ?? wsConfig.projects[0];
175
- if (!wsConfig.projects.includes(targetId)) {
176
- process.stderr.write(`[mcp] Project "${targetId}" is not part of workspace "${opts.workspace}"\n`);
177
- process.exit(1);
178
- }
179
- const instance = manager.getProject(targetId);
180
- const sessionCtx = {
181
- projectId: targetId,
182
- workspaceId: opts.workspace,
183
- workspaceProjects: wsConfig.projects,
184
- };
185
- await (0, index_1.startStdioServer)(instance.docGraph, instance.codeGraph, instance.knowledgeGraph, instance.fileIndexGraph, instance.taskGraph, instance.embedFns, instance.config.projectDir, instance.skillGraph, sessionCtx);
186
- // Load models and index in background
187
- (async () => {
188
- await manager.loadWorkspaceModels(opts.workspace);
189
- for (const projId of wsConfig.projects) {
190
- await manager.loadModels(projId);
191
- await manager.startIndexing(projId);
192
- }
193
- await manager.startWorkspaceMirror(opts.workspace);
194
- process.stderr.write(`[mcp] Workspace "${opts.workspace}" fully indexed\n`);
195
- })().catch((err) => {
196
- process.stderr.write(`[mcp] Workspace indexer error: ${err}\n`);
197
- });
198
- let shuttingDown = false;
199
- async function shutdown() {
200
- if (shuttingDown) {
201
- process.stderr.write('[mcp] Force exit\n');
202
- process.exit(1);
203
- }
204
- shuttingDown = true;
205
- process.stderr.write('[mcp] Shutting down...\n');
206
- const forceTimer = setTimeout(() => { process.stderr.write('[mcp] Shutdown timeout, force exit\n'); process.exit(1); }, 5000);
207
- try {
208
- await manager.shutdown();
209
- }
210
- catch { /* ignore */ }
211
- clearTimeout(forceTimer);
212
- // Let event loop drain naturally — avoids ONNX global thread pool destructor crash on macOS
213
- }
214
- process.on('SIGINT', () => { void shutdown(); });
215
- process.on('SIGTERM', () => { void shutdown(); });
216
- return;
217
- }
218
- const { id, project, server } = resolveProject(opts.config, opts.project);
219
- const projectDir = path_1.default.resolve(project.projectDir);
220
- const fresh = !!opts.reindex;
221
- if (fresh)
222
- process.stderr.write(`[mcp] Re-indexing project "${id}" from scratch\n`);
223
- const gc = project.graphConfigs;
224
- // Load persisted graphs (or create fresh ones if reindexing / model changed) and start MCP server immediately
225
- const docGraph = gc.docs.enabled ? (0, docs_1.loadGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.docs.model)) : undefined;
226
- const codeGraph = gc.code.enabled ? (0, code_1.loadCodeGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.code.model)) : undefined;
227
- const knowledgeGraph = gc.knowledge.enabled ? (0, knowledge_1.loadKnowledgeGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model)) : undefined;
228
- const fileIndexGraph = gc.files.enabled ? (0, file_index_1.loadFileIndexGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.files.model)) : undefined;
229
- const taskGraph = gc.tasks.enabled ? (0, task_1.loadTaskGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model)) : undefined;
230
- const skillGraph = gc.skills.enabled ? (0, skill_1.loadSkillGraph)(project.graphMemory, fresh, (0, multi_config_1.embeddingFingerprint)(gc.skills.model)) : undefined;
231
- const embedFns = buildEmbedFns(id);
232
- const sessionCtx = { projectId: id };
233
- await (0, index_1.startStdioServer)(docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, embedFns, project.projectDir, skillGraph, sessionCtx);
234
- // Load models and start watcher in the background
235
- let watcher;
236
- let indexer;
237
- async function startIndexing() {
238
- await loadAllModels(id, project, server.modelsDir);
239
- indexer = (0, indexer_1.createProjectIndexer)(docGraph, codeGraph, {
240
- projectId: id,
241
- projectDir,
242
- docsInclude: gc.docs.enabled ? gc.docs.include : undefined,
243
- docsExclude: gc.docs.exclude,
244
- codeInclude: gc.code.enabled ? gc.code.include : undefined,
245
- codeExclude: gc.code.exclude,
246
- filesExclude: gc.files.exclude,
247
- chunkDepth: project.chunkDepth,
248
- maxFileSize: project.maxFileSize,
249
- docsModelName: `${id}:docs`,
250
- codeModelName: `${id}:code`,
251
- filesModelName: `${id}:files`,
252
- }, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph);
253
- watcher = indexer.watch();
254
- await watcher.whenReady;
255
- await indexer.drain();
256
- if (docGraph) {
257
- (0, docs_1.saveGraph)(docGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.docs.model));
258
- process.stderr.write(`[mcp] Docs indexed. ${docGraph.order} nodes, ${docGraph.size} edges.\n`);
259
- }
260
- if (codeGraph) {
261
- (0, code_1.saveCodeGraph)(codeGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.code.model));
262
- process.stderr.write(`[mcp] Code indexed. ${codeGraph.order} nodes, ${codeGraph.size} edges.\n`);
263
- }
264
- if (fileIndexGraph) {
265
- (0, file_index_1.saveFileIndexGraph)(fileIndexGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.files.model));
266
- process.stderr.write(`[mcp] File index done. ${fileIndexGraph.order} nodes, ${fileIndexGraph.size} edges.\n`);
267
- }
268
- }
269
- startIndexing().catch((err) => {
270
- process.stderr.write(`[mcp] Indexer error: ${err}\n`);
271
- });
272
- let shuttingDown = false;
273
- async function shutdown() {
274
- if (shuttingDown) {
275
- process.stderr.write('[mcp] Force exit\n');
276
- process.exit(1);
277
- }
278
- shuttingDown = true;
279
- process.stderr.write('[mcp] Shutting down...\n');
280
- const forceTimer = setTimeout(() => {
281
- process.stderr.write('[mcp] Shutdown timeout, force exit\n');
282
- process.exit(1);
283
- }, 5000);
284
- try {
285
- if (watcher)
286
- await watcher.close();
287
- if (indexer)
288
- await indexer.drain();
289
- if (docGraph)
290
- (0, docs_1.saveGraph)(docGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.docs.model));
291
- if (codeGraph)
292
- (0, code_1.saveCodeGraph)(codeGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.code.model));
293
- if (knowledgeGraph)
294
- (0, knowledge_1.saveKnowledgeGraph)(knowledgeGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model));
295
- if (fileIndexGraph)
296
- (0, file_index_1.saveFileIndexGraph)(fileIndexGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.files.model));
297
- if (taskGraph)
298
- (0, task_1.saveTaskGraph)(taskGraph, project.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model));
299
- }
300
- catch { /* ignore */ }
301
- clearTimeout(forceTimer);
302
- // Let event loop drain naturally — avoids ONNX global thread pool destructor crash on macOS
303
- }
304
- process.on('SIGINT', () => { void shutdown(); });
305
- process.on('SIGTERM', () => { void shutdown(); });
306
- });
307
- // ---------------------------------------------------------------------------
308
109
  // Command: serve — multi-project HTTP mode
309
110
  // ---------------------------------------------------------------------------
310
111
  program
@@ -315,7 +116,7 @@ program
315
116
  .option('--port <n>', 'HTTP server port', parseIntArg)
316
117
  .option('--reindex', 'Discard persisted graphs and re-index from scratch')
317
118
  .action(async (opts) => {
318
- const mc = (0, multi_config_1.loadMultiConfig)(opts.config);
119
+ const mc = loadConfigOrDefault(opts.config);
319
120
  const host = opts.host ?? mc.server.host;
320
121
  const port = opts.port ?? mc.server.port;
321
122
  const sessionTimeoutMs = mc.server.sessionTimeout * 1000;
@@ -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 */ }
@@ -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 and single-project stdio mode. */
19
+ /** No-op context for tests. */
20
20
  function noopContext(projectId = '') {
21
21
  return {
22
22
  markDirty: () => { },
@@ -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 path separators, .., and null bytes. */
425
+ /** Sanitize a filename: extract basename, strip null bytes and path traversal. */
426
426
  function sanitizeFilename(name) {
427
- const sanitized = name
428
- .replace(/\0/g, '')
429
- .replace(/\.\./g, '')
430
- .replace(/[/\\]/g, '')
431
- .trim();
432
- return sanitized; // empty string is a valid return — callers must check
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) {
@@ -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 match = raw.match(/^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/);
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: raw };
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, mtime] of this.recentWrites) {
90
- if (now - mtime > MirrorWriteTracker.STALE_MS)
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
  }
@@ -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
- const parser = new _wts.Parser();
57
- parser.setLanguage(lang);
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 that includes the JSDoc first line if present. */
30
- function buildFullSignature(node, docComment) {
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. */
@@ -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
- /** Directories that are always excluded from watching (heavy, never useful). */
11
- const ALWAYS_IGNORED = ['.git', 'node_modules', '.next', '.nuxt', '.turbo', 'dist', 'build', '.graph-memory', '.notes', '.tasks', '.skills'];
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
- // Always ignore heavy directories by exact basename match
25
- if (alwaysIgnoredSet.has(filePath))
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
- // User-defined exclude patterns (glob-based)
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;