@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,198 @@
|
|
|
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.DocGraphManager = void 0;
|
|
7
|
+
exports.createGraph = createGraph;
|
|
8
|
+
exports.updateFile = updateFile;
|
|
9
|
+
exports.removeFile = removeFile;
|
|
10
|
+
exports.getFileChunks = getFileChunks;
|
|
11
|
+
exports.getFileMtime = getFileMtime;
|
|
12
|
+
exports.listFiles = listFiles;
|
|
13
|
+
exports.saveGraph = saveGraph;
|
|
14
|
+
exports.loadGraph = loadGraph;
|
|
15
|
+
const graphology_1 = require("graphology");
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const manager_types_1 = require("../graphs/manager-types");
|
|
19
|
+
const docs_1 = require("../lib/search/docs");
|
|
20
|
+
const files_1 = require("../lib/search/files");
|
|
21
|
+
const bm25_1 = require("../lib/search/bm25");
|
|
22
|
+
function createGraph() {
|
|
23
|
+
return new graphology_1.DirectedGraph({ multi: false, allowSelfLoops: false });
|
|
24
|
+
}
|
|
25
|
+
// Replace all chunks for a given file
|
|
26
|
+
function updateFile(graph, chunks, mtime) {
|
|
27
|
+
if (chunks.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
const fileId = chunks[0].fileId;
|
|
30
|
+
removeFile(graph, fileId);
|
|
31
|
+
// Add nodes
|
|
32
|
+
for (const chunk of chunks) {
|
|
33
|
+
graph.addNode(chunk.id, {
|
|
34
|
+
fileId,
|
|
35
|
+
title: chunk.title,
|
|
36
|
+
content: chunk.content,
|
|
37
|
+
embedding: chunk.embedding,
|
|
38
|
+
fileEmbedding: [],
|
|
39
|
+
level: chunk.level,
|
|
40
|
+
mtime,
|
|
41
|
+
language: chunk.language,
|
|
42
|
+
symbols: chunk.symbols ?? [],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// Sibling edges: consecutive chunks within the same file
|
|
46
|
+
for (let i = 0; i < chunks.length - 1; i++) {
|
|
47
|
+
graph.addEdge(chunks[i].id, chunks[i + 1].id);
|
|
48
|
+
}
|
|
49
|
+
// Cross-file link edges: chunk → root chunk of target file
|
|
50
|
+
for (const chunk of chunks) {
|
|
51
|
+
for (const targetFileId of chunk.links) {
|
|
52
|
+
const targetRootId = targetFileId; // root chunk id === fileId
|
|
53
|
+
if (graph.hasNode(targetRootId) && chunk.id !== targetRootId) {
|
|
54
|
+
if (!graph.hasEdge(chunk.id, targetRootId)) {
|
|
55
|
+
graph.addEdge(chunk.id, targetRootId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function removeFile(graph, fileId) {
|
|
62
|
+
const toRemove = graph.filterNodes((_, attrs) => attrs.fileId === fileId);
|
|
63
|
+
toRemove.forEach(id => graph.dropNode(id));
|
|
64
|
+
}
|
|
65
|
+
function getFileChunks(graph, fileId) {
|
|
66
|
+
return graph
|
|
67
|
+
.filterNodes((_, attrs) => attrs.fileId === fileId)
|
|
68
|
+
.map(id => ({ id, ...graph.getNodeAttributes(id) }))
|
|
69
|
+
.sort((a, b) => a.level - b.level);
|
|
70
|
+
}
|
|
71
|
+
function getFileMtime(graph, fileId) {
|
|
72
|
+
const nodes = graph.filterNodes((_, attrs) => attrs.fileId === fileId);
|
|
73
|
+
if (nodes.length === 0)
|
|
74
|
+
return 0;
|
|
75
|
+
return graph.getNodeAttribute(nodes[0], 'mtime');
|
|
76
|
+
}
|
|
77
|
+
function listFiles(graph, filter, limit = 20) {
|
|
78
|
+
const files = new Map();
|
|
79
|
+
const lowerFilter = filter?.toLowerCase();
|
|
80
|
+
graph.forEachNode((_, attrs) => {
|
|
81
|
+
const entry = files.get(attrs.fileId);
|
|
82
|
+
if (!entry) {
|
|
83
|
+
files.set(attrs.fileId, { title: attrs.title, chunks: 1 });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
if (attrs.level === 1)
|
|
87
|
+
entry.title = attrs.title;
|
|
88
|
+
entry.chunks++;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
let result = [...files.entries()]
|
|
92
|
+
.map(([fileId, { title, chunks }]) => ({ fileId, title, chunks }))
|
|
93
|
+
.sort((a, b) => a.fileId.localeCompare(b.fileId));
|
|
94
|
+
if (lowerFilter) {
|
|
95
|
+
result = result.filter(f => f.fileId.toLowerCase().includes(lowerFilter));
|
|
96
|
+
}
|
|
97
|
+
return result.slice(0, limit);
|
|
98
|
+
}
|
|
99
|
+
function saveGraph(graph, graphMemory, embeddingFingerprint) {
|
|
100
|
+
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
101
|
+
const file = path_1.default.join(graphMemory, 'docs.json');
|
|
102
|
+
const tmp = file + '.tmp';
|
|
103
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
|
|
104
|
+
fs_1.default.renameSync(tmp, file);
|
|
105
|
+
}
|
|
106
|
+
function loadGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
107
|
+
const graph = createGraph();
|
|
108
|
+
if (fresh)
|
|
109
|
+
return graph;
|
|
110
|
+
const file = path_1.default.join(graphMemory, 'docs.json');
|
|
111
|
+
if (!fs_1.default.existsSync(file))
|
|
112
|
+
return graph;
|
|
113
|
+
try {
|
|
114
|
+
const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
115
|
+
const stored = data.embeddingModel;
|
|
116
|
+
if (embeddingFingerprint && stored !== embeddingFingerprint) {
|
|
117
|
+
process.stderr.write(`[graph] Embedding config changed, re-indexing docs graph\n`);
|
|
118
|
+
return graph;
|
|
119
|
+
}
|
|
120
|
+
graph.import(data.graph);
|
|
121
|
+
process.stderr.write(`[graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
process.stderr.write(`[graph] Failed to load graph, starting fresh: ${err}\n`);
|
|
125
|
+
}
|
|
126
|
+
return graph;
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// DocGraphManager — unified API for docs graph operations
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
class DocGraphManager {
|
|
132
|
+
_graph;
|
|
133
|
+
embedFns;
|
|
134
|
+
ext;
|
|
135
|
+
_bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.title} ${attrs.content}`);
|
|
136
|
+
constructor(_graph, embedFns, ext = {}) {
|
|
137
|
+
this._graph = _graph;
|
|
138
|
+
this.embedFns = embedFns;
|
|
139
|
+
this.ext = ext;
|
|
140
|
+
_graph.forEachNode((id, attrs) => {
|
|
141
|
+
this._bm25Index.addDocument(id, attrs);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
get graph() { return this._graph; }
|
|
145
|
+
get bm25Index() { return this._bm25Index; }
|
|
146
|
+
// -- Write (used by indexer) --
|
|
147
|
+
updateFile(chunks, mtime) {
|
|
148
|
+
// Remove old nodes from BM25
|
|
149
|
+
if (chunks.length > 0) {
|
|
150
|
+
const fileId = chunks[0].fileId;
|
|
151
|
+
this._graph.forEachNode((id, attrs) => {
|
|
152
|
+
if (attrs.fileId === fileId)
|
|
153
|
+
this._bm25Index.removeDocument(id);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
updateFile(this._graph, chunks, mtime);
|
|
157
|
+
// Add new nodes to BM25
|
|
158
|
+
if (chunks.length > 0) {
|
|
159
|
+
const fileId = chunks[0].fileId;
|
|
160
|
+
this._graph.forEachNode((id, attrs) => {
|
|
161
|
+
if (attrs.fileId === fileId)
|
|
162
|
+
this._bm25Index.addDocument(id, attrs);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
removeFile(fileId) {
|
|
167
|
+
this._graph.forEachNode((id, attrs) => {
|
|
168
|
+
if (attrs.fileId === fileId)
|
|
169
|
+
this._bm25Index.removeDocument(id);
|
|
170
|
+
});
|
|
171
|
+
removeFile(this._graph, fileId);
|
|
172
|
+
}
|
|
173
|
+
// -- Read --
|
|
174
|
+
listFiles(filter, limit) {
|
|
175
|
+
return listFiles(this._graph, filter, limit);
|
|
176
|
+
}
|
|
177
|
+
getFileChunks(fileId) {
|
|
178
|
+
return getFileChunks(this._graph, fileId);
|
|
179
|
+
}
|
|
180
|
+
getFileMtime(fileId) {
|
|
181
|
+
return getFileMtime(this._graph, fileId);
|
|
182
|
+
}
|
|
183
|
+
getNode(nodeId) {
|
|
184
|
+
if (!this._graph.hasNode(nodeId))
|
|
185
|
+
return null;
|
|
186
|
+
const crossLinks = (0, manager_types_1.findIncomingCrossLinks)(this.ext, 'docs', nodeId);
|
|
187
|
+
return { id: nodeId, ...this._graph.getNodeAttributes(nodeId), ...(crossLinks.length > 0 ? { crossLinks } : {}) };
|
|
188
|
+
}
|
|
189
|
+
async search(query, opts) {
|
|
190
|
+
const embedding = opts?.searchMode === 'keyword' ? [] : await this.embedFns.query(query);
|
|
191
|
+
return (0, docs_1.search)(this._graph, embedding, { ...opts, queryText: query, bm25Index: this._bm25Index });
|
|
192
|
+
}
|
|
193
|
+
async searchFiles(query, opts) {
|
|
194
|
+
const embedding = await this.embedFns.query(query);
|
|
195
|
+
return (0, files_1.searchDocFiles)(this._graph, embedding, opts);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
exports.DocGraphManager = DocGraphManager;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createFileIndexGraph = createFileIndexGraph;
|
|
4
|
+
const graphology_1 = require("graphology");
|
|
5
|
+
function createFileIndexGraph() {
|
|
6
|
+
return new graphology_1.DirectedGraph({
|
|
7
|
+
multi: false,
|
|
8
|
+
allowSelfLoops: false,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
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.FileIndexGraphManager = void 0;
|
|
7
|
+
exports.ensureDirectoryChain = ensureDirectoryChain;
|
|
8
|
+
exports.updateFileEntry = updateFileEntry;
|
|
9
|
+
exports.removeFileEntry = removeFileEntry;
|
|
10
|
+
exports.getFileEntryMtime = getFileEntryMtime;
|
|
11
|
+
exports.listAllFiles = listAllFiles;
|
|
12
|
+
exports.getFileInfo = getFileInfo;
|
|
13
|
+
exports.rebuildDirectoryStats = rebuildDirectoryStats;
|
|
14
|
+
exports.saveFileIndexGraph = saveFileIndexGraph;
|
|
15
|
+
exports.loadFileIndexGraph = loadFileIndexGraph;
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const file_index_types_1 = require("../graphs/file-index-types");
|
|
19
|
+
const file_lang_1 = require("../graphs/file-lang");
|
|
20
|
+
const manager_types_1 = require("../graphs/manager-types");
|
|
21
|
+
const file_index_1 = require("../lib/search/file-index");
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// CRUD
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
/**
|
|
26
|
+
* Ensure the full directory chain exists from `dir` up to root (`.`).
|
|
27
|
+
* Creates directory nodes and `contains` edges as needed.
|
|
28
|
+
*/
|
|
29
|
+
function ensureDirectoryChain(graph, dir) {
|
|
30
|
+
if (dir === '.') {
|
|
31
|
+
ensureRootNode(graph);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!graph.hasNode(dir)) {
|
|
35
|
+
const parent = path_1.default.dirname(dir);
|
|
36
|
+
const parentDir = parent === '.' ? '.' : parent;
|
|
37
|
+
graph.addNode(dir, {
|
|
38
|
+
kind: 'directory',
|
|
39
|
+
filePath: dir,
|
|
40
|
+
fileName: path_1.default.basename(dir),
|
|
41
|
+
directory: parentDir,
|
|
42
|
+
extension: '',
|
|
43
|
+
language: null,
|
|
44
|
+
mimeType: null,
|
|
45
|
+
size: 0,
|
|
46
|
+
fileCount: 0,
|
|
47
|
+
embedding: [],
|
|
48
|
+
mtime: 0,
|
|
49
|
+
});
|
|
50
|
+
ensureDirectoryChain(graph, parentDir);
|
|
51
|
+
if (!graph.hasEdge(parentDir, dir)) {
|
|
52
|
+
graph.addEdge(parentDir, dir, { kind: 'contains' });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function ensureRootNode(graph) {
|
|
57
|
+
if (!graph.hasNode('.')) {
|
|
58
|
+
graph.addNode('.', {
|
|
59
|
+
kind: 'directory',
|
|
60
|
+
filePath: '.',
|
|
61
|
+
fileName: '.',
|
|
62
|
+
directory: '',
|
|
63
|
+
extension: '',
|
|
64
|
+
language: null,
|
|
65
|
+
mimeType: null,
|
|
66
|
+
size: 0,
|
|
67
|
+
fileCount: 0,
|
|
68
|
+
embedding: [],
|
|
69
|
+
mtime: 0,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Add or update a file entry in the graph.
|
|
75
|
+
* Creates parent directory chain + `contains` edges automatically.
|
|
76
|
+
*/
|
|
77
|
+
function updateFileEntry(graph, filePath, size, mtime, embedding) {
|
|
78
|
+
const ext = path_1.default.extname(filePath);
|
|
79
|
+
const dir = path_1.default.dirname(filePath);
|
|
80
|
+
const directory = dir === '.' ? '.' : dir;
|
|
81
|
+
const attrs = {
|
|
82
|
+
kind: 'file',
|
|
83
|
+
filePath,
|
|
84
|
+
fileName: path_1.default.basename(filePath),
|
|
85
|
+
directory,
|
|
86
|
+
extension: ext,
|
|
87
|
+
language: (0, file_lang_1.getLanguage)(ext),
|
|
88
|
+
mimeType: (0, file_lang_1.getMimeType)(ext),
|
|
89
|
+
size,
|
|
90
|
+
fileCount: 0,
|
|
91
|
+
embedding,
|
|
92
|
+
mtime,
|
|
93
|
+
};
|
|
94
|
+
if (graph.hasNode(filePath)) {
|
|
95
|
+
graph.replaceNodeAttributes(filePath, attrs);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
graph.addNode(filePath, attrs);
|
|
99
|
+
ensureDirectoryChain(graph, directory);
|
|
100
|
+
if (!graph.hasEdge(directory, filePath)) {
|
|
101
|
+
graph.addEdge(directory, filePath, { kind: 'contains' });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Remove a file node from the graph.
|
|
107
|
+
* Cleans up empty directory nodes (directories with no remaining children).
|
|
108
|
+
*/
|
|
109
|
+
function removeFileEntry(graph, filePath) {
|
|
110
|
+
if (!graph.hasNode(filePath))
|
|
111
|
+
return;
|
|
112
|
+
const dir = graph.getNodeAttribute(filePath, 'directory');
|
|
113
|
+
graph.dropNode(filePath);
|
|
114
|
+
cleanEmptyDirs(graph, dir);
|
|
115
|
+
}
|
|
116
|
+
/** Recursively remove directory nodes that have no children. */
|
|
117
|
+
function cleanEmptyDirs(graph, dir) {
|
|
118
|
+
if (!dir || dir === '')
|
|
119
|
+
return;
|
|
120
|
+
if (!graph.hasNode(dir))
|
|
121
|
+
return;
|
|
122
|
+
if (graph.getNodeAttribute(dir, 'kind') !== 'directory')
|
|
123
|
+
return;
|
|
124
|
+
// Count outgoing `contains` edges
|
|
125
|
+
const children = graph.outDegree(dir);
|
|
126
|
+
if (children > 0)
|
|
127
|
+
return;
|
|
128
|
+
// No children — remove this directory
|
|
129
|
+
const parent = graph.getNodeAttribute(dir, 'directory');
|
|
130
|
+
graph.dropNode(dir);
|
|
131
|
+
// Don't remove root
|
|
132
|
+
if (dir === '.')
|
|
133
|
+
return;
|
|
134
|
+
cleanEmptyDirs(graph, parent);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get mtime for a file node. Returns 0 if not found.
|
|
138
|
+
*/
|
|
139
|
+
function getFileEntryMtime(graph, filePath) {
|
|
140
|
+
if (!graph.hasNode(filePath))
|
|
141
|
+
return 0;
|
|
142
|
+
return graph.getNodeAttribute(filePath, 'mtime');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* List files (and directories when browsing a directory).
|
|
146
|
+
* When `directory` is provided, returns immediate children of that directory.
|
|
147
|
+
* Otherwise returns all file nodes matching the filters.
|
|
148
|
+
*/
|
|
149
|
+
function listAllFiles(graph, options = {}) {
|
|
150
|
+
const { directory, extension, language, filter, limit = 50 } = options;
|
|
151
|
+
const lowerFilter = filter?.toLowerCase();
|
|
152
|
+
const results = [];
|
|
153
|
+
if (directory !== undefined) {
|
|
154
|
+
// List immediate children of the specified directory
|
|
155
|
+
const dirId = directory || '.';
|
|
156
|
+
if (!graph.hasNode(dirId))
|
|
157
|
+
return [];
|
|
158
|
+
graph.forEachOutNeighbor(dirId, (childId) => {
|
|
159
|
+
const attrs = graph.getNodeAttributes(childId);
|
|
160
|
+
if (graph.getEdgeAttribute(graph.edge(dirId, childId), 'kind') !== 'contains')
|
|
161
|
+
return;
|
|
162
|
+
if (extension && attrs.extension !== extension)
|
|
163
|
+
return;
|
|
164
|
+
if (language && attrs.language !== language)
|
|
165
|
+
return;
|
|
166
|
+
if (lowerFilter && !attrs.filePath.toLowerCase().includes(lowerFilter))
|
|
167
|
+
return;
|
|
168
|
+
results.push(toEntry(attrs));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// List all files matching filters (no dirs in flat mode)
|
|
173
|
+
graph.forEachNode((_, attrs) => {
|
|
174
|
+
if (attrs.kind !== 'file')
|
|
175
|
+
return;
|
|
176
|
+
if (extension && attrs.extension !== extension)
|
|
177
|
+
return;
|
|
178
|
+
if (language && attrs.language !== language)
|
|
179
|
+
return;
|
|
180
|
+
if (lowerFilter && !attrs.filePath.toLowerCase().includes(lowerFilter))
|
|
181
|
+
return;
|
|
182
|
+
results.push(toEntry(attrs));
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
results.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
186
|
+
return results.slice(0, limit);
|
|
187
|
+
}
|
|
188
|
+
function toEntry(attrs) {
|
|
189
|
+
return {
|
|
190
|
+
filePath: attrs.filePath,
|
|
191
|
+
kind: attrs.kind,
|
|
192
|
+
fileName: attrs.fileName,
|
|
193
|
+
extension: attrs.extension,
|
|
194
|
+
language: attrs.language,
|
|
195
|
+
mimeType: attrs.mimeType,
|
|
196
|
+
size: attrs.size,
|
|
197
|
+
fileCount: attrs.fileCount,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get full info for a specific file or directory.
|
|
202
|
+
*/
|
|
203
|
+
function getFileInfo(graph, filePath) {
|
|
204
|
+
if (!graph.hasNode(filePath))
|
|
205
|
+
return null;
|
|
206
|
+
const attrs = graph.getNodeAttributes(filePath);
|
|
207
|
+
return {
|
|
208
|
+
...toEntry(attrs),
|
|
209
|
+
directory: attrs.directory,
|
|
210
|
+
mtime: attrs.mtime,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Recompute `size` and `fileCount` aggregates on all directory nodes.
|
|
215
|
+
* Call after a full scan/drain to ensure stats are up to date.
|
|
216
|
+
*/
|
|
217
|
+
function rebuildDirectoryStats(graph) {
|
|
218
|
+
// Reset all directory stats
|
|
219
|
+
graph.forEachNode((id, attrs) => {
|
|
220
|
+
if (attrs.kind === 'directory') {
|
|
221
|
+
graph.setNodeAttribute(id, 'size', 0);
|
|
222
|
+
graph.setNodeAttribute(id, 'fileCount', 0);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
// Walk all file nodes and accumulate to their direct parent
|
|
226
|
+
graph.forEachNode((_, attrs) => {
|
|
227
|
+
if (attrs.kind !== 'file')
|
|
228
|
+
return;
|
|
229
|
+
const dir = attrs.directory;
|
|
230
|
+
if (!graph.hasNode(dir))
|
|
231
|
+
return;
|
|
232
|
+
graph.setNodeAttribute(dir, 'size', graph.getNodeAttribute(dir, 'size') + attrs.size);
|
|
233
|
+
graph.setNodeAttribute(dir, 'fileCount', graph.getNodeAttribute(dir, 'fileCount') + 1);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Persistence
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
function saveFileIndexGraph(graph, graphMemory, embeddingFingerprint) {
|
|
240
|
+
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
241
|
+
const file = path_1.default.join(graphMemory, 'file-index.json');
|
|
242
|
+
const tmp = file + '.tmp';
|
|
243
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
|
|
244
|
+
fs_1.default.renameSync(tmp, file);
|
|
245
|
+
}
|
|
246
|
+
function loadFileIndexGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
247
|
+
const graph = (0, file_index_types_1.createFileIndexGraph)();
|
|
248
|
+
if (fresh)
|
|
249
|
+
return graph;
|
|
250
|
+
const file = path_1.default.join(graphMemory, 'file-index.json');
|
|
251
|
+
if (!fs_1.default.existsSync(file))
|
|
252
|
+
return graph;
|
|
253
|
+
try {
|
|
254
|
+
const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
255
|
+
const stored = data.embeddingModel;
|
|
256
|
+
if (embeddingFingerprint && stored !== embeddingFingerprint) {
|
|
257
|
+
process.stderr.write(`[file-index] Embedding config changed, re-indexing file index\n`);
|
|
258
|
+
return graph;
|
|
259
|
+
}
|
|
260
|
+
graph.import(data.graph);
|
|
261
|
+
process.stderr.write(`[file-index] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
process.stderr.write(`[file-index] Failed to load graph, starting fresh: ${err}\n`);
|
|
265
|
+
}
|
|
266
|
+
return graph;
|
|
267
|
+
}
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// FileIndexGraphManager — unified API for file index graph operations
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
class FileIndexGraphManager {
|
|
272
|
+
_graph;
|
|
273
|
+
embedFns;
|
|
274
|
+
ext;
|
|
275
|
+
constructor(_graph, embedFns, ext = {}) {
|
|
276
|
+
this._graph = _graph;
|
|
277
|
+
this.embedFns = embedFns;
|
|
278
|
+
this.ext = ext;
|
|
279
|
+
}
|
|
280
|
+
get graph() { return this._graph; }
|
|
281
|
+
// -- Write (used by indexer) --
|
|
282
|
+
updateFileEntry(filePath, size, mtime, embedding) {
|
|
283
|
+
updateFileEntry(this._graph, filePath, size, mtime, embedding);
|
|
284
|
+
}
|
|
285
|
+
removeFileEntry(filePath) {
|
|
286
|
+
removeFileEntry(this._graph, filePath);
|
|
287
|
+
}
|
|
288
|
+
rebuildDirectoryStats() {
|
|
289
|
+
rebuildDirectoryStats(this._graph);
|
|
290
|
+
}
|
|
291
|
+
getFileEntryMtime(filePath) {
|
|
292
|
+
return getFileEntryMtime(this._graph, filePath);
|
|
293
|
+
}
|
|
294
|
+
// -- Read --
|
|
295
|
+
listAllFiles(options) {
|
|
296
|
+
return listAllFiles(this._graph, options);
|
|
297
|
+
}
|
|
298
|
+
getFileInfo(filePath) {
|
|
299
|
+
const info = getFileInfo(this._graph, filePath);
|
|
300
|
+
if (!info)
|
|
301
|
+
return null;
|
|
302
|
+
const crossLinks = (0, manager_types_1.findIncomingCrossLinks)(this.ext, 'files', filePath);
|
|
303
|
+
return crossLinks.length > 0 ? { ...info, crossLinks } : info;
|
|
304
|
+
}
|
|
305
|
+
async search(query, opts) {
|
|
306
|
+
const embedding = await this.embedFns.query(query);
|
|
307
|
+
return (0, file_index_1.searchFileIndex)(this._graph, embedding, opts);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
exports.FileIndexGraphManager = FileIndexGraphManager;
|
|
@@ -0,0 +1,119 @@
|
|
|
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.EXT_TO_LANGUAGE = void 0;
|
|
7
|
+
exports.getLanguage = getLanguage;
|
|
8
|
+
exports.getMimeType = getMimeType;
|
|
9
|
+
const mime_1 = __importDefault(require("mime"));
|
|
10
|
+
/** Extension → programming/markup language name. */
|
|
11
|
+
exports.EXT_TO_LANGUAGE = {
|
|
12
|
+
// JavaScript / TypeScript
|
|
13
|
+
'.js': 'javascript',
|
|
14
|
+
'.jsx': 'javascript',
|
|
15
|
+
'.mjs': 'javascript',
|
|
16
|
+
'.cjs': 'javascript',
|
|
17
|
+
'.ts': 'typescript',
|
|
18
|
+
'.tsx': 'typescript',
|
|
19
|
+
'.mts': 'typescript',
|
|
20
|
+
'.cts': 'typescript',
|
|
21
|
+
// Web
|
|
22
|
+
'.html': 'html',
|
|
23
|
+
'.htm': 'html',
|
|
24
|
+
'.css': 'css',
|
|
25
|
+
'.scss': 'scss',
|
|
26
|
+
'.sass': 'sass',
|
|
27
|
+
'.less': 'less',
|
|
28
|
+
'.svg': 'svg',
|
|
29
|
+
// Data / Config
|
|
30
|
+
'.json': 'json',
|
|
31
|
+
'.jsonc': 'json',
|
|
32
|
+
'.json5': 'json',
|
|
33
|
+
'.yaml': 'yaml',
|
|
34
|
+
'.yml': 'yaml',
|
|
35
|
+
'.toml': 'toml',
|
|
36
|
+
'.xml': 'xml',
|
|
37
|
+
'.csv': 'csv',
|
|
38
|
+
'.tsv': 'csv',
|
|
39
|
+
'.ini': 'ini',
|
|
40
|
+
'.env': 'dotenv',
|
|
41
|
+
// Markdown / Docs
|
|
42
|
+
'.md': 'markdown',
|
|
43
|
+
'.mdx': 'markdown',
|
|
44
|
+
'.rst': 'restructuredtext',
|
|
45
|
+
'.txt': 'plaintext',
|
|
46
|
+
// Shell
|
|
47
|
+
'.sh': 'shell',
|
|
48
|
+
'.bash': 'shell',
|
|
49
|
+
'.zsh': 'shell',
|
|
50
|
+
'.fish': 'shell',
|
|
51
|
+
'.ps1': 'powershell',
|
|
52
|
+
'.bat': 'batch',
|
|
53
|
+
'.cmd': 'batch',
|
|
54
|
+
// Python
|
|
55
|
+
'.py': 'python',
|
|
56
|
+
'.pyi': 'python',
|
|
57
|
+
'.pyx': 'python',
|
|
58
|
+
// Ruby
|
|
59
|
+
'.rb': 'ruby',
|
|
60
|
+
'.erb': 'ruby',
|
|
61
|
+
// Go
|
|
62
|
+
'.go': 'go',
|
|
63
|
+
// Rust
|
|
64
|
+
'.rs': 'rust',
|
|
65
|
+
// Java / Kotlin / Scala
|
|
66
|
+
'.java': 'java',
|
|
67
|
+
'.kt': 'kotlin',
|
|
68
|
+
'.kts': 'kotlin',
|
|
69
|
+
'.scala': 'scala',
|
|
70
|
+
// C / C++
|
|
71
|
+
'.c': 'c',
|
|
72
|
+
'.h': 'c',
|
|
73
|
+
'.cpp': 'cpp',
|
|
74
|
+
'.cc': 'cpp',
|
|
75
|
+
'.cxx': 'cpp',
|
|
76
|
+
'.hpp': 'cpp',
|
|
77
|
+
'.hxx': 'cpp',
|
|
78
|
+
// C#
|
|
79
|
+
'.cs': 'csharp',
|
|
80
|
+
// Swift / Objective-C
|
|
81
|
+
'.swift': 'swift',
|
|
82
|
+
'.m': 'objectivec',
|
|
83
|
+
'.mm': 'objectivec',
|
|
84
|
+
// PHP
|
|
85
|
+
'.php': 'php',
|
|
86
|
+
// SQL
|
|
87
|
+
'.sql': 'sql',
|
|
88
|
+
// Docker
|
|
89
|
+
'.dockerfile': 'dockerfile',
|
|
90
|
+
// GraphQL
|
|
91
|
+
'.graphql': 'graphql',
|
|
92
|
+
'.gql': 'graphql',
|
|
93
|
+
// Protocol Buffers
|
|
94
|
+
'.proto': 'protobuf',
|
|
95
|
+
// Lua
|
|
96
|
+
'.lua': 'lua',
|
|
97
|
+
// R
|
|
98
|
+
'.r': 'r',
|
|
99
|
+
'.R': 'r',
|
|
100
|
+
// Elixir / Erlang
|
|
101
|
+
'.ex': 'elixir',
|
|
102
|
+
'.exs': 'elixir',
|
|
103
|
+
'.erl': 'erlang',
|
|
104
|
+
// Haskell
|
|
105
|
+
'.hs': 'haskell',
|
|
106
|
+
// Dart
|
|
107
|
+
'.dart': 'dart',
|
|
108
|
+
// Zig
|
|
109
|
+
'.zig': 'zig',
|
|
110
|
+
};
|
|
111
|
+
/** Look up language from file extension. Returns null if unknown. */
|
|
112
|
+
function getLanguage(ext) {
|
|
113
|
+
return exports.EXT_TO_LANGUAGE[ext.toLowerCase()] ?? null;
|
|
114
|
+
}
|
|
115
|
+
/** Look up MIME type from file extension via `mime` library. Returns null if unknown. */
|
|
116
|
+
function getMimeType(ext) {
|
|
117
|
+
// mime.getType accepts extension with or without dot, e.g. "ts" or ".ts"
|
|
118
|
+
return mime_1.default.getType(ext) ?? null;
|
|
119
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createKnowledgeGraph = createKnowledgeGraph;
|
|
4
|
+
exports.slugify = slugify;
|
|
5
|
+
const graphology_1 = require("graphology");
|
|
6
|
+
function createKnowledgeGraph() {
|
|
7
|
+
return new graphology_1.DirectedGraph({
|
|
8
|
+
multi: false,
|
|
9
|
+
allowSelfLoops: false,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generate a slug ID from a title: lowercase, spaces → hyphens, strip non-alphanumeric.
|
|
14
|
+
* Dedup with ::2, ::3, etc. if the ID already exists in the graph.
|
|
15
|
+
*/
|
|
16
|
+
function slugify(title, graph) {
|
|
17
|
+
const base = title
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.trim()
|
|
20
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
21
|
+
.replace(/\s+/g, '-')
|
|
22
|
+
.replace(/-+/g, '-')
|
|
23
|
+
.replace(/^-|-$/g, '');
|
|
24
|
+
if (!base)
|
|
25
|
+
return `note-${Date.now()}`;
|
|
26
|
+
if (!graph.hasNode(base))
|
|
27
|
+
return base;
|
|
28
|
+
let n = 2;
|
|
29
|
+
while (graph.hasNode(`${base}::${n}`))
|
|
30
|
+
n++;
|
|
31
|
+
return `${base}::${n}`;
|
|
32
|
+
}
|