@graphmemory/server 1.1.0 → 1.3.0
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 +84 -12
- package/README.md +66 -101
- package/dist/api/index.js +279 -169
- package/dist/api/rest/index.js +36 -16
- package/dist/api/rest/tools.js +8 -1
- package/dist/api/rest/websocket.js +22 -1
- package/dist/api/tools/code/search-code.js +12 -9
- package/dist/api/tools/code/search-files.js +1 -1
- package/dist/api/tools/docs/cross-references.js +3 -2
- package/dist/api/tools/docs/explain-symbol.js +2 -1
- package/dist/api/tools/docs/find-examples.js +2 -1
- package/dist/api/tools/docs/search-files.js +1 -1
- package/dist/api/tools/docs/search-snippets.js +1 -1
- package/dist/api/tools/docs/search.js +5 -4
- package/dist/api/tools/file-index/search-all-files.js +1 -1
- package/dist/api/tools/knowledge/add-attachment.js +14 -3
- package/dist/api/tools/knowledge/create-relation.js +2 -2
- package/dist/api/tools/knowledge/delete-relation.js +2 -2
- package/dist/api/tools/knowledge/find-linked-notes.js +1 -1
- package/dist/api/tools/knowledge/remove-attachment.js +5 -1
- package/dist/api/tools/knowledge/search-notes.js +5 -4
- package/dist/api/tools/skills/add-attachment.js +14 -3
- package/dist/api/tools/skills/recall-skills.js +1 -1
- package/dist/api/tools/skills/remove-attachment.js +5 -1
- package/dist/api/tools/skills/search-skills.js +6 -5
- package/dist/api/tools/tasks/add-attachment.js +14 -3
- package/dist/api/tools/tasks/create-task-link.js +1 -1
- package/dist/api/tools/tasks/delete-task-link.js +1 -1
- package/dist/api/tools/tasks/find-linked-tasks.js +1 -1
- package/dist/api/tools/tasks/remove-attachment.js +5 -1
- package/dist/api/tools/tasks/search-tasks.js +5 -4
- package/dist/cli/index.js +69 -311
- package/dist/cli/indexer.js +61 -29
- package/dist/graphs/code.js +70 -7
- package/dist/graphs/docs.js +15 -2
- package/dist/graphs/file-index.js +20 -6
- package/dist/graphs/file-lang.js +1 -1
- package/dist/graphs/knowledge.js +20 -3
- package/dist/graphs/manager-types.js +1 -1
- package/dist/graphs/skill.js +23 -4
- package/dist/graphs/task.js +23 -4
- package/dist/lib/embedding-codec.js +65 -0
- package/dist/lib/file-mirror.js +7 -7
- package/dist/lib/frontmatter.js +3 -2
- package/dist/lib/jwt.js +4 -4
- package/dist/lib/mirror-watcher.js +5 -4
- package/dist/lib/multi-config.js +60 -1
- package/dist/lib/parsers/code.js +158 -31
- package/dist/lib/parsers/codeblock.js +11 -6
- package/dist/lib/parsers/docs.js +59 -31
- package/dist/lib/parsers/languages/registry.js +10 -4
- package/dist/lib/parsers/languages/typescript.js +195 -48
- package/dist/lib/project-manager.js +14 -10
- package/dist/lib/search/bm25.js +18 -1
- package/dist/lib/search/code.js +12 -3
- package/dist/lib/watcher.js +17 -9
- package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
- package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
- package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
- package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
- package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
- package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
- package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
- package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
- package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
- package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
- package/dist/ui/assets/api-BMnBjMMf.js +1 -0
- package/dist/ui/assets/api-BlFF6gX-.js +1 -0
- package/dist/ui/assets/api-CrGJOcaN.js +1 -0
- package/dist/ui/assets/api-DuX-0a_X.js +1 -0
- package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
- package/dist/ui/assets/client-Bq88u7gN.js +1 -0
- package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
- package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
- package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
- package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
- package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
- package/dist/ui/assets/files-0bPg6NH9.js +1 -0
- package/dist/ui/assets/graph-DXGud_wF.js +1 -0
- package/dist/ui/assets/help-CEMQqZUR.js +891 -0
- package/dist/ui/assets/help-DJ52_fxN.js +1 -0
- package/dist/ui/assets/index-BCZDAYZi.js +2 -0
- package/dist/ui/assets/index-D6zSNtzo.css +1 -0
- package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
- package/dist/ui/assets/new-CpD7hOBA.js +1 -0
- package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
- package/dist/ui/assets/new-s8c0M75X.js +1 -0
- package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
- package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
- package/dist/ui/assets/search-EpJhdP2a.js +1 -0
- package/dist/ui/assets/skill-y9pizyqE.js +1 -0
- package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
- package/dist/ui/assets/tasks-CobouTKV.js +1 -0
- package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
- package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
- package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
- package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
- package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
- package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
- package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
- package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
- package/dist/ui/index.html +11 -3
- package/package.json +2 -2
- package/dist/ui/assets/index-D6oxrVF7.js +0 -1759
package/dist/graphs/code.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.CodeGraphManager = exports.createCodeGraph = void 0;
|
|
7
7
|
exports.updateCodeFile = updateCodeFile;
|
|
8
8
|
exports.resolvePendingImports = resolvePendingImports;
|
|
9
|
+
exports.resolvePendingEdges = resolvePendingEdges;
|
|
9
10
|
exports.removeCodeFile = removeCodeFile;
|
|
10
11
|
exports.getFileSymbols = getFileSymbols;
|
|
11
12
|
exports.getCodeFileMtime = getCodeFileMtime;
|
|
@@ -20,6 +21,7 @@ const manager_types_1 = require("../graphs/manager-types");
|
|
|
20
21
|
const code_1 = require("../lib/search/code");
|
|
21
22
|
const files_1 = require("../lib/search/files");
|
|
22
23
|
const bm25_1 = require("../lib/search/bm25");
|
|
24
|
+
const embedding_codec_1 = require("../lib/embedding-codec");
|
|
23
25
|
// ---------------------------------------------------------------------------
|
|
24
26
|
// CRUD
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
@@ -30,19 +32,31 @@ function updateCodeFile(graph, parsed) {
|
|
|
30
32
|
graph.addNode(id, attrs);
|
|
31
33
|
}
|
|
32
34
|
const pendingImports = [];
|
|
35
|
+
const pendingEdges = [];
|
|
33
36
|
for (const { from, to, attrs } of parsed.edges) {
|
|
34
37
|
if (!graph.hasNode(to)) {
|
|
35
|
-
if (attrs.kind === 'imports')
|
|
38
|
+
if (attrs.kind === 'imports') {
|
|
36
39
|
pendingImports.push(to);
|
|
40
|
+
}
|
|
41
|
+
else if (attrs.kind === 'extends' || attrs.kind === 'implements') {
|
|
42
|
+
// Target class/interface may be in another file — defer resolution
|
|
43
|
+
const toName = to.split('::').pop();
|
|
44
|
+
pendingEdges.push({ from, toName, kind: attrs.kind });
|
|
45
|
+
}
|
|
37
46
|
continue;
|
|
38
47
|
}
|
|
39
48
|
if (graph.hasNode(from) && !graph.hasEdge(from, to)) {
|
|
40
49
|
graph.addEdgeWithKey(`${from}→${to}`, from, to, attrs);
|
|
41
50
|
}
|
|
42
51
|
}
|
|
43
|
-
// Store pending
|
|
44
|
-
if (
|
|
45
|
-
|
|
52
|
+
// Store pending data on the file node for post-drain resolution
|
|
53
|
+
if (graph.hasNode(parsed.fileId)) {
|
|
54
|
+
if (pendingImports.length > 0) {
|
|
55
|
+
graph.setNodeAttribute(parsed.fileId, 'pendingImports', pendingImports);
|
|
56
|
+
}
|
|
57
|
+
if (pendingEdges.length > 0) {
|
|
58
|
+
graph.setNodeAttribute(parsed.fileId, 'pendingEdges', pendingEdges);
|
|
59
|
+
}
|
|
46
60
|
}
|
|
47
61
|
}
|
|
48
62
|
/**
|
|
@@ -71,6 +85,43 @@ function resolvePendingImports(graph) {
|
|
|
71
85
|
});
|
|
72
86
|
return created;
|
|
73
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve pending extends/implements edges after all files have been indexed.
|
|
90
|
+
* Searches the entire graph for nodes matching toName (by symbol name).
|
|
91
|
+
*/
|
|
92
|
+
function resolvePendingEdges(graph) {
|
|
93
|
+
// Build name → nodeId index for fast lookup
|
|
94
|
+
const nameIndex = new Map();
|
|
95
|
+
graph.forEachNode((id, attrs) => {
|
|
96
|
+
if (attrs.kind === 'class' || attrs.kind === 'interface') {
|
|
97
|
+
const list = nameIndex.get(attrs.name) ?? [];
|
|
98
|
+
list.push(id);
|
|
99
|
+
nameIndex.set(attrs.name, list);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
let created = 0;
|
|
103
|
+
graph.forEachNode((id, attrs) => {
|
|
104
|
+
if (!attrs.pendingEdges || attrs.pendingEdges.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
const remaining = [];
|
|
107
|
+
for (const edge of attrs.pendingEdges) {
|
|
108
|
+
const candidates = nameIndex.get(edge.toName);
|
|
109
|
+
if (candidates && candidates.length > 0 && graph.hasNode(edge.from)) {
|
|
110
|
+
// Use first match (ambiguity is rare in practice)
|
|
111
|
+
const toId = candidates[0];
|
|
112
|
+
if (toId !== edge.from && !graph.hasEdge(edge.from, toId)) {
|
|
113
|
+
graph.addEdgeWithKey(`${edge.from}→${toId}`, edge.from, toId, { kind: edge.kind });
|
|
114
|
+
created++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
remaining.push(edge);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
graph.setNodeAttribute(id, 'pendingEdges', remaining.length > 0 ? remaining : undefined);
|
|
122
|
+
});
|
|
123
|
+
return created;
|
|
124
|
+
}
|
|
74
125
|
/** Remove all nodes (and their incident edges) belonging to a file. */
|
|
75
126
|
function removeCodeFile(graph, fileId) {
|
|
76
127
|
const toRemove = graph.filterNodes((_, attrs) => attrs.fileId === fileId);
|
|
@@ -112,8 +163,19 @@ function saveCodeGraph(graph, graphMemory, embeddingFingerprint) {
|
|
|
112
163
|
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
113
164
|
const file = path_1.default.join(graphMemory, 'code.json');
|
|
114
165
|
const tmp = file + '.tmp';
|
|
115
|
-
|
|
116
|
-
|
|
166
|
+
try {
|
|
167
|
+
const exported = graph.export();
|
|
168
|
+
(0, embedding_codec_1.compressEmbeddings)(exported);
|
|
169
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
|
|
170
|
+
fs_1.default.renameSync(tmp, file);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
try {
|
|
174
|
+
fs_1.default.unlinkSync(tmp);
|
|
175
|
+
}
|
|
176
|
+
catch { /* ignore cleanup error */ }
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
117
179
|
}
|
|
118
180
|
function loadCodeGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
119
181
|
const graph = (0, code_types_1.createCodeGraph)();
|
|
@@ -129,6 +191,7 @@ function loadCodeGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
|
129
191
|
process.stderr.write(`[code-graph] Embedding config changed, re-indexing code graph\n`);
|
|
130
192
|
return graph;
|
|
131
193
|
}
|
|
194
|
+
(0, embedding_codec_1.decompressEmbeddings)(data.graph);
|
|
132
195
|
graph.import(data.graph);
|
|
133
196
|
process.stderr.write(`[code-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
134
197
|
}
|
|
@@ -144,7 +207,7 @@ class CodeGraphManager {
|
|
|
144
207
|
_graph;
|
|
145
208
|
embedFns;
|
|
146
209
|
ext;
|
|
147
|
-
_bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment}`);
|
|
210
|
+
_bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment} ${attrs.body}`);
|
|
148
211
|
constructor(_graph, embedFns, ext = {}) {
|
|
149
212
|
this._graph = _graph;
|
|
150
213
|
this.embedFns = embedFns;
|
package/dist/graphs/docs.js
CHANGED
|
@@ -20,6 +20,7 @@ const manager_types_1 = require("../graphs/manager-types");
|
|
|
20
20
|
const docs_1 = require("../lib/search/docs");
|
|
21
21
|
const files_1 = require("../lib/search/files");
|
|
22
22
|
const bm25_1 = require("../lib/search/bm25");
|
|
23
|
+
const embedding_codec_1 = require("../lib/embedding-codec");
|
|
23
24
|
function createGraph() {
|
|
24
25
|
return new graphology_1.DirectedGraph({ multi: false, allowSelfLoops: false });
|
|
25
26
|
}
|
|
@@ -133,8 +134,19 @@ function saveGraph(graph, graphMemory, embeddingFingerprint) {
|
|
|
133
134
|
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
134
135
|
const file = path_1.default.join(graphMemory, 'docs.json');
|
|
135
136
|
const tmp = file + '.tmp';
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
try {
|
|
138
|
+
const exported = graph.export();
|
|
139
|
+
(0, embedding_codec_1.compressEmbeddings)(exported);
|
|
140
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
|
|
141
|
+
fs_1.default.renameSync(tmp, file);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
try {
|
|
145
|
+
fs_1.default.unlinkSync(tmp);
|
|
146
|
+
}
|
|
147
|
+
catch { /* ignore cleanup error */ }
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
138
150
|
}
|
|
139
151
|
function loadGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
140
152
|
const graph = createGraph();
|
|
@@ -150,6 +162,7 @@ function loadGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
|
150
162
|
process.stderr.write(`[graph] Embedding config changed, re-indexing docs graph\n`);
|
|
151
163
|
return graph;
|
|
152
164
|
}
|
|
165
|
+
(0, embedding_codec_1.decompressEmbeddings)(data.graph);
|
|
153
166
|
graph.import(data.graph);
|
|
154
167
|
process.stderr.write(`[graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
155
168
|
}
|
|
@@ -19,6 +19,7 @@ const file_index_types_1 = require("../graphs/file-index-types");
|
|
|
19
19
|
const file_lang_1 = require("../graphs/file-lang");
|
|
20
20
|
const manager_types_1 = require("../graphs/manager-types");
|
|
21
21
|
const file_index_1 = require("../lib/search/file-index");
|
|
22
|
+
const embedding_codec_1 = require("../lib/embedding-codec");
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// CRUD
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
@@ -121,6 +122,9 @@ function cleanEmptyDirs(graph, dir) {
|
|
|
121
122
|
return;
|
|
122
123
|
if (graph.getNodeAttribute(dir, 'kind') !== 'directory')
|
|
123
124
|
return;
|
|
125
|
+
// Never remove root
|
|
126
|
+
if (dir === '.')
|
|
127
|
+
return;
|
|
124
128
|
// Count outgoing `contains` edges
|
|
125
129
|
const children = graph.outDegree(dir);
|
|
126
130
|
if (children > 0)
|
|
@@ -128,9 +132,6 @@ function cleanEmptyDirs(graph, dir) {
|
|
|
128
132
|
// No children — remove this directory
|
|
129
133
|
const parent = graph.getNodeAttribute(dir, 'directory');
|
|
130
134
|
graph.dropNode(dir);
|
|
131
|
-
// Don't remove root
|
|
132
|
-
if (dir === '.')
|
|
133
|
-
return;
|
|
134
135
|
cleanEmptyDirs(graph, parent);
|
|
135
136
|
}
|
|
136
137
|
/**
|
|
@@ -157,7 +158,8 @@ function listAllFiles(graph, options = {}) {
|
|
|
157
158
|
return [];
|
|
158
159
|
graph.forEachOutNeighbor(dirId, (childId) => {
|
|
159
160
|
const attrs = graph.getNodeAttributes(childId);
|
|
160
|
-
|
|
161
|
+
const edgeKey = graph.edge(dirId, childId);
|
|
162
|
+
if (!edgeKey || graph.getEdgeAttribute(edgeKey, 'kind') !== 'contains')
|
|
161
163
|
return;
|
|
162
164
|
if (extension && attrs.extension !== extension)
|
|
163
165
|
return;
|
|
@@ -240,8 +242,19 @@ function saveFileIndexGraph(graph, graphMemory, embeddingFingerprint) {
|
|
|
240
242
|
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
241
243
|
const file = path_1.default.join(graphMemory, 'file-index.json');
|
|
242
244
|
const tmp = file + '.tmp';
|
|
243
|
-
|
|
244
|
-
|
|
245
|
+
try {
|
|
246
|
+
const exported = graph.export();
|
|
247
|
+
(0, embedding_codec_1.compressEmbeddings)(exported);
|
|
248
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
|
|
249
|
+
fs_1.default.renameSync(tmp, file);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
try {
|
|
253
|
+
fs_1.default.unlinkSync(tmp);
|
|
254
|
+
}
|
|
255
|
+
catch { /* ignore cleanup error */ }
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
245
258
|
}
|
|
246
259
|
function loadFileIndexGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
247
260
|
const graph = (0, file_index_types_1.createFileIndexGraph)();
|
|
@@ -257,6 +270,7 @@ function loadFileIndexGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
|
257
270
|
process.stderr.write(`[file-index] Embedding config changed, re-indexing file index\n`);
|
|
258
271
|
return graph;
|
|
259
272
|
}
|
|
273
|
+
(0, embedding_codec_1.decompressEmbeddings)(data.graph);
|
|
260
274
|
graph.import(data.graph);
|
|
261
275
|
process.stderr.write(`[file-index] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
262
276
|
}
|
package/dist/graphs/file-lang.js
CHANGED
package/dist/graphs/knowledge.js
CHANGED
|
@@ -28,6 +28,7 @@ const manager_types_1 = require("../graphs/manager-types");
|
|
|
28
28
|
const knowledge_1 = require("../lib/search/knowledge");
|
|
29
29
|
const bm25_1 = require("../lib/search/bm25");
|
|
30
30
|
const file_mirror_1 = require("../lib/file-mirror");
|
|
31
|
+
const embedding_codec_1 = require("../lib/embedding-codec");
|
|
31
32
|
const attachment_types_1 = require("../graphs/attachment-types");
|
|
32
33
|
const file_import_1 = require("../lib/file-import");
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
@@ -329,8 +330,19 @@ function saveKnowledgeGraph(graph, graphMemory, embeddingFingerprint) {
|
|
|
329
330
|
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
330
331
|
const file = path_1.default.join(graphMemory, 'knowledge.json');
|
|
331
332
|
const tmp = file + '.tmp';
|
|
332
|
-
|
|
333
|
-
|
|
333
|
+
try {
|
|
334
|
+
const exported = graph.export();
|
|
335
|
+
(0, embedding_codec_1.compressEmbeddings)(exported);
|
|
336
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
|
|
337
|
+
fs_1.default.renameSync(tmp, file);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
try {
|
|
341
|
+
fs_1.default.unlinkSync(tmp);
|
|
342
|
+
}
|
|
343
|
+
catch { /* ignore cleanup error */ }
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
334
346
|
}
|
|
335
347
|
function loadKnowledgeGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
336
348
|
const graph = (0, knowledge_types_1.createKnowledgeGraph)();
|
|
@@ -346,6 +358,7 @@ function loadKnowledgeGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
|
346
358
|
process.stderr.write(`[knowledge-graph] Embedding config changed, re-indexing knowledge graph\n`);
|
|
347
359
|
return graph;
|
|
348
360
|
}
|
|
361
|
+
(0, embedding_codec_1.decompressEmbeddings)(data.graph);
|
|
349
362
|
graph.import(data.graph);
|
|
350
363
|
process.stderr.write(`[knowledge-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
351
364
|
}
|
|
@@ -552,7 +565,9 @@ class KnowledgeGraphManager {
|
|
|
552
565
|
try {
|
|
553
566
|
const actualToId = targetGraph ? proxyId(targetGraph, toId, pid) : toId;
|
|
554
567
|
if (this._graph.hasEdge(fromId, actualToId)) {
|
|
555
|
-
|
|
568
|
+
const ek = this._graph.edge(fromId, actualToId);
|
|
569
|
+
if (ek)
|
|
570
|
+
kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
|
|
556
571
|
}
|
|
557
572
|
}
|
|
558
573
|
catch { /* ignore */ }
|
|
@@ -628,6 +643,8 @@ class KnowledgeGraphManager {
|
|
|
628
643
|
return;
|
|
629
644
|
const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, noteId));
|
|
630
645
|
this._graph.setNodeAttribute(noteId, 'attachments', attachments);
|
|
646
|
+
this._graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
|
|
647
|
+
this._graph.setNodeAttribute(noteId, 'version', (this._graph.getNodeAttribute(noteId, 'version') ?? 0) + 1);
|
|
631
648
|
this.ctx.markDirty();
|
|
632
649
|
}
|
|
633
650
|
listAttachments(noteId) {
|
|
@@ -16,7 +16,7 @@ class VersionConflictError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
exports.VersionConflictError = VersionConflictError;
|
|
19
|
-
/** No-op context for tests
|
|
19
|
+
/** No-op context for tests. */
|
|
20
20
|
function noopContext(projectId = '') {
|
|
21
21
|
return {
|
|
22
22
|
markDirty: () => { },
|
package/dist/graphs/skill.js
CHANGED
|
@@ -30,6 +30,7 @@ const manager_types_1 = require("../graphs/manager-types");
|
|
|
30
30
|
const skills_1 = require("../lib/search/skills");
|
|
31
31
|
const bm25_1 = require("../lib/search/bm25");
|
|
32
32
|
const file_mirror_1 = require("../lib/file-mirror");
|
|
33
|
+
const embedding_codec_1 = require("../lib/embedding-codec");
|
|
33
34
|
const attachment_types_1 = require("../graphs/attachment-types");
|
|
34
35
|
const file_import_1 = require("../lib/file-import");
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
@@ -473,8 +474,19 @@ function saveSkillGraph(graph, graphMemory, embeddingFingerprint) {
|
|
|
473
474
|
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
474
475
|
const file = path_1.default.join(graphMemory, 'skills.json');
|
|
475
476
|
const tmp = file + '.tmp';
|
|
476
|
-
|
|
477
|
-
|
|
477
|
+
try {
|
|
478
|
+
const exported = graph.export();
|
|
479
|
+
(0, embedding_codec_1.compressEmbeddings)(exported);
|
|
480
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
|
|
481
|
+
fs_1.default.renameSync(tmp, file);
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
try {
|
|
485
|
+
fs_1.default.unlinkSync(tmp);
|
|
486
|
+
}
|
|
487
|
+
catch { /* ignore cleanup error */ }
|
|
488
|
+
throw err;
|
|
489
|
+
}
|
|
478
490
|
}
|
|
479
491
|
function loadSkillGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
480
492
|
const graph = (0, skill_types_1.createSkillGraph)();
|
|
@@ -490,6 +502,7 @@ function loadSkillGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
|
490
502
|
process.stderr.write(`[skill-graph] Embedding config changed, re-indexing skill graph\n`);
|
|
491
503
|
return graph;
|
|
492
504
|
}
|
|
505
|
+
(0, embedding_codec_1.decompressEmbeddings)(data.graph);
|
|
493
506
|
graph.import(data.graph);
|
|
494
507
|
process.stderr.write(`[skill-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
495
508
|
}
|
|
@@ -760,7 +773,9 @@ class SkillGraphManager {
|
|
|
760
773
|
try {
|
|
761
774
|
const proxyNodeId = proxyId(targetGraph, targetId, pid);
|
|
762
775
|
if (this._graph.hasEdge(skillId, proxyNodeId)) {
|
|
763
|
-
|
|
776
|
+
const ek = this._graph.edge(skillId, proxyNodeId);
|
|
777
|
+
if (ek)
|
|
778
|
+
kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
|
|
764
779
|
}
|
|
765
780
|
}
|
|
766
781
|
catch { /* ignore */ }
|
|
@@ -788,7 +803,9 @@ class SkillGraphManager {
|
|
|
788
803
|
let kind = '';
|
|
789
804
|
try {
|
|
790
805
|
if (this._graph.hasEdge(fromId, toId)) {
|
|
791
|
-
|
|
806
|
+
const ek = this._graph.edge(fromId, toId);
|
|
807
|
+
if (ek)
|
|
808
|
+
kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
|
|
792
809
|
}
|
|
793
810
|
}
|
|
794
811
|
catch { /* ignore */ }
|
|
@@ -856,6 +873,8 @@ class SkillGraphManager {
|
|
|
856
873
|
return;
|
|
857
874
|
const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, skillId));
|
|
858
875
|
this._graph.setNodeAttribute(skillId, 'attachments', attachments);
|
|
876
|
+
this._graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
|
|
877
|
+
this._graph.setNodeAttribute(skillId, 'version', (this._graph.getNodeAttribute(skillId, 'version') ?? 0) + 1);
|
|
859
878
|
this.ctx.markDirty();
|
|
860
879
|
}
|
|
861
880
|
listAttachments(skillId) {
|
package/dist/graphs/task.js
CHANGED
|
@@ -30,6 +30,7 @@ const manager_types_1 = require("../graphs/manager-types");
|
|
|
30
30
|
const tasks_1 = require("../lib/search/tasks");
|
|
31
31
|
const bm25_1 = require("../lib/search/bm25");
|
|
32
32
|
const file_mirror_1 = require("../lib/file-mirror");
|
|
33
|
+
const embedding_codec_1 = require("../lib/embedding-codec");
|
|
33
34
|
const attachment_types_1 = require("../graphs/attachment-types");
|
|
34
35
|
const file_import_1 = require("../lib/file-import");
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
@@ -494,8 +495,19 @@ function saveTaskGraph(graph, graphMemory, embeddingFingerprint) {
|
|
|
494
495
|
fs_1.default.mkdirSync(graphMemory, { recursive: true });
|
|
495
496
|
const file = path_1.default.join(graphMemory, 'tasks.json');
|
|
496
497
|
const tmp = file + '.tmp';
|
|
497
|
-
|
|
498
|
-
|
|
498
|
+
try {
|
|
499
|
+
const exported = graph.export();
|
|
500
|
+
(0, embedding_codec_1.compressEmbeddings)(exported);
|
|
501
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
|
|
502
|
+
fs_1.default.renameSync(tmp, file);
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
try {
|
|
506
|
+
fs_1.default.unlinkSync(tmp);
|
|
507
|
+
}
|
|
508
|
+
catch { /* ignore cleanup error */ }
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
499
511
|
}
|
|
500
512
|
function loadTaskGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
501
513
|
const graph = (0, task_types_1.createTaskGraph)();
|
|
@@ -511,6 +523,7 @@ function loadTaskGraph(graphMemory, fresh = false, embeddingFingerprint) {
|
|
|
511
523
|
process.stderr.write(`[task-graph] Embedding config changed, re-indexing task graph\n`);
|
|
512
524
|
return graph;
|
|
513
525
|
}
|
|
526
|
+
(0, embedding_codec_1.decompressEmbeddings)(data.graph);
|
|
514
527
|
graph.import(data.graph);
|
|
515
528
|
process.stderr.write(`[task-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
|
|
516
529
|
}
|
|
@@ -733,7 +746,9 @@ class TaskGraphManager {
|
|
|
733
746
|
try {
|
|
734
747
|
const proxyNodeId = proxyId(targetGraph, targetId, pid);
|
|
735
748
|
if (this._graph.hasEdge(taskId, proxyNodeId)) {
|
|
736
|
-
|
|
749
|
+
const ek = this._graph.edge(taskId, proxyNodeId);
|
|
750
|
+
if (ek)
|
|
751
|
+
kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
|
|
737
752
|
}
|
|
738
753
|
}
|
|
739
754
|
catch { /* ignore */ }
|
|
@@ -759,7 +774,9 @@ class TaskGraphManager {
|
|
|
759
774
|
let kind = '';
|
|
760
775
|
try {
|
|
761
776
|
if (this._graph.hasEdge(fromId, toId)) {
|
|
762
|
-
|
|
777
|
+
const ek = this._graph.edge(fromId, toId);
|
|
778
|
+
if (ek)
|
|
779
|
+
kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
|
|
763
780
|
}
|
|
764
781
|
}
|
|
765
782
|
catch { /* ignore */ }
|
|
@@ -827,6 +844,8 @@ class TaskGraphManager {
|
|
|
827
844
|
return;
|
|
828
845
|
const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, taskId));
|
|
829
846
|
this._graph.setNodeAttribute(taskId, 'attachments', attachments);
|
|
847
|
+
this._graph.setNodeAttribute(taskId, 'updatedAt', Date.now());
|
|
848
|
+
this._graph.setNodeAttribute(taskId, 'version', (this._graph.getNodeAttribute(taskId, 'version') ?? 0) + 1);
|
|
830
849
|
this.ctx.markDirty();
|
|
831
850
|
}
|
|
832
851
|
listAttachments(taskId) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Encode/decode embedding vectors as Base64 for compact JSON serialization.
|
|
4
|
+
* Float32Array → Base64 string saves ~3x vs JSON number arrays.
|
|
5
|
+
* Backwards compatible: detects old format (number[]) on load.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.compressEmbeddings = compressEmbeddings;
|
|
9
|
+
exports.decompressEmbeddings = decompressEmbeddings;
|
|
10
|
+
const EMBEDDING_FIELDS = ['embedding', 'fileEmbedding'];
|
|
11
|
+
/** Convert a number[] to a Base64-encoded Float32Array. */
|
|
12
|
+
function float32ToBase64(arr) {
|
|
13
|
+
const f32 = new Float32Array(arr);
|
|
14
|
+
const bytes = new Uint8Array(f32.buffer);
|
|
15
|
+
let binary = '';
|
|
16
|
+
for (let i = 0; i < bytes.length; i++)
|
|
17
|
+
binary += String.fromCharCode(bytes[i]);
|
|
18
|
+
return Buffer.from(binary, 'binary').toString('base64');
|
|
19
|
+
}
|
|
20
|
+
/** Convert a Base64-encoded Float32Array back to number[]. */
|
|
21
|
+
function base64ToFloat32(b64) {
|
|
22
|
+
const buf = Buffer.from(b64, 'base64');
|
|
23
|
+
// Copy to aligned buffer to guarantee 4-byte alignment for Float32Array
|
|
24
|
+
const aligned = new Uint8Array(buf.byteLength);
|
|
25
|
+
aligned.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
26
|
+
const f32 = new Float32Array(aligned.buffer, 0, aligned.byteLength / 4);
|
|
27
|
+
return Array.from(f32);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compress embedding fields in a graphology export object (mutates in place).
|
|
31
|
+
* Converts number[] → Base64 string for fields named 'embedding' or 'fileEmbedding'.
|
|
32
|
+
*/
|
|
33
|
+
function compressEmbeddings(exported) {
|
|
34
|
+
if (!exported?.nodes)
|
|
35
|
+
return;
|
|
36
|
+
for (const node of exported.nodes) {
|
|
37
|
+
const attrs = node.attributes;
|
|
38
|
+
if (!attrs)
|
|
39
|
+
continue;
|
|
40
|
+
for (const field of EMBEDDING_FIELDS) {
|
|
41
|
+
if (Array.isArray(attrs[field]) && attrs[field].length > 0) {
|
|
42
|
+
attrs[field] = float32ToBase64(attrs[field]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decompress embedding fields in a graphology export object (mutates in place).
|
|
49
|
+
* Converts Base64 string → number[]. Handles both old format (number[]) and new (string).
|
|
50
|
+
*/
|
|
51
|
+
function decompressEmbeddings(exported) {
|
|
52
|
+
if (!exported?.nodes)
|
|
53
|
+
return;
|
|
54
|
+
for (const node of exported.nodes) {
|
|
55
|
+
const attrs = node.attributes;
|
|
56
|
+
if (!attrs)
|
|
57
|
+
continue;
|
|
58
|
+
for (const field of EMBEDDING_FIELDS) {
|
|
59
|
+
if (typeof attrs[field] === 'string' && attrs[field].length > 0) {
|
|
60
|
+
attrs[field] = base64ToFloat32(attrs[field]);
|
|
61
|
+
}
|
|
62
|
+
// number[] stays as-is (backwards compatible with old format)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/lib/file-mirror.js
CHANGED
|
@@ -422,14 +422,14 @@ function deleteMirrorDir(dir, id) {
|
|
|
422
422
|
// ---------------------------------------------------------------------------
|
|
423
423
|
// Attachment file helpers (paths now go through attachments/ subdir)
|
|
424
424
|
// ---------------------------------------------------------------------------
|
|
425
|
-
/** Sanitize a filename: strip
|
|
425
|
+
/** Sanitize a filename: extract basename, strip null bytes and path traversal. */
|
|
426
426
|
function sanitizeFilename(name) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return
|
|
427
|
+
// Normalize backslashes to forward slashes (path.basename on Unix doesn't treat \ as separator)
|
|
428
|
+
const base = path.basename(name.replace(/\0/g, '').replace(/\\/g, '/')).trim();
|
|
429
|
+
// Reject pure traversal names
|
|
430
|
+
if (base === '.' || base === '..')
|
|
431
|
+
return '';
|
|
432
|
+
return base;
|
|
433
433
|
}
|
|
434
434
|
/** Write an attachment file to the entity's attachments/ subdirectory. */
|
|
435
435
|
function writeAttachment(baseDir, entityId, filename, data) {
|
package/dist/lib/frontmatter.js
CHANGED
|
@@ -8,9 +8,10 @@ function serializeMarkdown(frontmatter, body) {
|
|
|
8
8
|
return `---\n${yamlStr}\n---\n\n${body}\n`;
|
|
9
9
|
}
|
|
10
10
|
function parseMarkdown(raw) {
|
|
11
|
-
const
|
|
11
|
+
const normalized = raw.replace(/\r\n/g, '\n');
|
|
12
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/);
|
|
12
13
|
if (!match)
|
|
13
|
-
return { frontmatter: {}, body:
|
|
14
|
+
return { frontmatter: {}, body: normalized };
|
|
14
15
|
const frontmatter = (0, yaml_1.parse)(match[1], { maxAliasCount: 10 }) ?? {};
|
|
15
16
|
const body = match[2].replace(/\n$/, '');
|
|
16
17
|
return { frontmatter, body };
|
package/dist/lib/jwt.js
CHANGED
|
@@ -85,15 +85,15 @@ function parseTtl(ttl) {
|
|
|
85
85
|
}
|
|
86
86
|
function signAccessToken(userId, secret, ttl) {
|
|
87
87
|
const payload = { userId, type: 'access' };
|
|
88
|
-
return jsonwebtoken_1.default.sign(payload, secret, { expiresIn: parseTtl(ttl) });
|
|
88
|
+
return jsonwebtoken_1.default.sign(payload, secret, { algorithm: 'HS256', expiresIn: parseTtl(ttl) });
|
|
89
89
|
}
|
|
90
90
|
function signRefreshToken(userId, secret, ttl) {
|
|
91
91
|
const payload = { userId, type: 'refresh' };
|
|
92
|
-
return jsonwebtoken_1.default.sign(payload, secret, { expiresIn: parseTtl(ttl) });
|
|
92
|
+
return jsonwebtoken_1.default.sign(payload, secret, { algorithm: 'HS256', expiresIn: parseTtl(ttl) });
|
|
93
93
|
}
|
|
94
94
|
function verifyToken(token, secret) {
|
|
95
95
|
try {
|
|
96
|
-
const decoded = jsonwebtoken_1.default.verify(token, secret);
|
|
96
|
+
const decoded = jsonwebtoken_1.default.verify(token, secret, { algorithms: ['HS256'] });
|
|
97
97
|
if (!decoded.userId || !decoded.type)
|
|
98
98
|
return null;
|
|
99
99
|
return { userId: decoded.userId, type: decoded.type };
|
|
@@ -108,7 +108,7 @@ function verifyToken(token, secret) {
|
|
|
108
108
|
const ACCESS_COOKIE = 'mgm_access';
|
|
109
109
|
const REFRESH_COOKIE = 'mgm_refresh';
|
|
110
110
|
function setAuthCookies(res, accessToken, refreshToken, accessTtl, refreshTtl) {
|
|
111
|
-
const secure = process.env.NODE_ENV
|
|
111
|
+
const secure = process.env.NODE_ENV !== 'development';
|
|
112
112
|
res.cookie(ACCESS_COOKIE, accessToken, {
|
|
113
113
|
httpOnly: true,
|
|
114
114
|
secure,
|
|
@@ -51,6 +51,7 @@ const frontmatter_1 = require("./frontmatter");
|
|
|
51
51
|
* this tracker lets us detect our own writes and skip them.
|
|
52
52
|
*/
|
|
53
53
|
class MirrorWriteTracker {
|
|
54
|
+
/** Map from filePath → { mtimeMs (for comparison), recordedAt (for eviction) } */
|
|
54
55
|
recentWrites = new Map();
|
|
55
56
|
static STALE_MS = 10_000; // entries older than 10s are stale
|
|
56
57
|
static MAX_ENTRIES = 10_000;
|
|
@@ -59,7 +60,7 @@ class MirrorWriteTracker {
|
|
|
59
60
|
try {
|
|
60
61
|
const stat = fs.statSync(filePath, { throwIfNoEntry: false });
|
|
61
62
|
if (stat)
|
|
62
|
-
this.recentWrites.set(filePath, stat.mtimeMs);
|
|
63
|
+
this.recentWrites.set(filePath, { mtimeMs: stat.mtimeMs, recordedAt: Date.now() });
|
|
63
64
|
}
|
|
64
65
|
catch { /* ignore */ }
|
|
65
66
|
// Prevent unbounded growth — evict stale entries periodically
|
|
@@ -75,7 +76,7 @@ class MirrorWriteTracker {
|
|
|
75
76
|
const stat = fs.statSync(filePath, { throwIfNoEntry: false });
|
|
76
77
|
if (!stat)
|
|
77
78
|
return false;
|
|
78
|
-
if (Math.abs(stat.mtimeMs - recorded) < 100) {
|
|
79
|
+
if (Math.abs(stat.mtimeMs - recorded.mtimeMs) < 100) {
|
|
79
80
|
this.recentWrites.delete(filePath);
|
|
80
81
|
return true;
|
|
81
82
|
}
|
|
@@ -86,8 +87,8 @@ class MirrorWriteTracker {
|
|
|
86
87
|
}
|
|
87
88
|
evictStale() {
|
|
88
89
|
const now = Date.now();
|
|
89
|
-
for (const [filePath,
|
|
90
|
-
if (now -
|
|
90
|
+
for (const [filePath, entry] of this.recentWrites) {
|
|
91
|
+
if (now - entry.recordedAt > MirrorWriteTracker.STALE_MS)
|
|
91
92
|
this.recentWrites.delete(filePath);
|
|
92
93
|
}
|
|
93
94
|
}
|