@prih/mcp-graph-memory 1.0.3

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 (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +512 -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/files.js +64 -0
  7. package/dist/api/rest/graph.js +56 -0
  8. package/dist/api/rest/index.js +117 -0
  9. package/dist/api/rest/knowledge.js +238 -0
  10. package/dist/api/rest/skills.js +284 -0
  11. package/dist/api/rest/tasks.js +272 -0
  12. package/dist/api/rest/tools.js +126 -0
  13. package/dist/api/rest/validation.js +191 -0
  14. package/dist/api/rest/websocket.js +65 -0
  15. package/dist/api/tools/code/get-file-symbols.js +30 -0
  16. package/dist/api/tools/code/get-symbol.js +22 -0
  17. package/dist/api/tools/code/list-files.js +18 -0
  18. package/dist/api/tools/code/search-code.js +27 -0
  19. package/dist/api/tools/code/search-files.js +22 -0
  20. package/dist/api/tools/context/get-context.js +19 -0
  21. package/dist/api/tools/docs/cross-references.js +76 -0
  22. package/dist/api/tools/docs/explain-symbol.js +55 -0
  23. package/dist/api/tools/docs/find-examples.js +52 -0
  24. package/dist/api/tools/docs/get-node.js +24 -0
  25. package/dist/api/tools/docs/get-toc.js +22 -0
  26. package/dist/api/tools/docs/list-snippets.js +46 -0
  27. package/dist/api/tools/docs/list-topics.js +18 -0
  28. package/dist/api/tools/docs/search-files.js +22 -0
  29. package/dist/api/tools/docs/search-snippets.js +43 -0
  30. package/dist/api/tools/docs/search.js +27 -0
  31. package/dist/api/tools/file-index/get-file-info.js +21 -0
  32. package/dist/api/tools/file-index/list-all-files.js +28 -0
  33. package/dist/api/tools/file-index/search-all-files.js +24 -0
  34. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  35. package/dist/api/tools/knowledge/create-note.js +20 -0
  36. package/dist/api/tools/knowledge/create-relation.js +29 -0
  37. package/dist/api/tools/knowledge/delete-note.js +19 -0
  38. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  39. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  40. package/dist/api/tools/knowledge/get-note.js +20 -0
  41. package/dist/api/tools/knowledge/list-notes.js +18 -0
  42. package/dist/api/tools/knowledge/list-relations.js +17 -0
  43. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  44. package/dist/api/tools/knowledge/search-notes.js +25 -0
  45. package/dist/api/tools/knowledge/update-note.js +34 -0
  46. package/dist/api/tools/skills/add-attachment.js +31 -0
  47. package/dist/api/tools/skills/bump-usage.js +19 -0
  48. package/dist/api/tools/skills/create-skill-link.js +25 -0
  49. package/dist/api/tools/skills/create-skill.js +26 -0
  50. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  51. package/dist/api/tools/skills/delete-skill.js +20 -0
  52. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  53. package/dist/api/tools/skills/get-skill.js +21 -0
  54. package/dist/api/tools/skills/link-skill.js +23 -0
  55. package/dist/api/tools/skills/list-skills.js +20 -0
  56. package/dist/api/tools/skills/recall-skills.js +18 -0
  57. package/dist/api/tools/skills/remove-attachment.js +19 -0
  58. package/dist/api/tools/skills/search-skills.js +25 -0
  59. package/dist/api/tools/skills/update-skill.js +58 -0
  60. package/dist/api/tools/tasks/add-attachment.js +31 -0
  61. package/dist/api/tools/tasks/create-task-link.js +25 -0
  62. package/dist/api/tools/tasks/create-task.js +25 -0
  63. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  64. package/dist/api/tools/tasks/delete-task.js +20 -0
  65. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  66. package/dist/api/tools/tasks/get-task.js +20 -0
  67. package/dist/api/tools/tasks/link-task.js +23 -0
  68. package/dist/api/tools/tasks/list-tasks.js +24 -0
  69. package/dist/api/tools/tasks/move-task.js +38 -0
  70. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  71. package/dist/api/tools/tasks/search-tasks.js +25 -0
  72. package/dist/api/tools/tasks/update-task.js +55 -0
  73. package/dist/cli/index.js +451 -0
  74. package/dist/cli/indexer.js +277 -0
  75. package/dist/graphs/attachment-types.js +74 -0
  76. package/dist/graphs/code-types.js +10 -0
  77. package/dist/graphs/code.js +172 -0
  78. package/dist/graphs/docs.js +198 -0
  79. package/dist/graphs/file-index-types.js +10 -0
  80. package/dist/graphs/file-index.js +310 -0
  81. package/dist/graphs/file-lang.js +119 -0
  82. package/dist/graphs/knowledge-types.js +32 -0
  83. package/dist/graphs/knowledge.js +764 -0
  84. package/dist/graphs/manager-types.js +87 -0
  85. package/dist/graphs/skill-types.js +10 -0
  86. package/dist/graphs/skill.js +1013 -0
  87. package/dist/graphs/task-types.js +17 -0
  88. package/dist/graphs/task.js +960 -0
  89. package/dist/lib/embedder.js +101 -0
  90. package/dist/lib/events-log.js +400 -0
  91. package/dist/lib/file-import.js +327 -0
  92. package/dist/lib/file-mirror.js +446 -0
  93. package/dist/lib/frontmatter.js +17 -0
  94. package/dist/lib/mirror-watcher.js +637 -0
  95. package/dist/lib/multi-config.js +254 -0
  96. package/dist/lib/parsers/code.js +246 -0
  97. package/dist/lib/parsers/codeblock.js +66 -0
  98. package/dist/lib/parsers/docs.js +196 -0
  99. package/dist/lib/project-manager.js +418 -0
  100. package/dist/lib/promise-queue.js +22 -0
  101. package/dist/lib/search/bm25.js +167 -0
  102. package/dist/lib/search/code.js +103 -0
  103. package/dist/lib/search/docs.js +108 -0
  104. package/dist/lib/search/file-index.js +31 -0
  105. package/dist/lib/search/files.js +61 -0
  106. package/dist/lib/search/knowledge.js +101 -0
  107. package/dist/lib/search/skills.js +104 -0
  108. package/dist/lib/search/tasks.js +103 -0
  109. package/dist/lib/watcher.js +67 -0
  110. package/package.json +83 -0
  111. package/ui/README.md +54 -0
@@ -0,0 +1,196 @@
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.parseFile = parseFile;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const codeblock_1 = require("../../lib/parsers/codeblock");
10
+ // Parse a markdown file into chunks split by headings
11
+ function parseFile(content, absolutePath, projectDir, chunkDepth) {
12
+ const fileId = path_1.default.relative(projectDir, absolutePath);
13
+ const lines = content.split('\n');
14
+ const rawChunks = [];
15
+ let current = {
16
+ level: 1,
17
+ title: extractFileTitle(content, absolutePath),
18
+ lines: [],
19
+ };
20
+ for (const line of lines) {
21
+ const heading = matchHeading(line, chunkDepth);
22
+ if (heading) {
23
+ rawChunks.push(current);
24
+ current = { level: heading.level, title: heading.title, lines: [line] };
25
+ }
26
+ else {
27
+ current.lines.push(line);
28
+ }
29
+ }
30
+ rawChunks.push(current);
31
+ // Build Chunk objects
32
+ const seenIds = new Set();
33
+ const textChunks = rawChunks
34
+ .filter(c => c.lines.join('\n').trim() || c.level === 1)
35
+ .map(c => {
36
+ const chunkContent = c.lines.join('\n').trim();
37
+ const baseId = c.level === 1 ? fileId : `${fileId}::${c.title}`;
38
+ let id = baseId;
39
+ let counter = 2;
40
+ while (seenIds.has(id))
41
+ id = `${baseId}::${counter++}`;
42
+ seenIds.add(id);
43
+ return {
44
+ id,
45
+ fileId,
46
+ title: c.title,
47
+ content: chunkContent,
48
+ level: c.level,
49
+ links: extractLinks(chunkContent, absolutePath, projectDir),
50
+ embedding: [], // filled later by embedder
51
+ symbols: [],
52
+ };
53
+ });
54
+ return spliceCodeBlocks(textChunks, seenIds);
55
+ }
56
+ // --- code block extraction ---
57
+ const FENCE_RE = /^(`{3,}|~{3,})(\S*)\s*\n([\s\S]*?)^\1\s*$/gm;
58
+ function spliceCodeBlocks(chunks, seenIds) {
59
+ const result = [];
60
+ for (const chunk of chunks) {
61
+ // Skip chunks that are already code blocks (shouldn't happen, but guard)
62
+ if (chunk.language !== undefined) {
63
+ result.push(chunk);
64
+ continue;
65
+ }
66
+ const codeBlocks = [];
67
+ let match;
68
+ FENCE_RE.lastIndex = 0;
69
+ while ((match = FENCE_RE.exec(chunk.content)) !== null) {
70
+ const lang = match[2].toLowerCase();
71
+ const code = match[3].trimEnd();
72
+ if (code)
73
+ codeBlocks.push({ language: lang, code, index: match.index });
74
+ }
75
+ result.push(chunk);
76
+ // Create child chunks for each code block
77
+ let codeIdx = 0;
78
+ for (const cb of codeBlocks) {
79
+ codeIdx++;
80
+ const baseId = `${chunk.id}::code-${codeIdx}`;
81
+ let id = baseId;
82
+ let counter = 2;
83
+ while (seenIds.has(id))
84
+ id = `${baseId}::${counter++}`;
85
+ seenIds.add(id);
86
+ const lang = cb.language || undefined;
87
+ const symbols = lang ? (0, codeblock_1.extractSymbols)(cb.code, lang) : [];
88
+ result.push({
89
+ id,
90
+ fileId: chunk.fileId,
91
+ title: lang || 'code',
92
+ content: cb.code,
93
+ level: chunk.level + 1,
94
+ links: [],
95
+ embedding: [],
96
+ language: lang,
97
+ symbols,
98
+ });
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+ // --- helpers ---
104
+ function matchHeading(line, maxDepth) {
105
+ const match = line.match(/^(#{1,6})\s+(.+)$/);
106
+ if (!match)
107
+ return null;
108
+ const level = match[1].length;
109
+ if (level < 2 || level > maxDepth)
110
+ return null; // # is file title, not a chunk split
111
+ return { level, title: match[2].trim() };
112
+ }
113
+ function extractFileTitle(content, filePath) {
114
+ const match = content.match(/^#\s+(.+)$/m);
115
+ if (match)
116
+ return match[1].trim();
117
+ return path_1.default.basename(filePath, '.md');
118
+ }
119
+ function extractLinks(content, fromFile, projectDir) {
120
+ const results = new Set();
121
+ const fileDir = path_1.default.dirname(fromFile);
122
+ // [text](./path.md)
123
+ const mdLinks = content.matchAll(/\[[^\]]*\]\(([^)#\s]+)/g);
124
+ for (const [, href] of mdLinks) {
125
+ if (isExternal(href))
126
+ continue;
127
+ const resolved = path_1.default.resolve(fileDir, href);
128
+ const fileId = toFileId(resolved, projectDir);
129
+ if (fileId)
130
+ results.add(fileId);
131
+ }
132
+ // [[wiki link]] or [[wiki link|alias]]
133
+ const wikiLinks = content.matchAll(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g);
134
+ for (const [, name] of wikiLinks) {
135
+ const resolved = findWikiFile(name.trim(), projectDir);
136
+ if (!resolved)
137
+ continue;
138
+ const fileId = toFileId(resolved, projectDir); // same guard as md links
139
+ if (fileId)
140
+ results.add(fileId);
141
+ }
142
+ return [...results];
143
+ }
144
+ // Reject anything that looks like an external URL
145
+ function isExternal(href) {
146
+ return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(href) // http://, ftp://, mailto://, etc.
147
+ || href.startsWith('//') // protocol-relative //cdn.example.com
148
+ || href.startsWith('data:') // data URIs
149
+ || href.startsWith('mailto:');
150
+ }
151
+ function toFileId(absolutePath, projectDir) {
152
+ if (!absolutePath.startsWith(projectDir))
153
+ return null;
154
+ const rel = path_1.default.relative(projectDir, absolutePath);
155
+ if (fs_1.default.existsSync(absolutePath))
156
+ return rel;
157
+ // try adding .md
158
+ const withMd = absolutePath + '.md';
159
+ if (fs_1.default.existsSync(withMd))
160
+ return path_1.default.relative(projectDir, withMd);
161
+ return null;
162
+ }
163
+ function findWikiFile(name, projectDir) {
164
+ const direct = path_1.default.join(projectDir, name);
165
+ if (fs_1.default.existsSync(direct))
166
+ return direct;
167
+ const withMd = direct + '.md';
168
+ if (fs_1.default.existsSync(withMd))
169
+ return withMd;
170
+ return searchRecursive(name, projectDir);
171
+ }
172
+ function searchRecursive(name, dir) {
173
+ let entries;
174
+ try {
175
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ for (const entry of entries) {
181
+ if (entry.name.startsWith('.'))
182
+ continue;
183
+ const full = path_1.default.join(dir, entry.name);
184
+ if (entry.isDirectory()) {
185
+ const found = searchRecursive(name, full);
186
+ if (found)
187
+ return found;
188
+ }
189
+ else if (entry.isFile()) {
190
+ if (entry.name === name || entry.name === `${name}.md` || path_1.default.basename(entry.name, '.md') === name) {
191
+ return full;
192
+ }
193
+ }
194
+ }
195
+ return null;
196
+ }
@@ -0,0 +1,418 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProjectManager = void 0;
4
+ const events_1 = require("events");
5
+ const embedder_1 = require("../lib/embedder");
6
+ const docs_1 = require("../graphs/docs");
7
+ const code_1 = require("../graphs/code");
8
+ const knowledge_1 = require("../graphs/knowledge");
9
+ const file_index_1 = require("../graphs/file-index");
10
+ const task_1 = require("../graphs/task");
11
+ const skill_1 = require("../graphs/skill");
12
+ const indexer_1 = require("../cli/indexer");
13
+ const promise_queue_1 = require("../lib/promise-queue");
14
+ const multi_config_1 = require("../lib/multi-config");
15
+ const mirror_watcher_1 = require("../lib/mirror-watcher");
16
+ // ---------------------------------------------------------------------------
17
+ // ProjectManager
18
+ // ---------------------------------------------------------------------------
19
+ class ProjectManager extends events_1.EventEmitter {
20
+ serverConfig;
21
+ projects = new Map();
22
+ workspaces = new Map();
23
+ autoSaveInterval;
24
+ constructor(serverConfig) {
25
+ super();
26
+ this.serverConfig = serverConfig;
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Workspaces
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Add a workspace: load shared knowledge/task/skill graphs.
33
+ * Must be called before addProject for projects that belong to this workspace.
34
+ */
35
+ async addWorkspace(id, config, reindex = false) {
36
+ if (this.workspaces.has(id)) {
37
+ throw new Error(`Workspace "${id}" already exists`);
38
+ }
39
+ const ge = config.graphEmbeddings;
40
+ const knowledgeGraph = (0, knowledge_1.loadKnowledgeGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(ge.knowledge));
41
+ const taskGraph = (0, task_1.loadTaskGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(ge.tasks));
42
+ const skillGraph = (0, skill_1.loadSkillGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(ge.skills));
43
+ const mutationQueue = new promise_queue_1.PromiseQueue();
44
+ const mirrorTracker = new mirror_watcher_1.MirrorWriteTracker();
45
+ const wsInstance = {
46
+ id,
47
+ config,
48
+ knowledgeGraph,
49
+ taskGraph,
50
+ skillGraph,
51
+ mutationQueue,
52
+ mirrorTracker,
53
+ dirty: false,
54
+ };
55
+ const ctx = {
56
+ markDirty: () => { wsInstance.dirty = true; },
57
+ emit: (event, data) => { this.emit(event, data); },
58
+ projectId: id,
59
+ mirrorDir: config.mirrorDir,
60
+ author: (0, multi_config_1.formatAuthor)(config.author),
61
+ };
62
+ // ExternalGraphs for workspace — projectGraphs will be populated as projects are added
63
+ const ext = {
64
+ knowledgeGraph,
65
+ taskGraph,
66
+ skillGraph,
67
+ projectGraphs: new Map(),
68
+ };
69
+ const knowledgeEmbedFns = {
70
+ document: (q) => (0, embedder_1.embed)(q, '', `${id}:knowledge`),
71
+ query: (q) => (0, embedder_1.embedQuery)(q, `${id}:knowledge`),
72
+ };
73
+ const taskEmbedFns = {
74
+ document: (q) => (0, embedder_1.embed)(q, '', `${id}:tasks`),
75
+ query: (q) => (0, embedder_1.embedQuery)(q, `${id}:tasks`),
76
+ };
77
+ const skillEmbedFns = {
78
+ document: (q) => (0, embedder_1.embed)(q, '', `${id}:skills`),
79
+ query: (q) => (0, embedder_1.embedQuery)(q, `${id}:skills`),
80
+ };
81
+ wsInstance.knowledgeManager = new knowledge_1.KnowledgeGraphManager(knowledgeGraph, knowledgeEmbedFns, ctx, ext);
82
+ wsInstance.taskManager = new task_1.TaskGraphManager(taskGraph, taskEmbedFns, ctx, ext);
83
+ wsInstance.skillManager = new skill_1.SkillGraphManager(skillGraph, skillEmbedFns, ctx, ext);
84
+ wsInstance.knowledgeManager.setMirrorTracker(mirrorTracker);
85
+ wsInstance.taskManager.setMirrorTracker(mirrorTracker);
86
+ wsInstance.skillManager.setMirrorTracker(mirrorTracker);
87
+ this.workspaces.set(id, wsInstance);
88
+ process.stderr.write(`[project-manager] Added workspace "${id}"\n`);
89
+ }
90
+ /**
91
+ * Load embedding models for a workspace. Call after addWorkspace.
92
+ */
93
+ async loadWorkspaceModels(id) {
94
+ const ws = this.workspaces.get(id);
95
+ if (!ws)
96
+ throw new Error(`Workspace "${id}" not found`);
97
+ const ge = ws.config.graphEmbeddings;
98
+ await (0, embedder_1.loadModel)(ge.knowledge, this.serverConfig.modelsDir, 2000, `${id}:knowledge`);
99
+ await (0, embedder_1.loadModel)(ge.tasks, this.serverConfig.modelsDir, 2000, `${id}:tasks`);
100
+ await (0, embedder_1.loadModel)(ge.skills, this.serverConfig.modelsDir, 2000, `${id}:skills`);
101
+ }
102
+ /**
103
+ * Start mirror watcher for a workspace. Call after all workspace projects are indexed.
104
+ */
105
+ async startWorkspaceMirror(id) {
106
+ const ws = this.workspaces.get(id);
107
+ if (!ws)
108
+ throw new Error(`Workspace "${id}" not found`);
109
+ const mirrorConfig = {
110
+ projectDir: ws.config.mirrorDir,
111
+ knowledgeManager: ws.knowledgeManager,
112
+ taskManager: ws.taskManager,
113
+ skillManager: ws.skillManager,
114
+ mutationQueue: ws.mutationQueue,
115
+ tracker: ws.mirrorTracker,
116
+ };
117
+ await (0, mirror_watcher_1.scanMirrorDirs)(mirrorConfig);
118
+ ws.mirrorWatcher = (0, mirror_watcher_1.startMirrorWatcher)(mirrorConfig);
119
+ }
120
+ getWorkspace(id) {
121
+ return this.workspaces.get(id);
122
+ }
123
+ listWorkspaces() {
124
+ return Array.from(this.workspaces.keys());
125
+ }
126
+ /** Find which workspace a project belongs to. */
127
+ getProjectWorkspace(projectId) {
128
+ const project = this.projects.get(projectId);
129
+ if (!project?.workspaceId)
130
+ return undefined;
131
+ return this.workspaces.get(project.workspaceId);
132
+ }
133
+ /**
134
+ * Add a project: load graphs, load models, create indexer, start watcher.
135
+ */
136
+ async addProject(id, config, reindex = false, workspaceId) {
137
+ if (this.projects.has(id)) {
138
+ throw new Error(`Project "${id}" already exists`);
139
+ }
140
+ const ws = workspaceId ? this.workspaces.get(workspaceId) : undefined;
141
+ if (workspaceId && !ws)
142
+ throw new Error(`Workspace "${workspaceId}" not found`);
143
+ const ge = config.graphEmbeddings;
144
+ // Load per-project graphs (docs, code, file-index are always per-project)
145
+ const docGraph = config.docsPattern ? (0, docs_1.loadGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(ge.docs)) : undefined;
146
+ const codeGraph = config.codePattern ? (0, code_1.loadCodeGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(ge.code)) : undefined;
147
+ const fileIndexGraph = (0, file_index_1.loadFileIndexGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(ge.files));
148
+ // Knowledge/tasks/skills: shared from workspace or per-project
149
+ const knowledgeGraph = ws ? ws.knowledgeGraph : (0, knowledge_1.loadKnowledgeGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(ge.knowledge));
150
+ const taskGraph = ws ? ws.taskGraph : (0, task_1.loadTaskGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(ge.tasks));
151
+ const skillGraph = ws ? ws.skillGraph : (0, skill_1.loadSkillGraph)(config.graphMemory, reindex, (0, multi_config_1.embeddingFingerprint)(ge.skills));
152
+ // Build embed functions (project-scoped model names)
153
+ const embedFns = this.buildEmbedFns(id);
154
+ const instance = {
155
+ id,
156
+ config,
157
+ docGraph,
158
+ codeGraph,
159
+ knowledgeGraph,
160
+ fileIndexGraph,
161
+ taskGraph,
162
+ skillGraph,
163
+ embedFns,
164
+ mutationQueue: ws ? ws.mutationQueue : new promise_queue_1.PromiseQueue(),
165
+ dirty: false,
166
+ workspaceId,
167
+ };
168
+ // Build graph manager context
169
+ const ctx = {
170
+ markDirty: () => { instance.dirty = true; },
171
+ emit: (event, data) => { this.emit(event, data); },
172
+ projectId: id,
173
+ projectDir: config.projectDir,
174
+ author: (0, multi_config_1.formatAuthor)(config.author),
175
+ };
176
+ const ext = { docGraph, codeGraph, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph };
177
+ // In workspace mode, register this project's graphs for cross-graph resolution
178
+ if (ws) {
179
+ const wsExt = ws.knowledgeManager.externalGraphs;
180
+ if (wsExt?.projectGraphs) {
181
+ wsExt.projectGraphs.set(id, { docGraph, codeGraph, fileIndexGraph });
182
+ }
183
+ }
184
+ instance.docManager = docGraph ? new docs_1.DocGraphManager(docGraph, embedFns.docs, ext) : undefined;
185
+ instance.codeManager = codeGraph ? new code_1.CodeGraphManager(codeGraph, embedFns.code, ext) : undefined;
186
+ instance.fileIndexManager = new file_index_1.FileIndexGraphManager(fileIndexGraph, embedFns.files, ext);
187
+ if (ws) {
188
+ // Use workspace-level shared managers
189
+ instance.knowledgeManager = ws.knowledgeManager;
190
+ instance.taskManager = ws.taskManager;
191
+ instance.skillManager = ws.skillManager;
192
+ instance.mirrorTracker = ws.mirrorTracker;
193
+ }
194
+ else {
195
+ // Per-project managers
196
+ instance.knowledgeManager = new knowledge_1.KnowledgeGraphManager(knowledgeGraph, embedFns.knowledge, ctx, ext);
197
+ instance.taskManager = new task_1.TaskGraphManager(taskGraph, embedFns.tasks, ctx, ext);
198
+ instance.skillManager = new skill_1.SkillGraphManager(skillGraph, embedFns.skills, ctx, ext);
199
+ // Set up mirror write tracker for feedback loop prevention
200
+ const mirrorTracker = new mirror_watcher_1.MirrorWriteTracker();
201
+ instance.mirrorTracker = mirrorTracker;
202
+ instance.knowledgeManager.setMirrorTracker(mirrorTracker);
203
+ instance.taskManager.setMirrorTracker(mirrorTracker);
204
+ instance.skillManager.setMirrorTracker(mirrorTracker);
205
+ }
206
+ this.projects.set(id, instance);
207
+ process.stderr.write(`[project-manager] Added project "${id}" (${config.projectDir})${ws ? ` [workspace: ${workspaceId}]` : ''}\n`);
208
+ }
209
+ /**
210
+ * Load embedding models for a project. Call after addProject.
211
+ * Separated because model loading is slow and server can start before it's done.
212
+ */
213
+ async loadModels(id) {
214
+ const instance = this.projects.get(id);
215
+ if (!instance)
216
+ throw new Error(`Project "${id}" not found`);
217
+ const ge = instance.config.graphEmbeddings;
218
+ // Skip knowledge/tasks/skills models for workspace projects (loaded by workspace)
219
+ const skipGraphs = instance.workspaceId
220
+ ? new Set(['knowledge', 'tasks', 'skills'])
221
+ : new Set();
222
+ for (const gn of multi_config_1.GRAPH_NAMES) {
223
+ if (skipGraphs.has(gn))
224
+ continue;
225
+ await (0, embedder_1.loadModel)(ge[gn], this.serverConfig.modelsDir, instance.config.embedMaxChars, `${id}:${gn}`);
226
+ }
227
+ }
228
+ /**
229
+ * Start indexing + watching for a project. Call after loadModels.
230
+ */
231
+ async startIndexing(id) {
232
+ const instance = this.projects.get(id);
233
+ if (!instance)
234
+ throw new Error(`Project "${id}" not found`);
235
+ const indexer = (0, indexer_1.createProjectIndexer)(instance.docGraph, instance.codeGraph, {
236
+ projectDir: instance.config.projectDir,
237
+ docsPattern: instance.config.docsPattern || undefined,
238
+ codePattern: instance.config.codePattern || undefined,
239
+ excludePattern: instance.config.excludePattern || undefined,
240
+ chunkDepth: instance.config.chunkDepth,
241
+ tsconfig: instance.config.tsconfig,
242
+ docsModelName: `${id}:docs`,
243
+ codeModelName: `${id}:code`,
244
+ filesModelName: `${id}:files`,
245
+ }, instance.knowledgeGraph, instance.fileIndexGraph, instance.taskGraph, instance.skillGraph);
246
+ instance.indexer = indexer;
247
+ instance.watcher = indexer.watch();
248
+ await instance.watcher.whenReady;
249
+ await indexer.drain();
250
+ // Save after initial scan
251
+ this.saveProject(instance);
252
+ instance.dirty = false;
253
+ // Scan and watch .notes/ and .tasks/ for reverse import (skip for workspace projects — handled by workspace)
254
+ if (instance.mirrorTracker && !instance.workspaceId) {
255
+ const mirrorConfig = {
256
+ projectDir: instance.config.projectDir,
257
+ knowledgeManager: instance.knowledgeManager,
258
+ taskManager: instance.taskManager,
259
+ skillManager: instance.skillManager,
260
+ mutationQueue: instance.mutationQueue,
261
+ tracker: instance.mirrorTracker,
262
+ };
263
+ await (0, mirror_watcher_1.scanMirrorDirs)(mirrorConfig);
264
+ instance.mirrorWatcher = (0, mirror_watcher_1.startMirrorWatcher)(mirrorConfig);
265
+ }
266
+ this.emit('project:indexed', { projectId: id });
267
+ process.stderr.write(`[project-manager] Project "${id}" indexed\n`);
268
+ }
269
+ /**
270
+ * Remove a project: drain indexer, save graphs, close watcher.
271
+ */
272
+ async removeProject(id) {
273
+ const instance = this.projects.get(id);
274
+ if (!instance)
275
+ return;
276
+ if (instance.mirrorWatcher)
277
+ await instance.mirrorWatcher.close();
278
+ if (instance.watcher)
279
+ await instance.watcher.close();
280
+ if (instance.indexer)
281
+ await instance.indexer.drain();
282
+ if (instance.mcpClientCleanup)
283
+ await instance.mcpClientCleanup();
284
+ this.saveProject(instance);
285
+ // Clean up workspace shared graphs: remove projectGraphs reference and orphaned proxies
286
+ if (instance.workspaceId) {
287
+ const ws = this.workspaces.get(instance.workspaceId);
288
+ if (ws) {
289
+ ws.knowledgeManager.externalGraphs?.projectGraphs?.delete(id);
290
+ // Remove orphaned proxy nodes that reference this project
291
+ for (const graph of [ws.knowledgeManager.graph, ws.taskManager?.graph, ws.skillManager?.graph]) {
292
+ if (!graph)
293
+ continue;
294
+ const toRemove = [];
295
+ graph.forEachNode((nodeId, attrs) => {
296
+ if (attrs.proxyFor?.projectId === id)
297
+ toRemove.push(nodeId);
298
+ });
299
+ for (const nodeId of toRemove)
300
+ graph.dropNode(nodeId);
301
+ }
302
+ }
303
+ }
304
+ this.projects.delete(id);
305
+ process.stderr.write(`[project-manager] Removed project "${id}"\n`);
306
+ }
307
+ getProject(id) {
308
+ return this.projects.get(id);
309
+ }
310
+ listProjects() {
311
+ return Array.from(this.projects.keys());
312
+ }
313
+ markDirty(id) {
314
+ const instance = this.projects.get(id);
315
+ if (instance)
316
+ instance.dirty = true;
317
+ }
318
+ /**
319
+ * Start auto-save interval (every intervalMs, save dirty projects).
320
+ */
321
+ startAutoSave(intervalMs = 30_000) {
322
+ this.autoSaveInterval = setInterval(() => {
323
+ for (const instance of this.projects.values()) {
324
+ if (instance.dirty) {
325
+ try {
326
+ this.saveProject(instance);
327
+ instance.dirty = false;
328
+ }
329
+ catch (err) {
330
+ process.stderr.write(`[project-manager] Auto-save error for "${instance.id}": ${err}\n`);
331
+ }
332
+ }
333
+ }
334
+ for (const ws of this.workspaces.values()) {
335
+ if (ws.dirty) {
336
+ try {
337
+ this.saveWorkspace(ws);
338
+ ws.dirty = false;
339
+ }
340
+ catch (err) {
341
+ process.stderr.write(`[project-manager] Auto-save error for workspace "${ws.id}": ${err}\n`);
342
+ }
343
+ }
344
+ }
345
+ }, intervalMs);
346
+ this.autoSaveInterval.unref();
347
+ }
348
+ /**
349
+ * Save all projects and shut down.
350
+ */
351
+ async shutdown() {
352
+ if (this.autoSaveInterval)
353
+ clearInterval(this.autoSaveInterval);
354
+ for (const instance of this.projects.values()) {
355
+ try {
356
+ if (instance.mirrorWatcher)
357
+ await instance.mirrorWatcher.close();
358
+ if (instance.watcher)
359
+ await instance.watcher.close();
360
+ if (instance.indexer)
361
+ await instance.indexer.drain();
362
+ if (instance.mcpClientCleanup)
363
+ await instance.mcpClientCleanup();
364
+ this.saveProject(instance);
365
+ }
366
+ catch (err) {
367
+ process.stderr.write(`[project-manager] Shutdown error for "${instance.id}": ${err}\n`);
368
+ }
369
+ }
370
+ for (const ws of this.workspaces.values()) {
371
+ try {
372
+ if (ws.mirrorWatcher)
373
+ await ws.mirrorWatcher.close();
374
+ this.saveWorkspace(ws);
375
+ }
376
+ catch (err) {
377
+ process.stderr.write(`[project-manager] Shutdown error for workspace "${ws.id}": ${err}\n`);
378
+ }
379
+ }
380
+ this.projects.clear();
381
+ this.workspaces.clear();
382
+ process.stderr.write('[project-manager] Shutdown complete\n');
383
+ }
384
+ // ---------------------------------------------------------------------------
385
+ // Private helpers
386
+ // ---------------------------------------------------------------------------
387
+ saveProject(instance) {
388
+ const ge = instance.config.graphEmbeddings;
389
+ if (instance.docGraph)
390
+ (0, docs_1.saveGraph)(instance.docGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.docs));
391
+ if (instance.codeGraph)
392
+ (0, code_1.saveCodeGraph)(instance.codeGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.code));
393
+ (0, file_index_1.saveFileIndexGraph)(instance.fileIndexGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.files));
394
+ // Skip knowledge/tasks/skills for workspace projects (saved by workspace)
395
+ if (!instance.workspaceId) {
396
+ (0, knowledge_1.saveKnowledgeGraph)(instance.knowledgeGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.knowledge));
397
+ (0, task_1.saveTaskGraph)(instance.taskGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.tasks));
398
+ (0, skill_1.saveSkillGraph)(instance.skillGraph, instance.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.skills));
399
+ }
400
+ }
401
+ saveWorkspace(ws) {
402
+ const ge = ws.config.graphEmbeddings;
403
+ (0, knowledge_1.saveKnowledgeGraph)(ws.knowledgeGraph, ws.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.knowledge));
404
+ (0, task_1.saveTaskGraph)(ws.taskGraph, ws.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.tasks));
405
+ (0, skill_1.saveSkillGraph)(ws.skillGraph, ws.config.graphMemory, (0, multi_config_1.embeddingFingerprint)(ge.skills));
406
+ }
407
+ buildEmbedFns(projectId) {
408
+ const pair = (gn) => ({
409
+ document: (q) => (0, embedder_1.embed)(q, '', `${projectId}:${gn}`),
410
+ query: (q) => (0, embedder_1.embedQuery)(q, `${projectId}:${gn}`),
411
+ });
412
+ return {
413
+ docs: pair('docs'), code: pair('code'), knowledge: pair('knowledge'),
414
+ tasks: pair('tasks'), files: pair('files'), skills: pair('skills'),
415
+ };
416
+ }
417
+ }
418
+ 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;