@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,277 @@
|
|
|
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.createProjectIndexer = createProjectIndexer;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const micromatch_1 = __importDefault(require("micromatch"));
|
|
10
|
+
const embedder_1 = require("../lib/embedder");
|
|
11
|
+
const docs_1 = require("../lib/parsers/docs");
|
|
12
|
+
const docs_2 = require("../graphs/docs");
|
|
13
|
+
const code_1 = require("../lib/parsers/code");
|
|
14
|
+
const code_2 = require("../graphs/code");
|
|
15
|
+
const watcher_1 = require("../lib/watcher");
|
|
16
|
+
const knowledge_1 = require("../graphs/knowledge");
|
|
17
|
+
const task_1 = require("../graphs/task");
|
|
18
|
+
const skill_1 = require("../graphs/skill");
|
|
19
|
+
const file_index_1 = require("../graphs/file-index");
|
|
20
|
+
function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileIndexGraph, taskGraph, skillGraph) {
|
|
21
|
+
// Three independent serial queues — docs, code, and file index.
|
|
22
|
+
let docsQueue = Promise.resolve();
|
|
23
|
+
let codeQueue = Promise.resolve();
|
|
24
|
+
let fileQueue = Promise.resolve();
|
|
25
|
+
// Error tracking
|
|
26
|
+
let docErrors = 0;
|
|
27
|
+
let codeErrors = 0;
|
|
28
|
+
let fileErrors = 0;
|
|
29
|
+
let project = codeGraph && config.codePattern
|
|
30
|
+
? (0, code_1.getProject)(config.projectDir, config.tsconfig)
|
|
31
|
+
: undefined;
|
|
32
|
+
// Resolve the tsconfig path the indexer watches for changes
|
|
33
|
+
const tsconfigPath = config.tsconfig ?? path_1.default.join(config.projectDir, 'tsconfig.json');
|
|
34
|
+
function enqueueDoc(fn) {
|
|
35
|
+
docsQueue = docsQueue.then(fn).catch((err) => {
|
|
36
|
+
docErrors++;
|
|
37
|
+
process.stderr.write(`[indexer] Doc error: ${err}\n`);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function enqueueCode(fn) {
|
|
41
|
+
codeQueue = codeQueue.then(fn).catch((err) => {
|
|
42
|
+
codeErrors++;
|
|
43
|
+
process.stderr.write(`[indexer] Code error: ${err}\n`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function enqueueFile(fn) {
|
|
47
|
+
fileQueue = fileQueue.then(fn).catch((err) => {
|
|
48
|
+
fileErrors++;
|
|
49
|
+
process.stderr.write(`[indexer] File index error: ${err}\n`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Per-file indexing
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
async function indexDocFile(absolutePath) {
|
|
56
|
+
if (!docGraph)
|
|
57
|
+
return;
|
|
58
|
+
let stat;
|
|
59
|
+
try {
|
|
60
|
+
stat = fs_1.default.statSync(absolutePath);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// File disappeared — remove stale node from graph if present
|
|
64
|
+
const fileId = path_1.default.relative(config.projectDir, absolutePath);
|
|
65
|
+
if (docGraph.hasNode(fileId)) {
|
|
66
|
+
(0, docs_2.removeFile)(docGraph, fileId);
|
|
67
|
+
if (knowledgeGraph)
|
|
68
|
+
(0, knowledge_1.cleanupProxies)(knowledgeGraph, 'docs', docGraph);
|
|
69
|
+
if (taskGraph)
|
|
70
|
+
(0, task_1.cleanupProxies)(taskGraph, 'docs', docGraph);
|
|
71
|
+
if (skillGraph)
|
|
72
|
+
(0, skill_1.cleanupProxies)(skillGraph, 'docs', docGraph);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const mtime = stat.mtimeMs;
|
|
77
|
+
const fileId = path_1.default.relative(config.projectDir, absolutePath);
|
|
78
|
+
if ((0, docs_2.getFileMtime)(docGraph, fileId) === mtime)
|
|
79
|
+
return;
|
|
80
|
+
const content = fs_1.default.readFileSync(absolutePath, 'utf-8');
|
|
81
|
+
const chunks = (0, docs_1.parseFile)(content, absolutePath, config.projectDir, config.chunkDepth);
|
|
82
|
+
// Batch-embed all chunks + file-level in one forward pass
|
|
83
|
+
const batchInputs = chunks.map(c => ({ title: c.title, content: c.content }));
|
|
84
|
+
const rootChunk = chunks.find(c => c.level === 1);
|
|
85
|
+
const embedText = rootChunk?.title
|
|
86
|
+
? `${fileId} ${rootChunk.title}`
|
|
87
|
+
: `${fileId} ${rootChunk?.content.slice(0, 200) ?? ''}`;
|
|
88
|
+
batchInputs.push({ title: embedText, content: '' });
|
|
89
|
+
const embeddings = await (0, embedder_1.embedBatch)(batchInputs, config.docsModelName);
|
|
90
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
91
|
+
chunks[i].embedding = embeddings[i];
|
|
92
|
+
}
|
|
93
|
+
(0, docs_2.updateFile)(docGraph, chunks, mtime);
|
|
94
|
+
if (rootChunk && docGraph.hasNode(rootChunk.id)) {
|
|
95
|
+
docGraph.setNodeAttribute(rootChunk.id, 'fileEmbedding', embeddings[chunks.length]);
|
|
96
|
+
}
|
|
97
|
+
process.stderr.write(`[indexer] doc ${fileId} (${chunks.length} chunks)\n`);
|
|
98
|
+
}
|
|
99
|
+
async function indexCodeFile(absolutePath) {
|
|
100
|
+
if (!codeGraph || !project)
|
|
101
|
+
return;
|
|
102
|
+
let stat;
|
|
103
|
+
try {
|
|
104
|
+
stat = fs_1.default.statSync(absolutePath);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
const fileId = path_1.default.relative(config.projectDir, absolutePath);
|
|
108
|
+
if (codeGraph.hasNode(fileId)) {
|
|
109
|
+
(0, code_2.removeCodeFile)(codeGraph, fileId);
|
|
110
|
+
if (knowledgeGraph)
|
|
111
|
+
(0, knowledge_1.cleanupProxies)(knowledgeGraph, 'code', codeGraph);
|
|
112
|
+
if (taskGraph)
|
|
113
|
+
(0, task_1.cleanupProxies)(taskGraph, 'code', codeGraph);
|
|
114
|
+
if (skillGraph)
|
|
115
|
+
(0, skill_1.cleanupProxies)(skillGraph, 'code', codeGraph);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const mtime = stat.mtimeMs;
|
|
120
|
+
const fileId = path_1.default.relative(config.projectDir, absolutePath);
|
|
121
|
+
if ((0, code_2.getCodeFileMtime)(codeGraph, fileId) === mtime)
|
|
122
|
+
return;
|
|
123
|
+
const parsed = (0, code_1.parseCodeFile)(absolutePath, config.projectDir, mtime, project);
|
|
124
|
+
// Batch-embed all symbols + file-level in one forward pass
|
|
125
|
+
const batchInputs = parsed.nodes.map(({ attrs }) => ({ title: attrs.signature, content: attrs.docComment }));
|
|
126
|
+
batchInputs.push({ title: fileId, content: '' });
|
|
127
|
+
const embeddings = await (0, embedder_1.embedBatch)(batchInputs, config.codeModelName);
|
|
128
|
+
for (let i = 0; i < parsed.nodes.length; i++) {
|
|
129
|
+
parsed.nodes[i].attrs.embedding = embeddings[i];
|
|
130
|
+
}
|
|
131
|
+
(0, code_2.updateCodeFile)(codeGraph, parsed);
|
|
132
|
+
if (codeGraph.hasNode(fileId)) {
|
|
133
|
+
codeGraph.setNodeAttribute(fileId, 'fileEmbedding', embeddings[parsed.nodes.length]);
|
|
134
|
+
}
|
|
135
|
+
process.stderr.write(`[indexer] code ${fileId} (${parsed.nodes.length} symbols)\n`);
|
|
136
|
+
}
|
|
137
|
+
async function indexFileEntry(absolutePath) {
|
|
138
|
+
if (!fileIndexGraph)
|
|
139
|
+
return;
|
|
140
|
+
let stat;
|
|
141
|
+
try {
|
|
142
|
+
stat = fs_1.default.statSync(absolutePath);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
const filePath = path_1.default.relative(config.projectDir, absolutePath);
|
|
146
|
+
if (fileIndexGraph.hasNode(filePath)) {
|
|
147
|
+
(0, file_index_1.removeFileEntry)(fileIndexGraph, filePath);
|
|
148
|
+
if (knowledgeGraph)
|
|
149
|
+
(0, knowledge_1.cleanupProxies)(knowledgeGraph, 'files', fileIndexGraph);
|
|
150
|
+
if (taskGraph)
|
|
151
|
+
(0, task_1.cleanupProxies)(taskGraph, 'files', fileIndexGraph);
|
|
152
|
+
if (skillGraph)
|
|
153
|
+
(0, skill_1.cleanupProxies)(skillGraph, 'files', fileIndexGraph);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const mtime = stat.mtimeMs;
|
|
158
|
+
const filePath = path_1.default.relative(config.projectDir, absolutePath);
|
|
159
|
+
if ((0, file_index_1.getFileEntryMtime)(fileIndexGraph, filePath) === mtime)
|
|
160
|
+
return;
|
|
161
|
+
const embedding = await (0, embedder_1.embed)(filePath, '', config.filesModelName);
|
|
162
|
+
(0, file_index_1.updateFileEntry)(fileIndexGraph, filePath, stat.size, mtime, embedding);
|
|
163
|
+
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Dispatch: match a file against both patterns, enqueue as needed
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
const excludePatterns = config.excludePattern
|
|
168
|
+
? config.excludePattern.split(',').map(p => p.trim()).filter(Boolean)
|
|
169
|
+
: [];
|
|
170
|
+
function isExcluded(rel) {
|
|
171
|
+
return excludePatterns.length > 0 && micromatch_1.default.isMatch(rel, excludePatterns);
|
|
172
|
+
}
|
|
173
|
+
function dispatchAdd(absolutePath) {
|
|
174
|
+
// tsconfig change — reset ts-morph project and re-index all code files
|
|
175
|
+
if (codeGraph && project && absolutePath === tsconfigPath) {
|
|
176
|
+
process.stderr.write(`[indexer] tsconfig changed, re-indexing code\n`);
|
|
177
|
+
(0, code_1.resetProject)(config.projectDir);
|
|
178
|
+
project = (0, code_1.getProject)(config.projectDir, config.tsconfig);
|
|
179
|
+
codeGraph.forEachNode((nodeId, attrs) => {
|
|
180
|
+
if (attrs.kind === 'file') {
|
|
181
|
+
const abs = path_1.default.join(config.projectDir, attrs.fileId);
|
|
182
|
+
// Reset mtime to force re-index
|
|
183
|
+
codeGraph.setNodeAttribute(nodeId, 'mtime', 0);
|
|
184
|
+
enqueueCode(() => indexCodeFile(abs));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const rel = path_1.default.relative(config.projectDir, absolutePath);
|
|
190
|
+
if (isExcluded(rel))
|
|
191
|
+
return;
|
|
192
|
+
if (config.docsPattern && micromatch_1.default.isMatch(rel, config.docsPattern)) {
|
|
193
|
+
enqueueDoc(() => indexDocFile(absolutePath));
|
|
194
|
+
}
|
|
195
|
+
if (codeGraph && config.codePattern && micromatch_1.default.isMatch(rel, config.codePattern)) {
|
|
196
|
+
enqueueCode(() => indexCodeFile(absolutePath));
|
|
197
|
+
}
|
|
198
|
+
if (fileIndexGraph) {
|
|
199
|
+
enqueueFile(() => indexFileEntry(absolutePath));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function dispatchRemove(absolutePath) {
|
|
203
|
+
const rel = path_1.default.relative(config.projectDir, absolutePath);
|
|
204
|
+
if (isExcluded(rel))
|
|
205
|
+
return;
|
|
206
|
+
if (docGraph && config.docsPattern && micromatch_1.default.isMatch(rel, config.docsPattern)) {
|
|
207
|
+
(0, docs_2.removeFile)(docGraph, rel);
|
|
208
|
+
if (knowledgeGraph)
|
|
209
|
+
(0, knowledge_1.cleanupProxies)(knowledgeGraph, 'docs', docGraph);
|
|
210
|
+
if (taskGraph)
|
|
211
|
+
(0, task_1.cleanupProxies)(taskGraph, 'docs', docGraph);
|
|
212
|
+
if (skillGraph)
|
|
213
|
+
(0, skill_1.cleanupProxies)(skillGraph, 'docs', docGraph);
|
|
214
|
+
process.stderr.write(`[indexer] removed doc ${rel}\n`);
|
|
215
|
+
}
|
|
216
|
+
if (codeGraph && config.codePattern && micromatch_1.default.isMatch(rel, config.codePattern)) {
|
|
217
|
+
(0, code_2.removeCodeFile)(codeGraph, rel);
|
|
218
|
+
if (knowledgeGraph)
|
|
219
|
+
(0, knowledge_1.cleanupProxies)(knowledgeGraph, 'code', codeGraph);
|
|
220
|
+
if (taskGraph)
|
|
221
|
+
(0, task_1.cleanupProxies)(taskGraph, 'code', codeGraph);
|
|
222
|
+
if (skillGraph)
|
|
223
|
+
(0, skill_1.cleanupProxies)(skillGraph, 'code', codeGraph);
|
|
224
|
+
process.stderr.write(`[indexer] removed code ${rel}\n`);
|
|
225
|
+
}
|
|
226
|
+
if (fileIndexGraph) {
|
|
227
|
+
(0, file_index_1.removeFileEntry)(fileIndexGraph, rel);
|
|
228
|
+
if (knowledgeGraph)
|
|
229
|
+
(0, knowledge_1.cleanupProxies)(knowledgeGraph, 'files', fileIndexGraph);
|
|
230
|
+
if (taskGraph)
|
|
231
|
+
(0, task_1.cleanupProxies)(taskGraph, 'files', fileIndexGraph);
|
|
232
|
+
if (skillGraph)
|
|
233
|
+
(0, skill_1.cleanupProxies)(skillGraph, 'files', fileIndexGraph);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Public API
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
function scan() {
|
|
240
|
+
function walk(dir) {
|
|
241
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
242
|
+
if (entry.name.startsWith('.'))
|
|
243
|
+
continue;
|
|
244
|
+
const full = path_1.default.join(dir, entry.name);
|
|
245
|
+
if (entry.isDirectory()) {
|
|
246
|
+
const relDir = path_1.default.relative(config.projectDir, full);
|
|
247
|
+
// prune directory if any file inside would be excluded (handles both `dir` and `dir/**` patterns)
|
|
248
|
+
if (excludePatterns.length > 0 && (micromatch_1.default.isMatch(relDir, excludePatterns) ||
|
|
249
|
+
micromatch_1.default.isMatch(relDir + '/x', excludePatterns)))
|
|
250
|
+
continue;
|
|
251
|
+
walk(full);
|
|
252
|
+
}
|
|
253
|
+
else if (entry.isFile()) {
|
|
254
|
+
dispatchAdd(full);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
walk(config.projectDir);
|
|
259
|
+
}
|
|
260
|
+
function watch() {
|
|
261
|
+
return (0, watcher_1.startWatcher)(config.projectDir, {
|
|
262
|
+
onAdd: (f) => dispatchAdd(f),
|
|
263
|
+
onChange: (f) => dispatchAdd(f),
|
|
264
|
+
onUnlink: (f) => dispatchRemove(f),
|
|
265
|
+
}, '**/*', excludePatterns.length > 0 ? excludePatterns : undefined);
|
|
266
|
+
}
|
|
267
|
+
async function drain() {
|
|
268
|
+
await Promise.all([docsQueue, codeQueue, fileQueue]);
|
|
269
|
+
if (fileIndexGraph)
|
|
270
|
+
(0, file_index_1.rebuildDirectoryStats)(fileIndexGraph);
|
|
271
|
+
const totalErrors = docErrors + codeErrors + fileErrors;
|
|
272
|
+
if (totalErrors > 0) {
|
|
273
|
+
process.stderr.write(`[indexer] Completed with ${totalErrors} error(s): docs=${docErrors}, code=${codeErrors}, files=${fileErrors}\n`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { scan, watch, drain };
|
|
277
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.scanAttachments = scanAttachments;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const mime_1 = __importDefault(require("mime"));
|
|
43
|
+
/**
|
|
44
|
+
* Scan the attachments/ subdirectory of an entity directory.
|
|
45
|
+
* Returns metadata for each file found.
|
|
46
|
+
*/
|
|
47
|
+
function scanAttachments(entityDir) {
|
|
48
|
+
try {
|
|
49
|
+
const attachmentsDir = path.join(entityDir, 'attachments');
|
|
50
|
+
if (!fs.existsSync(attachmentsDir))
|
|
51
|
+
return [];
|
|
52
|
+
const entries = fs.readdirSync(attachmentsDir, { withFileTypes: true });
|
|
53
|
+
const attachments = [];
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (!entry.isFile())
|
|
56
|
+
continue;
|
|
57
|
+
const filePath = path.join(attachmentsDir, entry.name);
|
|
58
|
+
try {
|
|
59
|
+
const stat = fs.statSync(filePath);
|
|
60
|
+
attachments.push({
|
|
61
|
+
filename: entry.name,
|
|
62
|
+
mimeType: mime_1.default.getType(entry.name) ?? 'application/octet-stream',
|
|
63
|
+
size: stat.size,
|
|
64
|
+
addedAt: stat.birthtimeMs || stat.mtimeMs,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch { /* skip unreadable files */ }
|
|
68
|
+
}
|
|
69
|
+
return attachments;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCodeGraph = createCodeGraph;
|
|
4
|
+
const graphology_1 = require("graphology");
|
|
5
|
+
function createCodeGraph() {
|
|
6
|
+
return new graphology_1.DirectedGraph({
|
|
7
|
+
multi: false,
|
|
8
|
+
allowSelfLoops: false,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
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.CodeGraphManager = exports.createCodeGraph = void 0;
|
|
7
|
+
exports.updateCodeFile = updateCodeFile;
|
|
8
|
+
exports.removeCodeFile = removeCodeFile;
|
|
9
|
+
exports.getFileSymbols = getFileSymbols;
|
|
10
|
+
exports.getCodeFileMtime = getCodeFileMtime;
|
|
11
|
+
exports.listCodeFiles = listCodeFiles;
|
|
12
|
+
exports.saveCodeGraph = saveCodeGraph;
|
|
13
|
+
exports.loadCodeGraph = loadCodeGraph;
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const code_types_1 = require("../graphs/code-types");
|
|
17
|
+
Object.defineProperty(exports, "createCodeGraph", { enumerable: true, get: function () { return code_types_1.createCodeGraph; } });
|
|
18
|
+
const manager_types_1 = require("../graphs/manager-types");
|
|
19
|
+
const code_1 = require("../lib/search/code");
|
|
20
|
+
const files_1 = require("../lib/search/files");
|
|
21
|
+
const bm25_1 = require("../lib/search/bm25");
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// CRUD
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/** Replace all nodes/edges for a given file. */
|
|
26
|
+
function updateCodeFile(graph, parsed) {
|
|
27
|
+
removeCodeFile(graph, parsed.fileId);
|
|
28
|
+
for (const { id, attrs } of parsed.nodes) {
|
|
29
|
+
graph.addNode(id, attrs);
|
|
30
|
+
}
|
|
31
|
+
for (const { from, to, attrs } of parsed.edges) {
|
|
32
|
+
// Target node may not be indexed yet (e.g. cross-file edge to un-indexed file).
|
|
33
|
+
// Add a pending stub so the edge can be created; it will be replaced when
|
|
34
|
+
// that file is indexed.
|
|
35
|
+
if (!graph.hasNode(to))
|
|
36
|
+
continue; // skip dangling edges for now
|
|
37
|
+
if (graph.hasNode(from) && !graph.hasEdge(from, to)) {
|
|
38
|
+
graph.addEdgeWithKey(`${from}→${to}`, from, to, attrs);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Remove all nodes (and their incident edges) belonging to a file. */
|
|
43
|
+
function removeCodeFile(graph, fileId) {
|
|
44
|
+
const toRemove = graph.filterNodes((_, attrs) => attrs.fileId === fileId);
|
|
45
|
+
toRemove.forEach(id => graph.dropNode(id));
|
|
46
|
+
}
|
|
47
|
+
/** Return all nodes for a file, sorted by startLine. */
|
|
48
|
+
function getFileSymbols(graph, fileId) {
|
|
49
|
+
return graph
|
|
50
|
+
.filterNodes((_, attrs) => attrs.fileId === fileId)
|
|
51
|
+
.map(id => ({ id, ...graph.getNodeAttributes(id) }))
|
|
52
|
+
.sort((a, b) => a.startLine - b.startLine);
|
|
53
|
+
}
|
|
54
|
+
/** Return mtime for any node in the file (0 if not indexed). */
|
|
55
|
+
function getCodeFileMtime(graph, fileId) {
|
|
56
|
+
const nodes = graph.filterNodes((_, attrs) => attrs.fileId === fileId);
|
|
57
|
+
if (nodes.length === 0)
|
|
58
|
+
return 0;
|
|
59
|
+
return graph.getNodeAttribute(nodes[0], 'mtime');
|
|
60
|
+
}
|
|
61
|
+
/** List all indexed files with symbol counts. */
|
|
62
|
+
function listCodeFiles(graph, filter, limit = 20) {
|
|
63
|
+
const files = new Map();
|
|
64
|
+
const lowerFilter = filter?.toLowerCase();
|
|
65
|
+
graph.forEachNode((_, attrs) => {
|
|
66
|
+
files.set(attrs.fileId, (files.get(attrs.fileId) ?? 0) + 1);
|
|
67
|
+
});
|
|
68
|
+
let result = [...files.entries()]
|
|
69
|
+
.map(([fileId, symbolCount]) => ({ fileId, symbolCount }))
|
|
70
|
+
.sort((a, b) => a.fileId.localeCompare(b.fileId));
|
|
71
|
+
if (lowerFilter) {
|
|
72
|
+
result = result.filter(f => f.fileId.toLowerCase().includes(lowerFilter));
|
|
73
|
+
}
|
|
74
|
+
return result.slice(0, limit);
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Persistence
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
function saveCodeGraph(graph, graphMemory, embeddingFingerprint) {
|
|
80
|
+
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
81
|
+
const file = path_1.default.join(graphMemory, 'code.json');
|
|
82
|
+
const tmp = file + '.tmp';
|
|
83
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
|
|
84
|
+
fs_1.default.renameSync(tmp, file);
|
|
85
|
+
}
|
|
86
|
+
function loadCodeGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
87
|
+
const graph = (0, code_types_1.createCodeGraph)();
|
|
88
|
+
if (fresh)
|
|
89
|
+
return graph;
|
|
90
|
+
const file = path_1.default.join(graphMemory, 'code.json');
|
|
91
|
+
if (!fs_1.default.existsSync(file))
|
|
92
|
+
return graph;
|
|
93
|
+
try {
|
|
94
|
+
const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
95
|
+
const stored = data.embeddingModel;
|
|
96
|
+
if (embeddingFingerprint && stored !== embeddingFingerprint) {
|
|
97
|
+
process.stderr.write(`[code-graph] Embedding config changed, re-indexing code graph\n`);
|
|
98
|
+
return graph;
|
|
99
|
+
}
|
|
100
|
+
graph.import(data.graph);
|
|
101
|
+
process.stderr.write(`[code-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
process.stderr.write(`[code-graph] Failed to load graph, starting fresh: ${err}\n`);
|
|
105
|
+
}
|
|
106
|
+
return graph;
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// CodeGraphManager — unified API for code graph operations
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
class CodeGraphManager {
|
|
112
|
+
_graph;
|
|
113
|
+
embedFns;
|
|
114
|
+
ext;
|
|
115
|
+
_bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment}`);
|
|
116
|
+
constructor(_graph, embedFns, ext = {}) {
|
|
117
|
+
this._graph = _graph;
|
|
118
|
+
this.embedFns = embedFns;
|
|
119
|
+
this.ext = ext;
|
|
120
|
+
_graph.forEachNode((id, attrs) => {
|
|
121
|
+
this._bm25Index.addDocument(id, attrs);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
get graph() { return this._graph; }
|
|
125
|
+
get bm25Index() { return this._bm25Index; }
|
|
126
|
+
// -- Write (used by indexer) --
|
|
127
|
+
updateFile(parsed) {
|
|
128
|
+
// Remove old nodes from BM25
|
|
129
|
+
this._graph.forEachNode((id, attrs) => {
|
|
130
|
+
if (attrs.fileId === parsed.fileId)
|
|
131
|
+
this._bm25Index.removeDocument(id);
|
|
132
|
+
});
|
|
133
|
+
updateCodeFile(this._graph, parsed);
|
|
134
|
+
// Add new nodes to BM25
|
|
135
|
+
this._graph.forEachNode((id, attrs) => {
|
|
136
|
+
if (attrs.fileId === parsed.fileId)
|
|
137
|
+
this._bm25Index.addDocument(id, attrs);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
removeFile(fileId) {
|
|
141
|
+
this._graph.forEachNode((id, attrs) => {
|
|
142
|
+
if (attrs.fileId === fileId)
|
|
143
|
+
this._bm25Index.removeDocument(id);
|
|
144
|
+
});
|
|
145
|
+
removeCodeFile(this._graph, fileId);
|
|
146
|
+
}
|
|
147
|
+
// -- Read --
|
|
148
|
+
listFiles(filter, limit) {
|
|
149
|
+
return listCodeFiles(this._graph, filter, limit);
|
|
150
|
+
}
|
|
151
|
+
getFileSymbols(fileId) {
|
|
152
|
+
return getFileSymbols(this._graph, fileId);
|
|
153
|
+
}
|
|
154
|
+
getFileMtime(fileId) {
|
|
155
|
+
return getCodeFileMtime(this._graph, fileId);
|
|
156
|
+
}
|
|
157
|
+
getSymbol(nodeId) {
|
|
158
|
+
if (!this._graph.hasNode(nodeId))
|
|
159
|
+
return null;
|
|
160
|
+
const crossLinks = (0, manager_types_1.findIncomingCrossLinks)(this.ext, 'code', nodeId);
|
|
161
|
+
return { id: nodeId, ...this._graph.getNodeAttributes(nodeId), ...(crossLinks.length > 0 ? { crossLinks } : {}) };
|
|
162
|
+
}
|
|
163
|
+
async search(query, opts) {
|
|
164
|
+
const embedding = opts?.searchMode === 'keyword' ? [] : await this.embedFns.query(query);
|
|
165
|
+
return (0, code_1.searchCode)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._bm25Index });
|
|
166
|
+
}
|
|
167
|
+
async searchFiles(query, opts) {
|
|
168
|
+
const embedding = await this.embedFns.query(query);
|
|
169
|
+
return (0, files_1.searchCodeFiles)(this._graph, embedding, opts);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.CodeGraphManager = CodeGraphManager;
|