@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.
Files changed (104) hide show
  1. package/LICENSE +84 -12
  2. package/README.md +66 -101
  3. package/dist/api/index.js +279 -169
  4. package/dist/api/rest/index.js +36 -16
  5. package/dist/api/rest/tools.js +8 -1
  6. package/dist/api/rest/websocket.js +22 -1
  7. package/dist/api/tools/code/search-code.js +12 -9
  8. package/dist/api/tools/code/search-files.js +1 -1
  9. package/dist/api/tools/docs/cross-references.js +3 -2
  10. package/dist/api/tools/docs/explain-symbol.js +2 -1
  11. package/dist/api/tools/docs/find-examples.js +2 -1
  12. package/dist/api/tools/docs/search-files.js +1 -1
  13. package/dist/api/tools/docs/search-snippets.js +1 -1
  14. package/dist/api/tools/docs/search.js +5 -4
  15. package/dist/api/tools/file-index/search-all-files.js +1 -1
  16. package/dist/api/tools/knowledge/add-attachment.js +14 -3
  17. package/dist/api/tools/knowledge/create-relation.js +2 -2
  18. package/dist/api/tools/knowledge/delete-relation.js +2 -2
  19. package/dist/api/tools/knowledge/find-linked-notes.js +1 -1
  20. package/dist/api/tools/knowledge/remove-attachment.js +5 -1
  21. package/dist/api/tools/knowledge/search-notes.js +5 -4
  22. package/dist/api/tools/skills/add-attachment.js +14 -3
  23. package/dist/api/tools/skills/recall-skills.js +1 -1
  24. package/dist/api/tools/skills/remove-attachment.js +5 -1
  25. package/dist/api/tools/skills/search-skills.js +6 -5
  26. package/dist/api/tools/tasks/add-attachment.js +14 -3
  27. package/dist/api/tools/tasks/create-task-link.js +1 -1
  28. package/dist/api/tools/tasks/delete-task-link.js +1 -1
  29. package/dist/api/tools/tasks/find-linked-tasks.js +1 -1
  30. package/dist/api/tools/tasks/remove-attachment.js +5 -1
  31. package/dist/api/tools/tasks/search-tasks.js +5 -4
  32. package/dist/cli/index.js +69 -311
  33. package/dist/cli/indexer.js +61 -29
  34. package/dist/graphs/code.js +70 -7
  35. package/dist/graphs/docs.js +15 -2
  36. package/dist/graphs/file-index.js +20 -6
  37. package/dist/graphs/file-lang.js +1 -1
  38. package/dist/graphs/knowledge.js +20 -3
  39. package/dist/graphs/manager-types.js +1 -1
  40. package/dist/graphs/skill.js +23 -4
  41. package/dist/graphs/task.js +23 -4
  42. package/dist/lib/embedding-codec.js +65 -0
  43. package/dist/lib/file-mirror.js +7 -7
  44. package/dist/lib/frontmatter.js +3 -2
  45. package/dist/lib/jwt.js +4 -4
  46. package/dist/lib/mirror-watcher.js +5 -4
  47. package/dist/lib/multi-config.js +60 -1
  48. package/dist/lib/parsers/code.js +158 -31
  49. package/dist/lib/parsers/codeblock.js +11 -6
  50. package/dist/lib/parsers/docs.js +59 -31
  51. package/dist/lib/parsers/languages/registry.js +10 -4
  52. package/dist/lib/parsers/languages/typescript.js +195 -48
  53. package/dist/lib/project-manager.js +14 -10
  54. package/dist/lib/search/bm25.js +18 -1
  55. package/dist/lib/search/code.js +12 -3
  56. package/dist/lib/watcher.js +17 -9
  57. package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
  58. package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
  59. package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
  60. package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
  61. package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
  62. package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
  63. package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
  64. package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
  65. package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
  66. package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
  67. package/dist/ui/assets/api-BMnBjMMf.js +1 -0
  68. package/dist/ui/assets/api-BlFF6gX-.js +1 -0
  69. package/dist/ui/assets/api-CrGJOcaN.js +1 -0
  70. package/dist/ui/assets/api-DuX-0a_X.js +1 -0
  71. package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
  72. package/dist/ui/assets/client-Bq88u7gN.js +1 -0
  73. package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
  74. package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
  75. package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
  76. package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
  77. package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
  78. package/dist/ui/assets/files-0bPg6NH9.js +1 -0
  79. package/dist/ui/assets/graph-DXGud_wF.js +1 -0
  80. package/dist/ui/assets/help-CEMQqZUR.js +891 -0
  81. package/dist/ui/assets/help-DJ52_fxN.js +1 -0
  82. package/dist/ui/assets/index-BCZDAYZi.js +2 -0
  83. package/dist/ui/assets/index-D6zSNtzo.css +1 -0
  84. package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
  85. package/dist/ui/assets/new-CpD7hOBA.js +1 -0
  86. package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
  87. package/dist/ui/assets/new-s8c0M75X.js +1 -0
  88. package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
  89. package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
  90. package/dist/ui/assets/search-EpJhdP2a.js +1 -0
  91. package/dist/ui/assets/skill-y9pizyqE.js +1 -0
  92. package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
  93. package/dist/ui/assets/tasks-CobouTKV.js +1 -0
  94. package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
  95. package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
  96. package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
  97. package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
  98. package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
  99. package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
  100. package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
  101. package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
  102. package/dist/ui/index.html +11 -3
  103. package/package.json +2 -2
  104. package/dist/ui/assets/index-D6oxrVF7.js +0 -1759
@@ -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 imports on the file node for post-drain resolution
44
- if (pendingImports.length > 0 && graph.hasNode(parsed.fileId)) {
45
- graph.setNodeAttribute(parsed.fileId, 'pendingImports', pendingImports);
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
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
116
- fs_1.default.renameSync(tmp, file);
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;
@@ -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
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
137
- fs_1.default.renameSync(tmp, file);
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
- if (graph.getEdgeAttribute(graph.edge(dirId, childId), 'kind') !== 'contains')
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
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
244
- fs_1.default.renameSync(tmp, file);
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
  }
@@ -15,7 +15,7 @@ exports.EXT_TO_LANGUAGE = {
15
15
  '.mjs': 'javascript',
16
16
  '.cjs': 'javascript',
17
17
  '.ts': 'typescript',
18
- '.tsx': 'typescript',
18
+ '.tsx': 'tsx',
19
19
  '.mts': 'typescript',
20
20
  '.cts': 'typescript',
21
21
  // Web
@@ -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
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
333
- fs_1.default.renameSync(tmp, file);
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
- kind = this._graph.getEdgeAttribute(this._graph.edge(fromId, actualToId), 'kind') ?? '';
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 and single-project stdio mode. */
19
+ /** No-op context for tests. */
20
20
  function noopContext(projectId = '') {
21
21
  return {
22
22
  markDirty: () => { },
@@ -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
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
477
- fs_1.default.renameSync(tmp, file);
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
- kind = this._graph.getEdgeAttribute(this._graph.edge(skillId, proxyNodeId), 'kind') ?? '';
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
- kind = this._graph.getEdgeAttribute(this._graph.edge(fromId, toId), 'kind') ?? '';
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) {
@@ -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
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
498
- fs_1.default.renameSync(tmp, file);
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
- kind = this._graph.getEdgeAttribute(this._graph.edge(taskId, proxyNodeId), 'kind') ?? '';
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
- kind = this._graph.getEdgeAttribute(this._graph.edge(fromId, toId), 'kind') ?? '';
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
+ }
@@ -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 path separators, .., and null bytes. */
425
+ /** Sanitize a filename: extract basename, strip null bytes and path traversal. */
426
426
  function sanitizeFilename(name) {
427
- const sanitized = name
428
- .replace(/\0/g, '')
429
- .replace(/\.\./g, '')
430
- .replace(/[/\\]/g, '')
431
- .trim();
432
- return sanitized; // empty string is a valid return — callers must check
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) {
@@ -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 match = raw.match(/^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/);
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: raw };
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 === 'production';
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, mtime] of this.recentWrites) {
90
- if (now - mtime > MirrorWriteTracker.STALE_MS)
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
  }