@prih/mcp-graph-memory 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +512 -0
  3. package/dist/api/index.js +473 -0
  4. package/dist/api/rest/code.js +78 -0
  5. package/dist/api/rest/docs.js +80 -0
  6. package/dist/api/rest/files.js +64 -0
  7. package/dist/api/rest/graph.js +56 -0
  8. package/dist/api/rest/index.js +117 -0
  9. package/dist/api/rest/knowledge.js +238 -0
  10. package/dist/api/rest/skills.js +284 -0
  11. package/dist/api/rest/tasks.js +272 -0
  12. package/dist/api/rest/tools.js +126 -0
  13. package/dist/api/rest/validation.js +191 -0
  14. package/dist/api/rest/websocket.js +65 -0
  15. package/dist/api/tools/code/get-file-symbols.js +30 -0
  16. package/dist/api/tools/code/get-symbol.js +22 -0
  17. package/dist/api/tools/code/list-files.js +18 -0
  18. package/dist/api/tools/code/search-code.js +27 -0
  19. package/dist/api/tools/code/search-files.js +22 -0
  20. package/dist/api/tools/context/get-context.js +19 -0
  21. package/dist/api/tools/docs/cross-references.js +76 -0
  22. package/dist/api/tools/docs/explain-symbol.js +55 -0
  23. package/dist/api/tools/docs/find-examples.js +52 -0
  24. package/dist/api/tools/docs/get-node.js +24 -0
  25. package/dist/api/tools/docs/get-toc.js +22 -0
  26. package/dist/api/tools/docs/list-snippets.js +46 -0
  27. package/dist/api/tools/docs/list-topics.js +18 -0
  28. package/dist/api/tools/docs/search-files.js +22 -0
  29. package/dist/api/tools/docs/search-snippets.js +43 -0
  30. package/dist/api/tools/docs/search.js +27 -0
  31. package/dist/api/tools/file-index/get-file-info.js +21 -0
  32. package/dist/api/tools/file-index/list-all-files.js +28 -0
  33. package/dist/api/tools/file-index/search-all-files.js +24 -0
  34. package/dist/api/tools/knowledge/add-attachment.js +31 -0
  35. package/dist/api/tools/knowledge/create-note.js +20 -0
  36. package/dist/api/tools/knowledge/create-relation.js +29 -0
  37. package/dist/api/tools/knowledge/delete-note.js +19 -0
  38. package/dist/api/tools/knowledge/delete-relation.js +23 -0
  39. package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
  40. package/dist/api/tools/knowledge/get-note.js +20 -0
  41. package/dist/api/tools/knowledge/list-notes.js +18 -0
  42. package/dist/api/tools/knowledge/list-relations.js +17 -0
  43. package/dist/api/tools/knowledge/remove-attachment.js +19 -0
  44. package/dist/api/tools/knowledge/search-notes.js +25 -0
  45. package/dist/api/tools/knowledge/update-note.js +34 -0
  46. package/dist/api/tools/skills/add-attachment.js +31 -0
  47. package/dist/api/tools/skills/bump-usage.js +19 -0
  48. package/dist/api/tools/skills/create-skill-link.js +25 -0
  49. package/dist/api/tools/skills/create-skill.js +26 -0
  50. package/dist/api/tools/skills/delete-skill-link.js +23 -0
  51. package/dist/api/tools/skills/delete-skill.js +20 -0
  52. package/dist/api/tools/skills/find-linked-skills.js +25 -0
  53. package/dist/api/tools/skills/get-skill.js +21 -0
  54. package/dist/api/tools/skills/link-skill.js +23 -0
  55. package/dist/api/tools/skills/list-skills.js +20 -0
  56. package/dist/api/tools/skills/recall-skills.js +18 -0
  57. package/dist/api/tools/skills/remove-attachment.js +19 -0
  58. package/dist/api/tools/skills/search-skills.js +25 -0
  59. package/dist/api/tools/skills/update-skill.js +58 -0
  60. package/dist/api/tools/tasks/add-attachment.js +31 -0
  61. package/dist/api/tools/tasks/create-task-link.js +25 -0
  62. package/dist/api/tools/tasks/create-task.js +25 -0
  63. package/dist/api/tools/tasks/delete-task-link.js +23 -0
  64. package/dist/api/tools/tasks/delete-task.js +20 -0
  65. package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
  66. package/dist/api/tools/tasks/get-task.js +20 -0
  67. package/dist/api/tools/tasks/link-task.js +23 -0
  68. package/dist/api/tools/tasks/list-tasks.js +24 -0
  69. package/dist/api/tools/tasks/move-task.js +38 -0
  70. package/dist/api/tools/tasks/remove-attachment.js +19 -0
  71. package/dist/api/tools/tasks/search-tasks.js +25 -0
  72. package/dist/api/tools/tasks/update-task.js +55 -0
  73. package/dist/cli/index.js +451 -0
  74. package/dist/cli/indexer.js +277 -0
  75. package/dist/graphs/attachment-types.js +74 -0
  76. package/dist/graphs/code-types.js +10 -0
  77. package/dist/graphs/code.js +172 -0
  78. package/dist/graphs/docs.js +198 -0
  79. package/dist/graphs/file-index-types.js +10 -0
  80. package/dist/graphs/file-index.js +310 -0
  81. package/dist/graphs/file-lang.js +119 -0
  82. package/dist/graphs/knowledge-types.js +32 -0
  83. package/dist/graphs/knowledge.js +764 -0
  84. package/dist/graphs/manager-types.js +87 -0
  85. package/dist/graphs/skill-types.js +10 -0
  86. package/dist/graphs/skill.js +1013 -0
  87. package/dist/graphs/task-types.js +17 -0
  88. package/dist/graphs/task.js +960 -0
  89. package/dist/lib/embedder.js +101 -0
  90. package/dist/lib/events-log.js +400 -0
  91. package/dist/lib/file-import.js +327 -0
  92. package/dist/lib/file-mirror.js +446 -0
  93. package/dist/lib/frontmatter.js +17 -0
  94. package/dist/lib/mirror-watcher.js +637 -0
  95. package/dist/lib/multi-config.js +254 -0
  96. package/dist/lib/parsers/code.js +246 -0
  97. package/dist/lib/parsers/codeblock.js +66 -0
  98. package/dist/lib/parsers/docs.js +196 -0
  99. package/dist/lib/project-manager.js +418 -0
  100. package/dist/lib/promise-queue.js +22 -0
  101. package/dist/lib/search/bm25.js +167 -0
  102. package/dist/lib/search/code.js +103 -0
  103. package/dist/lib/search/docs.js +108 -0
  104. package/dist/lib/search/file-index.js +31 -0
  105. package/dist/lib/search/files.js +61 -0
  106. package/dist/lib/search/knowledge.js +101 -0
  107. package/dist/lib/search/skills.js +104 -0
  108. package/dist/lib/search/tasks.js +103 -0
  109. package/dist/lib/watcher.js +67 -0
  110. package/package.json +83 -0
  111. package/ui/README.md +54 -0
@@ -0,0 +1,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
+ }