@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.
- package/LICENSE +15 -0
- package/README.md +512 -0
- package/dist/api/index.js +473 -0
- package/dist/api/rest/code.js +78 -0
- package/dist/api/rest/docs.js +80 -0
- package/dist/api/rest/files.js +64 -0
- package/dist/api/rest/graph.js +56 -0
- package/dist/api/rest/index.js +117 -0
- package/dist/api/rest/knowledge.js +238 -0
- package/dist/api/rest/skills.js +284 -0
- package/dist/api/rest/tasks.js +272 -0
- package/dist/api/rest/tools.js +126 -0
- package/dist/api/rest/validation.js +191 -0
- package/dist/api/rest/websocket.js +65 -0
- package/dist/api/tools/code/get-file-symbols.js +30 -0
- package/dist/api/tools/code/get-symbol.js +22 -0
- package/dist/api/tools/code/list-files.js +18 -0
- package/dist/api/tools/code/search-code.js +27 -0
- package/dist/api/tools/code/search-files.js +22 -0
- package/dist/api/tools/context/get-context.js +19 -0
- package/dist/api/tools/docs/cross-references.js +76 -0
- package/dist/api/tools/docs/explain-symbol.js +55 -0
- package/dist/api/tools/docs/find-examples.js +52 -0
- package/dist/api/tools/docs/get-node.js +24 -0
- package/dist/api/tools/docs/get-toc.js +22 -0
- package/dist/api/tools/docs/list-snippets.js +46 -0
- package/dist/api/tools/docs/list-topics.js +18 -0
- package/dist/api/tools/docs/search-files.js +22 -0
- package/dist/api/tools/docs/search-snippets.js +43 -0
- package/dist/api/tools/docs/search.js +27 -0
- package/dist/api/tools/file-index/get-file-info.js +21 -0
- package/dist/api/tools/file-index/list-all-files.js +28 -0
- package/dist/api/tools/file-index/search-all-files.js +24 -0
- package/dist/api/tools/knowledge/add-attachment.js +31 -0
- package/dist/api/tools/knowledge/create-note.js +20 -0
- package/dist/api/tools/knowledge/create-relation.js +29 -0
- package/dist/api/tools/knowledge/delete-note.js +19 -0
- package/dist/api/tools/knowledge/delete-relation.js +23 -0
- package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
- package/dist/api/tools/knowledge/get-note.js +20 -0
- package/dist/api/tools/knowledge/list-notes.js +18 -0
- package/dist/api/tools/knowledge/list-relations.js +17 -0
- package/dist/api/tools/knowledge/remove-attachment.js +19 -0
- package/dist/api/tools/knowledge/search-notes.js +25 -0
- package/dist/api/tools/knowledge/update-note.js +34 -0
- package/dist/api/tools/skills/add-attachment.js +31 -0
- package/dist/api/tools/skills/bump-usage.js +19 -0
- package/dist/api/tools/skills/create-skill-link.js +25 -0
- package/dist/api/tools/skills/create-skill.js +26 -0
- package/dist/api/tools/skills/delete-skill-link.js +23 -0
- package/dist/api/tools/skills/delete-skill.js +20 -0
- package/dist/api/tools/skills/find-linked-skills.js +25 -0
- package/dist/api/tools/skills/get-skill.js +21 -0
- package/dist/api/tools/skills/link-skill.js +23 -0
- package/dist/api/tools/skills/list-skills.js +20 -0
- package/dist/api/tools/skills/recall-skills.js +18 -0
- package/dist/api/tools/skills/remove-attachment.js +19 -0
- package/dist/api/tools/skills/search-skills.js +25 -0
- package/dist/api/tools/skills/update-skill.js +58 -0
- package/dist/api/tools/tasks/add-attachment.js +31 -0
- package/dist/api/tools/tasks/create-task-link.js +25 -0
- package/dist/api/tools/tasks/create-task.js +25 -0
- package/dist/api/tools/tasks/delete-task-link.js +23 -0
- package/dist/api/tools/tasks/delete-task.js +20 -0
- package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
- package/dist/api/tools/tasks/get-task.js +20 -0
- package/dist/api/tools/tasks/link-task.js +23 -0
- package/dist/api/tools/tasks/list-tasks.js +24 -0
- package/dist/api/tools/tasks/move-task.js +38 -0
- package/dist/api/tools/tasks/remove-attachment.js +19 -0
- package/dist/api/tools/tasks/search-tasks.js +25 -0
- package/dist/api/tools/tasks/update-task.js +55 -0
- package/dist/cli/index.js +451 -0
- package/dist/cli/indexer.js +277 -0
- package/dist/graphs/attachment-types.js +74 -0
- package/dist/graphs/code-types.js +10 -0
- package/dist/graphs/code.js +172 -0
- package/dist/graphs/docs.js +198 -0
- package/dist/graphs/file-index-types.js +10 -0
- package/dist/graphs/file-index.js +310 -0
- package/dist/graphs/file-lang.js +119 -0
- package/dist/graphs/knowledge-types.js +32 -0
- package/dist/graphs/knowledge.js +764 -0
- package/dist/graphs/manager-types.js +87 -0
- package/dist/graphs/skill-types.js +10 -0
- package/dist/graphs/skill.js +1013 -0
- package/dist/graphs/task-types.js +17 -0
- package/dist/graphs/task.js +960 -0
- package/dist/lib/embedder.js +101 -0
- package/dist/lib/events-log.js +400 -0
- package/dist/lib/file-import.js +327 -0
- package/dist/lib/file-mirror.js +446 -0
- package/dist/lib/frontmatter.js +17 -0
- package/dist/lib/mirror-watcher.js +637 -0
- package/dist/lib/multi-config.js +254 -0
- package/dist/lib/parsers/code.js +246 -0
- package/dist/lib/parsers/codeblock.js +66 -0
- package/dist/lib/parsers/docs.js +196 -0
- package/dist/lib/project-manager.js +418 -0
- package/dist/lib/promise-queue.js +22 -0
- package/dist/lib/search/bm25.js +167 -0
- package/dist/lib/search/code.js +103 -0
- package/dist/lib/search/docs.js +108 -0
- package/dist/lib/search/file-index.js +31 -0
- package/dist/lib/search/files.js +61 -0
- package/dist/lib/search/knowledge.js +101 -0
- package/dist/lib/search/skills.js +104 -0
- package/dist/lib/search/tasks.js +103 -0
- package/dist/lib/watcher.js +67 -0
- package/package.json +83 -0
- 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;
|