@graphmemory/server 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +216 -0
  3. package/dist/api/index.js +473 -0
  4. package/dist/api/rest/code.js +78 -0
  5. package/dist/api/rest/docs.js +80 -0
  6. package/dist/api/rest/embed.js +47 -0
  7. package/dist/api/rest/files.js +64 -0
  8. package/dist/api/rest/graph.js +71 -0
  9. package/dist/api/rest/index.js +371 -0
  10. package/dist/api/rest/knowledge.js +239 -0
  11. package/dist/api/rest/skills.js +285 -0
  12. package/dist/api/rest/tasks.js +273 -0
  13. package/dist/api/rest/tools.js +157 -0
  14. package/dist/api/rest/validation.js +196 -0
  15. package/dist/api/rest/websocket.js +71 -0
  16. package/dist/api/tools/code/get-file-symbols.js +30 -0
  17. package/dist/api/tools/code/get-symbol.js +22 -0
  18. package/dist/api/tools/code/list-files.js +18 -0
  19. package/dist/api/tools/code/search-code.js +27 -0
  20. package/dist/api/tools/code/search-files.js +22 -0
  21. package/dist/api/tools/context/get-context.js +19 -0
  22. package/dist/api/tools/docs/cross-references.js +76 -0
  23. package/dist/api/tools/docs/explain-symbol.js +55 -0
  24. package/dist/api/tools/docs/find-examples.js +52 -0
  25. package/dist/api/tools/docs/get-node.js +24 -0
  26. package/dist/api/tools/docs/get-toc.js +22 -0
  27. package/dist/api/tools/docs/list-snippets.js +46 -0
  28. package/dist/api/tools/docs/list-topics.js +18 -0
  29. package/dist/api/tools/docs/search-files.js +22 -0
  30. package/dist/api/tools/docs/search-snippets.js +43 -0
  31. package/dist/api/tools/docs/search.js +27 -0
  32. package/dist/api/tools/file-index/get-file-info.js +21 -0
  33. package/dist/api/tools/file-index/list-all-files.js +28 -0
  34. package/dist/api/tools/file-index/search-all-files.js +24 -0
  35. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  36. package/dist/api/tools/knowledge/create-note.js +20 -0
  37. package/dist/api/tools/knowledge/create-relation.js +29 -0
  38. package/dist/api/tools/knowledge/delete-note.js +19 -0
  39. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  40. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  41. package/dist/api/tools/knowledge/get-note.js +20 -0
  42. package/dist/api/tools/knowledge/list-notes.js +18 -0
  43. package/dist/api/tools/knowledge/list-relations.js +17 -0
  44. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  45. package/dist/api/tools/knowledge/search-notes.js +25 -0
  46. package/dist/api/tools/knowledge/update-note.js +34 -0
  47. package/dist/api/tools/skills/add-attachment.js +31 -0
  48. package/dist/api/tools/skills/bump-usage.js +19 -0
  49. package/dist/api/tools/skills/create-skill-link.js +25 -0
  50. package/dist/api/tools/skills/create-skill.js +26 -0
  51. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  52. package/dist/api/tools/skills/delete-skill.js +20 -0
  53. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  54. package/dist/api/tools/skills/get-skill.js +21 -0
  55. package/dist/api/tools/skills/link-skill.js +23 -0
  56. package/dist/api/tools/skills/list-skills.js +20 -0
  57. package/dist/api/tools/skills/recall-skills.js +18 -0
  58. package/dist/api/tools/skills/remove-attachment.js +19 -0
  59. package/dist/api/tools/skills/search-skills.js +25 -0
  60. package/dist/api/tools/skills/update-skill.js +58 -0
  61. package/dist/api/tools/tasks/add-attachment.js +31 -0
  62. package/dist/api/tools/tasks/create-task-link.js +25 -0
  63. package/dist/api/tools/tasks/create-task.js +26 -0
  64. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  65. package/dist/api/tools/tasks/delete-task.js +20 -0
  66. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  67. package/dist/api/tools/tasks/get-task.js +20 -0
  68. package/dist/api/tools/tasks/link-task.js +23 -0
  69. package/dist/api/tools/tasks/list-tasks.js +25 -0
  70. package/dist/api/tools/tasks/move-task.js +38 -0
  71. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  72. package/dist/api/tools/tasks/search-tasks.js +25 -0
  73. package/dist/api/tools/tasks/update-task.js +58 -0
  74. package/dist/cli/index.js +617 -0
  75. package/dist/cli/indexer.js +275 -0
  76. package/dist/graphs/attachment-types.js +74 -0
  77. package/dist/graphs/code-types.js +10 -0
  78. package/dist/graphs/code.js +204 -0
  79. package/dist/graphs/docs.js +231 -0
  80. package/dist/graphs/file-index-types.js +10 -0
  81. package/dist/graphs/file-index.js +310 -0
  82. package/dist/graphs/file-lang.js +119 -0
  83. package/dist/graphs/knowledge-types.js +32 -0
  84. package/dist/graphs/knowledge.js +768 -0
  85. package/dist/graphs/manager-types.js +87 -0
  86. package/dist/graphs/skill-types.js +10 -0
  87. package/dist/graphs/skill.js +1016 -0
  88. package/dist/graphs/task-types.js +17 -0
  89. package/dist/graphs/task.js +972 -0
  90. package/dist/lib/access.js +67 -0
  91. package/dist/lib/embedder.js +235 -0
  92. package/dist/lib/events-log.js +401 -0
  93. package/dist/lib/file-import.js +328 -0
  94. package/dist/lib/file-mirror.js +461 -0
  95. package/dist/lib/frontmatter.js +17 -0
  96. package/dist/lib/jwt.js +146 -0
  97. package/dist/lib/mirror-watcher.js +637 -0
  98. package/dist/lib/multi-config.js +393 -0
  99. package/dist/lib/parsers/code.js +214 -0
  100. package/dist/lib/parsers/codeblock.js +33 -0
  101. package/dist/lib/parsers/docs.js +199 -0
  102. package/dist/lib/parsers/languages/index.js +15 -0
  103. package/dist/lib/parsers/languages/registry.js +68 -0
  104. package/dist/lib/parsers/languages/types.js +2 -0
  105. package/dist/lib/parsers/languages/typescript.js +306 -0
  106. package/dist/lib/project-manager.js +458 -0
  107. package/dist/lib/promise-queue.js +22 -0
  108. package/dist/lib/search/bm25.js +167 -0
  109. package/dist/lib/search/code.js +103 -0
  110. package/dist/lib/search/docs.js +106 -0
  111. package/dist/lib/search/file-index.js +31 -0
  112. package/dist/lib/search/files.js +61 -0
  113. package/dist/lib/search/knowledge.js +101 -0
  114. package/dist/lib/search/skills.js +104 -0
  115. package/dist/lib/search/tasks.js +103 -0
  116. package/dist/lib/team.js +89 -0
  117. package/dist/lib/watcher.js +67 -0
  118. package/dist/ui/assets/index-D6oxrVF7.js +1759 -0
  119. package/dist/ui/assets/index-kKd4mVrh.css +1 -0
  120. package/dist/ui/favicon.svg +1 -0
  121. package/dist/ui/icons.svg +24 -0
  122. package/dist/ui/index.html +14 -0
  123. package/package.json +89 -0
@@ -0,0 +1,458 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ProjectManager = void 0;
7
+ const events_1 = require("events");
8
+ const embedder_1 = require("../lib/embedder");
9
+ const docs_1 = require("../graphs/docs");
10
+ const code_1 = require("../graphs/code");
11
+ const knowledge_1 = require("../graphs/knowledge");
12
+ const file_index_1 = require("../graphs/file-index");
13
+ const task_1 = require("../graphs/task");
14
+ const skill_1 = require("../graphs/skill");
15
+ const indexer_1 = require("../cli/indexer");
16
+ const promise_queue_1 = require("../lib/promise-queue");
17
+ const multi_config_1 = require("../lib/multi-config");
18
+ const mirror_watcher_1 = require("../lib/mirror-watcher");
19
+ const team_1 = require("../lib/team");
20
+ const path_1 = __importDefault(require("path"));
21
+ // ---------------------------------------------------------------------------
22
+ // ProjectManager
23
+ // ---------------------------------------------------------------------------
24
+ class ProjectManager extends events_1.EventEmitter {
25
+ serverConfig;
26
+ projects = new Map();
27
+ workspaces = new Map();
28
+ autoSaveInterval;
29
+ constructor(serverConfig) {
30
+ super();
31
+ this.serverConfig = serverConfig;
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Workspaces
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Add a workspace: load shared knowledge/task/skill graphs.
38
+ * Must be called before addProject for projects that belong to this workspace.
39
+ */
40
+ async addWorkspace(id, config, reindex = false) {
41
+ if (this.workspaces.has(id)) {
42
+ throw new Error(`Workspace "${id}" already exists`);
43
+ }
44
+ const gc = config.graphConfigs;
45
+ const knowledgeGraph = (0, knowledge_1.loadKnowledgeGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model));
46
+ const taskGraph = (0, task_1.loadTaskGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model));
47
+ const skillGraph = (0, skill_1.loadSkillGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.skills.model));
48
+ const mutationQueue = new promise_queue_1.PromiseQueue();
49
+ const mirrorTracker = new mirror_watcher_1.MirrorWriteTracker();
50
+ const wsInstance = {
51
+ id,
52
+ config,
53
+ knowledgeGraph,
54
+ taskGraph,
55
+ skillGraph,
56
+ mutationQueue,
57
+ mirrorTracker,
58
+ dirty: false,
59
+ };
60
+ let _authorEnsured = false;
61
+ const ctx = {
62
+ markDirty: () => {
63
+ wsInstance.dirty = true;
64
+ if (!_authorEnsured) {
65
+ _authorEnsured = true;
66
+ (0, team_1.ensureAuthorInTeam)(path_1.default.join(config.mirrorDir, '.team'), config.author);
67
+ }
68
+ },
69
+ emit: (event, data) => { this.emit(event, data); },
70
+ projectId: id,
71
+ mirrorDir: config.mirrorDir,
72
+ author: (0, multi_config_1.formatAuthor)(config.author),
73
+ };
74
+ // ExternalGraphs for workspace — projectGraphs will be populated as projects are added
75
+ const ext = {
76
+ knowledgeGraph,
77
+ taskGraph,
78
+ skillGraph,
79
+ projectGraphs: new Map(),
80
+ };
81
+ const knowledgeEmbedFns = {
82
+ document: (q) => (0, embedder_1.embed)(q, '', `${id}:knowledge`),
83
+ query: (q) => (0, embedder_1.embedQuery)(q, `${id}:knowledge`),
84
+ };
85
+ const taskEmbedFns = {
86
+ document: (q) => (0, embedder_1.embed)(q, '', `${id}:tasks`),
87
+ query: (q) => (0, embedder_1.embedQuery)(q, `${id}:tasks`),
88
+ };
89
+ const skillEmbedFns = {
90
+ document: (q) => (0, embedder_1.embed)(q, '', `${id}:skills`),
91
+ query: (q) => (0, embedder_1.embedQuery)(q, `${id}:skills`),
92
+ };
93
+ wsInstance.knowledgeManager = new knowledge_1.KnowledgeGraphManager(knowledgeGraph, knowledgeEmbedFns, ctx, ext);
94
+ wsInstance.taskManager = new task_1.TaskGraphManager(taskGraph, taskEmbedFns, ctx, ext);
95
+ wsInstance.skillManager = new skill_1.SkillGraphManager(skillGraph, skillEmbedFns, ctx, ext);
96
+ wsInstance.knowledgeManager.setMirrorTracker(mirrorTracker);
97
+ wsInstance.taskManager.setMirrorTracker(mirrorTracker);
98
+ wsInstance.skillManager.setMirrorTracker(mirrorTracker);
99
+ this.workspaces.set(id, wsInstance);
100
+ process.stderr.write(`[project-manager] Added workspace "${id}"\n`);
101
+ }
102
+ /**
103
+ * Load embedding models for a workspace. Call after addWorkspace.
104
+ */
105
+ async loadWorkspaceModels(id) {
106
+ const ws = this.workspaces.get(id);
107
+ if (!ws)
108
+ throw new Error(`Workspace "${id}" not found`);
109
+ const gc = ws.config.graphConfigs;
110
+ await (0, embedder_1.loadModel)(gc.knowledge.model, gc.knowledge.embedding, this.serverConfig.modelsDir, `${id}:knowledge`);
111
+ await (0, embedder_1.loadModel)(gc.tasks.model, gc.tasks.embedding, this.serverConfig.modelsDir, `${id}:tasks`);
112
+ await (0, embedder_1.loadModel)(gc.skills.model, gc.skills.embedding, this.serverConfig.modelsDir, `${id}:skills`);
113
+ }
114
+ /**
115
+ * Start mirror watcher for a workspace. Call after all workspace projects are indexed.
116
+ */
117
+ async startWorkspaceMirror(id) {
118
+ const ws = this.workspaces.get(id);
119
+ if (!ws)
120
+ throw new Error(`Workspace "${id}" not found`);
121
+ const mirrorConfig = {
122
+ projectDir: ws.config.mirrorDir,
123
+ knowledgeManager: ws.knowledgeManager,
124
+ taskManager: ws.taskManager,
125
+ skillManager: ws.skillManager,
126
+ mutationQueue: ws.mutationQueue,
127
+ tracker: ws.mirrorTracker,
128
+ };
129
+ await (0, mirror_watcher_1.scanMirrorDirs)(mirrorConfig);
130
+ ws.mirrorWatcher = (0, mirror_watcher_1.startMirrorWatcher)(mirrorConfig);
131
+ }
132
+ getWorkspace(id) {
133
+ return this.workspaces.get(id);
134
+ }
135
+ listWorkspaces() {
136
+ return Array.from(this.workspaces.keys());
137
+ }
138
+ /** Find which workspace a project belongs to. */
139
+ getProjectWorkspace(projectId) {
140
+ const project = this.projects.get(projectId);
141
+ if (!project?.workspaceId)
142
+ return undefined;
143
+ return this.workspaces.get(project.workspaceId);
144
+ }
145
+ /**
146
+ * Add a project: load graphs, load models, create indexer, start watcher.
147
+ */
148
+ async addProject(id, config, reindex = false, workspaceId) {
149
+ if (this.projects.has(id)) {
150
+ throw new Error(`Project "${id}" already exists`);
151
+ }
152
+ const ws = workspaceId ? this.workspaces.get(workspaceId) : undefined;
153
+ if (workspaceId && !ws)
154
+ throw new Error(`Workspace "${workspaceId}" not found`);
155
+ const gc = config.graphConfigs;
156
+ // Load per-project graphs (gated by enabled flag)
157
+ const docGraph = gc.docs.enabled ? (0, docs_1.loadGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.docs.model)) : undefined;
158
+ const codeGraph = gc.code.enabled ? (0, code_1.loadCodeGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.code.model)) : undefined;
159
+ const fileIndexGraph = gc.files.enabled ? (0, file_index_1.loadFileIndexGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.files.model)) : undefined;
160
+ // Knowledge/tasks/skills: shared from workspace or per-project (gated by enabled)
161
+ const knowledgeGraph = ws ? ws.knowledgeGraph
162
+ : gc.knowledge.enabled ? (0, knowledge_1.loadKnowledgeGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model)) : undefined;
163
+ const taskGraph = ws ? ws.taskGraph
164
+ : gc.tasks.enabled ? (0, task_1.loadTaskGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model)) : undefined;
165
+ const skillGraph = ws ? ws.skillGraph
166
+ : gc.skills.enabled ? (0, skill_1.loadSkillGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(gc.skills.model)) : undefined;
167
+ // Build embed functions (project-scoped model names)
168
+ const embedFns = this.buildEmbedFns(id);
169
+ const instance = {
170
+ id,
171
+ config,
172
+ docGraph,
173
+ codeGraph,
174
+ knowledgeGraph,
175
+ fileIndexGraph,
176
+ taskGraph,
177
+ skillGraph,
178
+ embedFns,
179
+ mutationQueue: ws ? ws.mutationQueue : new promise_queue_1.PromiseQueue(),
180
+ dirty: false,
181
+ workspaceId,
182
+ };
183
+ // Build graph manager context
184
+ let _authorEnsured = false;
185
+ const ctx = {
186
+ markDirty: () => {
187
+ instance.dirty = true;
188
+ if (!_authorEnsured) {
189
+ _authorEnsured = true;
190
+ (0, team_1.ensureAuthorInTeam)(path_1.default.join(config.projectDir, '.team'), config.author);
191
+ }
192
+ },
193
+ emit: (event, data) => { this.emit(event, data); },
194
+ projectId: id,
195
+ projectDir: config.projectDir,
196
+ author: (0, multi_config_1.formatAuthor)(config.author),
197
+ };
198
+ const ext = { docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph };
199
+ // In workspace mode, register this project's graphs for cross-graph resolution
200
+ if (ws) {
201
+ const wsExt = ws.knowledgeManager.externalGraphs;
202
+ if (wsExt?.projectGraphs) {
203
+ wsExt.projectGraphs.set(id, { docGraph, codeGraph, fileIndexGraph });
204
+ }
205
+ }
206
+ instance.docManager = docGraph ? new docs_1.DocGraphManager(docGraph, embedFns.docs, ext) : undefined;
207
+ instance.codeManager = codeGraph ? new code_1.CodeGraphManager(codeGraph, embedFns.code, ext) : undefined;
208
+ instance.fileIndexManager = fileIndexGraph ? new file_index_1.FileIndexGraphManager(fileIndexGraph, embedFns.files, ext) : undefined;
209
+ if (ws) {
210
+ // Use workspace-level shared managers
211
+ instance.knowledgeManager = ws.knowledgeManager;
212
+ instance.taskManager = ws.taskManager;
213
+ instance.skillManager = ws.skillManager;
214
+ instance.mirrorTracker = ws.mirrorTracker;
215
+ }
216
+ else {
217
+ // Per-project managers (only if enabled)
218
+ if (knowledgeGraph) {
219
+ instance.knowledgeManager = new knowledge_1.KnowledgeGraphManager(knowledgeGraph, embedFns.knowledge, ctx, ext);
220
+ }
221
+ if (taskGraph) {
222
+ instance.taskManager = new task_1.TaskGraphManager(taskGraph, embedFns.tasks, ctx, ext);
223
+ }
224
+ if (skillGraph) {
225
+ instance.skillManager = new skill_1.SkillGraphManager(skillGraph, embedFns.skills, ctx, ext);
226
+ }
227
+ // Set up mirror write tracker for feedback loop prevention
228
+ if (instance.knowledgeManager || instance.taskManager || instance.skillManager) {
229
+ const mirrorTracker = new mirror_watcher_1.MirrorWriteTracker();
230
+ instance.mirrorTracker = mirrorTracker;
231
+ instance.knowledgeManager?.setMirrorTracker(mirrorTracker);
232
+ instance.taskManager?.setMirrorTracker(mirrorTracker);
233
+ instance.skillManager?.setMirrorTracker(mirrorTracker);
234
+ }
235
+ }
236
+ this.projects.set(id, instance);
237
+ process.stderr.write(`[project-manager] Added project "${id}" (${config.projectDir})${ws ? ` [workspace: ${workspaceId}]` : ''}\n`);
238
+ }
239
+ /**
240
+ * Load embedding models for a project. Call after addProject.
241
+ * Separated because model loading is slow and server can start before it's done.
242
+ */
243
+ async loadModels(id) {
244
+ const instance = this.projects.get(id);
245
+ if (!instance)
246
+ throw new Error(`Project "${id}" not found`);
247
+ const gc = instance.config.graphConfigs;
248
+ // Skip knowledge/tasks/skills models for workspace projects (loaded by workspace)
249
+ const skipGraphs = instance.workspaceId
250
+ ? new Set(['knowledge', 'tasks', 'skills'])
251
+ : new Set();
252
+ for (const gn of multi_config_1.GRAPH_NAMES) {
253
+ if (skipGraphs.has(gn))
254
+ continue;
255
+ if (!gc[gn].enabled)
256
+ continue;
257
+ await (0, embedder_1.loadModel)(gc[gn].model, gc[gn].embedding, this.serverConfig.modelsDir, `${id}:${gn}`);
258
+ }
259
+ }
260
+ /**
261
+ * Start indexing + watching for a project. Call after loadModels.
262
+ */
263
+ async startIndexing(id) {
264
+ const instance = this.projects.get(id);
265
+ if (!instance)
266
+ throw new Error(`Project "${id}" not found`);
267
+ const gc = instance.config.graphConfigs;
268
+ const indexer = (0, indexer_1.createProjectIndexer)(instance.docGraph, instance.codeGraph, {
269
+ projectId: id,
270
+ projectDir: instance.config.projectDir,
271
+ docsInclude: gc.docs.enabled ? gc.docs.include : undefined,
272
+ docsExclude: gc.docs.exclude,
273
+ codeInclude: gc.code.enabled ? gc.code.include : undefined,
274
+ codeExclude: gc.code.exclude,
275
+ filesExclude: gc.files.exclude,
276
+ chunkDepth: instance.config.chunkDepth,
277
+ maxFileSize: instance.config.maxFileSize,
278
+ docsModelName: `${id}:docs`,
279
+ codeModelName: `${id}:code`,
280
+ filesModelName: `${id}:files`,
281
+ }, instance.knowledgeGraph, instance.fileIndexGraph, instance.taskGraph, instance.skillGraph);
282
+ instance.indexer = indexer;
283
+ instance.watcher = indexer.watch();
284
+ await instance.watcher.whenReady;
285
+ await indexer.drain();
286
+ // Save after initial scan
287
+ this.saveProject(instance);
288
+ instance.dirty = false;
289
+ // Scan and watch .notes/ and .tasks/ for reverse import (skip for workspace projects — handled by workspace)
290
+ if (instance.mirrorTracker && !instance.workspaceId && instance.knowledgeManager && instance.taskManager) {
291
+ const mirrorConfig = {
292
+ projectDir: instance.config.projectDir,
293
+ knowledgeManager: instance.knowledgeManager,
294
+ taskManager: instance.taskManager,
295
+ skillManager: instance.skillManager,
296
+ mutationQueue: instance.mutationQueue,
297
+ tracker: instance.mirrorTracker,
298
+ };
299
+ await (0, mirror_watcher_1.scanMirrorDirs)(mirrorConfig);
300
+ instance.mirrorWatcher = (0, mirror_watcher_1.startMirrorWatcher)(mirrorConfig);
301
+ }
302
+ this.emit('project:indexed', { projectId: id });
303
+ process.stderr.write(`[project-manager] Project "${id}" indexed\n`);
304
+ }
305
+ /**
306
+ * Remove a project: drain indexer, save graphs, close watcher.
307
+ */
308
+ async removeProject(id) {
309
+ const instance = this.projects.get(id);
310
+ if (!instance)
311
+ return;
312
+ if (instance.mirrorWatcher)
313
+ await instance.mirrorWatcher.close();
314
+ if (instance.watcher)
315
+ await instance.watcher.close();
316
+ if (instance.indexer)
317
+ await instance.indexer.drain();
318
+ if (instance.mcpClientCleanup)
319
+ await instance.mcpClientCleanup();
320
+ this.saveProject(instance);
321
+ // Clean up workspace shared graphs: remove projectGraphs reference and orphaned proxies
322
+ if (instance.workspaceId) {
323
+ const ws = this.workspaces.get(instance.workspaceId);
324
+ if (ws) {
325
+ ws.knowledgeManager.externalGraphs?.projectGraphs?.delete(id);
326
+ // Remove orphaned proxy nodes that reference this project
327
+ for (const graph of [ws.knowledgeManager.graph, ws.taskManager?.graph, ws.skillManager?.graph]) {
328
+ if (!graph)
329
+ continue;
330
+ const toRemove = [];
331
+ graph.forEachNode((nodeId, attrs) => {
332
+ if (attrs.proxyFor?.projectId === id)
333
+ toRemove.push(nodeId);
334
+ });
335
+ for (const nodeId of toRemove)
336
+ graph.dropNode(nodeId);
337
+ }
338
+ }
339
+ }
340
+ this.projects.delete(id);
341
+ process.stderr.write(`[project-manager] Removed project "${id}"\n`);
342
+ }
343
+ getProject(id) {
344
+ return this.projects.get(id);
345
+ }
346
+ listProjects() {
347
+ return Array.from(this.projects.keys());
348
+ }
349
+ markDirty(id) {
350
+ const instance = this.projects.get(id);
351
+ if (instance)
352
+ instance.dirty = true;
353
+ }
354
+ /**
355
+ * Start auto-save interval (every intervalMs, save dirty projects).
356
+ */
357
+ startAutoSave(intervalMs = 30_000) {
358
+ this.autoSaveInterval = setInterval(() => {
359
+ for (const instance of this.projects.values()) {
360
+ if (instance.dirty) {
361
+ try {
362
+ this.saveProject(instance);
363
+ instance.dirty = false;
364
+ }
365
+ catch (err) {
366
+ process.stderr.write(`[project-manager] Auto-save error for "${instance.id}": ${err}\n`);
367
+ }
368
+ }
369
+ }
370
+ for (const ws of this.workspaces.values()) {
371
+ if (ws.dirty) {
372
+ try {
373
+ this.saveWorkspace(ws);
374
+ ws.dirty = false;
375
+ }
376
+ catch (err) {
377
+ process.stderr.write(`[project-manager] Auto-save error for workspace "${ws.id}": ${err}\n`);
378
+ }
379
+ }
380
+ }
381
+ }, intervalMs);
382
+ this.autoSaveInterval.unref();
383
+ }
384
+ /**
385
+ * Save all projects and shut down.
386
+ */
387
+ async shutdown() {
388
+ if (this.autoSaveInterval)
389
+ clearInterval(this.autoSaveInterval);
390
+ for (const instance of this.projects.values()) {
391
+ try {
392
+ if (instance.mirrorWatcher)
393
+ await instance.mirrorWatcher.close();
394
+ if (instance.watcher)
395
+ await instance.watcher.close();
396
+ if (instance.indexer)
397
+ await instance.indexer.drain();
398
+ if (instance.mcpClientCleanup)
399
+ await instance.mcpClientCleanup();
400
+ this.saveProject(instance);
401
+ }
402
+ catch (err) {
403
+ process.stderr.write(`[project-manager] Shutdown error for "${instance.id}": ${err}\n`);
404
+ }
405
+ }
406
+ for (const ws of this.workspaces.values()) {
407
+ try {
408
+ if (ws.mirrorWatcher)
409
+ await ws.mirrorWatcher.close();
410
+ this.saveWorkspace(ws);
411
+ }
412
+ catch (err) {
413
+ process.stderr.write(`[project-manager] Shutdown error for workspace "${ws.id}": ${err}\n`);
414
+ }
415
+ }
416
+ this.projects.clear();
417
+ this.workspaces.clear();
418
+ process.stderr.write('[project-manager] Shutdown complete\n');
419
+ }
420
+ // ---------------------------------------------------------------------------
421
+ // Private helpers
422
+ // ---------------------------------------------------------------------------
423
+ saveProject(instance) {
424
+ const gc = instance.config.graphConfigs;
425
+ if (instance.docGraph)
426
+ (0, docs_1.saveGraph)(instance.docGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.docs.model));
427
+ if (instance.codeGraph)
428
+ (0, code_1.saveCodeGraph)(instance.codeGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.code.model));
429
+ if (instance.fileIndexGraph)
430
+ (0, file_index_1.saveFileIndexGraph)(instance.fileIndexGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.files.model));
431
+ // Skip knowledge/tasks/skills for workspace projects (saved by workspace)
432
+ if (!instance.workspaceId) {
433
+ if (instance.knowledgeGraph)
434
+ (0, knowledge_1.saveKnowledgeGraph)(instance.knowledgeGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model));
435
+ if (instance.taskGraph)
436
+ (0, task_1.saveTaskGraph)(instance.taskGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model));
437
+ if (instance.skillGraph)
438
+ (0, skill_1.saveSkillGraph)(instance.skillGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.skills.model));
439
+ }
440
+ }
441
+ saveWorkspace(ws) {
442
+ const gc = ws.config.graphConfigs;
443
+ (0, knowledge_1.saveKnowledgeGraph)(ws.knowledgeGraph, ws.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.knowledge.model));
444
+ (0, task_1.saveTaskGraph)(ws.taskGraph, ws.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.tasks.model));
445
+ (0, skill_1.saveSkillGraph)(ws.skillGraph, ws.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(gc.skills.model));
446
+ }
447
+ buildEmbedFns(projectId) {
448
+ const pair = (gn) => ({
449
+ document: (q) => (0, embedder_1.embed)(q, '', `${projectId}:${gn}`),
450
+ query: (q) => (0, embedder_1.embedQuery)(q, `${projectId}:${gn}`),
451
+ });
452
+ return {
453
+ docs: pair('docs'), code: pair('code'), knowledge: pair('knowledge'),
454
+ tasks: pair('tasks'), files: pair('files'), skills: pair('skills'),
455
+ };
456
+ }
457
+ }
458
+ exports.ProjectManager = ProjectManager;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PromiseQueue = void 0;
4
+ /**
5
+ * A simple serial promise queue.
6
+ * Enqueued functions execute one at a time, in order.
7
+ * If a function rejects, the error propagates to the caller
8
+ * but the queue continues processing subsequent items.
9
+ */
10
+ class PromiseQueue {
11
+ chain = Promise.resolve();
12
+ /**
13
+ * Enqueue an async function. Returns a promise that resolves/rejects
14
+ * with the function's result once it has been executed in turn.
15
+ */
16
+ enqueue(fn) {
17
+ return new Promise((resolve, reject) => {
18
+ this.chain = this.chain.then(fn).then(resolve, reject);
19
+ });
20
+ }
21
+ }
22
+ exports.PromiseQueue = PromiseQueue;
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ /**
3
+ * BM25 keyword search index with incremental updates.
4
+ * Used alongside vector cosine similarity for hybrid search.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.BM25Index = void 0;
8
+ exports.tokenize = tokenize;
9
+ exports.rrfFuse = rrfFuse;
10
+ // ---------------------------------------------------------------------------
11
+ // Tokenizer
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * Tokenize text: split on whitespace/punctuation, split camelCase, lowercase.
15
+ * "getUserById" → ["get", "user", "by", "id"]
16
+ * "JWT tokens" → ["jwt", "tokens"]
17
+ */
18
+ function tokenize(text) {
19
+ if (!text)
20
+ return [];
21
+ // Split camelCase/PascalCase boundaries, then split on non-alphanumeric
22
+ const parts = text
23
+ .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase → camel Case
24
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // XMLParser → XML Parser
25
+ .split(/[^a-zA-Z0-9]+/)
26
+ .map(t => t.toLowerCase())
27
+ .filter(t => t.length > 0);
28
+ return parts;
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // BM25 Index
32
+ // ---------------------------------------------------------------------------
33
+ class BM25Index {
34
+ docs = new Map();
35
+ df = new Map(); // document frequency per term
36
+ totalLength = 0;
37
+ k1;
38
+ b;
39
+ textExtractor;
40
+ constructor(textExtractor, opts) {
41
+ this.textExtractor = textExtractor;
42
+ this.k1 = opts?.k1 ?? 1.2;
43
+ this.b = opts?.b ?? 0.75;
44
+ }
45
+ get size() {
46
+ return this.docs.size;
47
+ }
48
+ hasDocument(id) {
49
+ return this.docs.has(id);
50
+ }
51
+ addDocument(id, attrs) {
52
+ // Remove old version first if exists
53
+ if (this.docs.has(id))
54
+ this.removeDocument(id);
55
+ const text = this.textExtractor(attrs);
56
+ const tokens = tokenize(text);
57
+ const termFreqs = new Map();
58
+ for (const token of tokens) {
59
+ termFreqs.set(token, (termFreqs.get(token) ?? 0) + 1);
60
+ }
61
+ // Update document frequency for each unique term
62
+ for (const term of termFreqs.keys()) {
63
+ this.df.set(term, (this.df.get(term) ?? 0) + 1);
64
+ }
65
+ this.docs.set(id, { termFreqs, length: tokens.length });
66
+ this.totalLength += tokens.length;
67
+ }
68
+ removeDocument(id) {
69
+ const doc = this.docs.get(id);
70
+ if (!doc)
71
+ return;
72
+ // Decrement document frequency for each unique term
73
+ for (const term of doc.termFreqs.keys()) {
74
+ const current = this.df.get(term) ?? 0;
75
+ if (current <= 1) {
76
+ this.df.delete(term);
77
+ }
78
+ else {
79
+ this.df.set(term, current - 1);
80
+ }
81
+ }
82
+ this.totalLength -= doc.length;
83
+ this.docs.delete(id);
84
+ }
85
+ updateDocument(id, attrs) {
86
+ this.removeDocument(id);
87
+ this.addDocument(id, attrs);
88
+ }
89
+ clear() {
90
+ this.docs.clear();
91
+ this.df.clear();
92
+ this.totalLength = 0;
93
+ }
94
+ /**
95
+ * Compute BM25 scores for all documents matching the query.
96
+ * Returns only documents with score > 0 (at least one query term matches).
97
+ */
98
+ score(query) {
99
+ const queryTokens = tokenize(query);
100
+ if (queryTokens.length === 0)
101
+ return new Map();
102
+ const N = this.docs.size;
103
+ if (N === 0)
104
+ return new Map();
105
+ const avgDl = this.totalLength / N;
106
+ const results = new Map();
107
+ for (const [id, doc] of this.docs) {
108
+ let docScore = 0;
109
+ for (const term of queryTokens) {
110
+ const tf = doc.termFreqs.get(term) ?? 0;
111
+ if (tf === 0)
112
+ continue;
113
+ const docFreq = this.df.get(term) ?? 0;
114
+ // IDF: log((N - df + 0.5) / (df + 0.5) + 1)
115
+ const idf = Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1);
116
+ // TF saturation: (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * dl/avgdl))
117
+ const tfNorm = (tf * (this.k1 + 1)) / (tf + this.k1 * (1 - this.b + this.b * doc.length / avgDl));
118
+ docScore += idf * tfNorm;
119
+ }
120
+ if (docScore > 0) {
121
+ results.set(id, docScore);
122
+ }
123
+ }
124
+ return results;
125
+ }
126
+ }
127
+ exports.BM25Index = BM25Index;
128
+ // ---------------------------------------------------------------------------
129
+ // Reciprocal Rank Fusion
130
+ // ---------------------------------------------------------------------------
131
+ /**
132
+ * Fuse two ranked lists using Reciprocal Rank Fusion (RRF).
133
+ * score(d) = 1/(k + rank_vector(d)) + 1/(k + rank_bm25(d))
134
+ *
135
+ * Nodes appearing in only one list get rank = Infinity for the other → only 1/(k+rank) from one source.
136
+ */
137
+ function rrfFuse(vectorScores, bm25Scores, k = 60) {
138
+ // Build ranked lists (sorted desc by score, rank starts at 1)
139
+ const vectorRank = buildRankMap(vectorScores);
140
+ const bm25Rank = buildRankMap(bm25Scores);
141
+ // Collect all unique document IDs
142
+ const allIds = new Set();
143
+ for (const id of vectorScores.keys())
144
+ allIds.add(id);
145
+ for (const id of bm25Scores.keys())
146
+ allIds.add(id);
147
+ const fused = new Map();
148
+ for (const id of allIds) {
149
+ const vRank = vectorRank.get(id);
150
+ const bRank = bm25Rank.get(id);
151
+ let score = 0;
152
+ if (vRank != null)
153
+ score += 1 / (k + vRank);
154
+ if (bRank != null)
155
+ score += 1 / (k + bRank);
156
+ fused.set(id, score);
157
+ }
158
+ return fused;
159
+ }
160
+ function buildRankMap(scores) {
161
+ const sorted = [...scores.entries()].sort((a, b) => b[1] - a[1]);
162
+ const ranks = new Map();
163
+ for (let i = 0; i < sorted.length; i++) {
164
+ ranks.set(sorted[i][0], i + 1); // rank starts at 1
165
+ }
166
+ return ranks;
167
+ }