@graphmemory/server 1.2.0 → 1.3.1

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 +84 -12
  2. package/README.md +7 -1
  3. package/dist/api/index.js +151 -54
  4. package/dist/api/rest/code.js +2 -1
  5. package/dist/api/rest/docs.js +2 -1
  6. package/dist/api/rest/embed.js +8 -1
  7. package/dist/api/rest/index.js +39 -18
  8. package/dist/api/rest/knowledge.js +4 -2
  9. package/dist/api/rest/skills.js +2 -1
  10. package/dist/api/rest/tasks.js +2 -1
  11. package/dist/api/rest/tools.js +8 -1
  12. package/dist/api/rest/validation.js +41 -40
  13. package/dist/api/rest/websocket.js +24 -7
  14. package/dist/api/tools/code/search-code.js +12 -9
  15. package/dist/api/tools/code/search-files.js +1 -1
  16. package/dist/api/tools/docs/cross-references.js +3 -2
  17. package/dist/api/tools/docs/explain-symbol.js +2 -1
  18. package/dist/api/tools/docs/find-examples.js +2 -1
  19. package/dist/api/tools/docs/search-files.js +1 -1
  20. package/dist/api/tools/docs/search-snippets.js +1 -1
  21. package/dist/api/tools/docs/search.js +5 -4
  22. package/dist/api/tools/file-index/search-all-files.js +1 -1
  23. package/dist/api/tools/knowledge/add-attachment.js +15 -3
  24. package/dist/api/tools/knowledge/remove-attachment.js +5 -1
  25. package/dist/api/tools/knowledge/search-notes.js +5 -4
  26. package/dist/api/tools/skills/add-attachment.js +15 -3
  27. package/dist/api/tools/skills/recall-skills.js +1 -1
  28. package/dist/api/tools/skills/remove-attachment.js +5 -1
  29. package/dist/api/tools/skills/search-skills.js +6 -5
  30. package/dist/api/tools/tasks/add-attachment.js +15 -3
  31. package/dist/api/tools/tasks/remove-attachment.js +5 -1
  32. package/dist/api/tools/tasks/search-tasks.js +5 -4
  33. package/dist/cli/index.js +63 -52
  34. package/dist/cli/indexer.js +62 -29
  35. package/dist/graphs/attachment-types.js +5 -0
  36. package/dist/graphs/code.js +99 -10
  37. package/dist/graphs/docs.js +20 -5
  38. package/dist/graphs/file-index.js +22 -6
  39. package/dist/graphs/file-lang.js +1 -1
  40. package/dist/graphs/knowledge.js +31 -7
  41. package/dist/graphs/skill.js +35 -9
  42. package/dist/graphs/task.js +35 -9
  43. package/dist/lib/defaults.js +78 -0
  44. package/dist/lib/embedder.js +11 -12
  45. package/dist/lib/embedding-codec.js +63 -0
  46. package/dist/lib/graph-persistence.js +68 -0
  47. package/dist/lib/jwt.js +4 -4
  48. package/dist/lib/mirror-watcher.js +4 -3
  49. package/dist/lib/multi-config.js +6 -1
  50. package/dist/lib/parsers/code.js +158 -31
  51. package/dist/lib/parsers/codeblock.js +11 -6
  52. package/dist/lib/parsers/docs.js +60 -31
  53. package/dist/lib/parsers/languages/registry.js +2 -2
  54. package/dist/lib/parsers/languages/typescript.js +214 -46
  55. package/dist/lib/project-manager.js +21 -11
  56. package/dist/lib/search/bm25.js +23 -5
  57. package/dist/lib/search/code.js +13 -3
  58. package/dist/lib/search/docs.js +2 -1
  59. package/dist/lib/search/file-index.js +2 -1
  60. package/dist/lib/search/files.js +3 -2
  61. package/dist/lib/search/knowledge.js +2 -1
  62. package/dist/lib/search/skills.js +2 -1
  63. package/dist/lib/search/tasks.js +2 -1
  64. package/dist/ui/assets/NoteForm-aZX9f6-3.js +1 -0
  65. package/dist/ui/assets/SkillForm-KYa3o92l.js +1 -0
  66. package/dist/ui/assets/TaskForm-Bl5nkybO.js +1 -0
  67. package/dist/ui/assets/_articleId_-DjbCByxM.js +1 -0
  68. package/dist/ui/assets/_docId_-hdCDjclV.js +1 -0
  69. package/dist/ui/assets/_filePath_-CpG836v4.js +1 -0
  70. package/dist/ui/assets/_noteId_-C1enaQd1.js +1 -0
  71. package/dist/ui/assets/_skillId_-hPoCet7J.js +1 -0
  72. package/dist/ui/assets/_taskId_-DSB3dLVz.js +1 -0
  73. package/dist/ui/assets/_toolName_-3SmCfxZy.js +2 -0
  74. package/dist/ui/assets/api-BMnBjMMf.js +1 -0
  75. package/dist/ui/assets/api-BlFF6gX-.js +1 -0
  76. package/dist/ui/assets/api-CrGJOcaN.js +1 -0
  77. package/dist/ui/assets/api-DuX-0a_X.js +1 -0
  78. package/dist/ui/assets/attachments-CEQ-2nMo.js +1 -0
  79. package/dist/ui/assets/client-Bq88u7gN.js +1 -0
  80. package/dist/ui/assets/docs-CrXsRcOG.js +1 -0
  81. package/dist/ui/assets/edit-BYiy1FZy.js +1 -0
  82. package/dist/ui/assets/edit-TUIIpUMF.js +1 -0
  83. package/dist/ui/assets/edit-hc-ZWz3y.js +1 -0
  84. package/dist/ui/assets/esm-BWiKNcBW.js +1 -0
  85. package/dist/ui/assets/files-0bPg6NH9.js +1 -0
  86. package/dist/ui/assets/graph-DXGud_wF.js +1 -0
  87. package/dist/ui/assets/help-CEMQqZUR.js +891 -0
  88. package/dist/ui/assets/help-DJ52_fxN.js +1 -0
  89. package/dist/ui/assets/index-BCZDAYZi.js +2 -0
  90. package/dist/ui/assets/index-D6zSNtzo.css +1 -0
  91. package/dist/ui/assets/knowledge-DeygeGGH.js +1 -0
  92. package/dist/ui/assets/new-CpD7hOBA.js +1 -0
  93. package/dist/ui/assets/new-DHTg3Dqq.js +1 -0
  94. package/dist/ui/assets/new-s8c0M75X.js +1 -0
  95. package/dist/ui/assets/prompts-BgOmdxgM.js +295 -0
  96. package/dist/ui/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
  97. package/dist/ui/assets/search-EpJhdP2a.js +1 -0
  98. package/dist/ui/assets/skill-y9pizyqE.js +1 -0
  99. package/dist/ui/assets/skills-Cga9iUZN.js +1 -0
  100. package/dist/ui/assets/tasks-CobouTKV.js +1 -0
  101. package/dist/ui/assets/tools-JxKH5BDF.js +1 -0
  102. package/dist/ui/assets/vendor-graph-BWpSgpMe.js +321 -0
  103. package/dist/ui/assets/vendor-markdown-CT8ZVEPu.js +50 -0
  104. package/dist/ui/assets/vendor-md-editor-DmWafJvr.js +44 -0
  105. package/dist/ui/assets/{index-kKd4mVrh.css → vendor-md-editor-HrwGbQou.css} +1 -1
  106. package/dist/ui/assets/vendor-mui-BPj7d3Sw.js +139 -0
  107. package/dist/ui/assets/vendor-mui-icons-B196sG3f.js +1 -0
  108. package/dist/ui/assets/vendor-react-CHUjhoxh.js +11 -0
  109. package/dist/ui/index.html +11 -3
  110. package/package.json +6 -3
  111. package/dist/ui/assets/index-0hRezICt.js +0 -1702
@@ -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,9 @@ 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");
25
+ const graph_persistence_1 = require("../lib/graph-persistence");
26
+ const defaults_1 = require("../lib/defaults");
23
27
  // ---------------------------------------------------------------------------
24
28
  // CRUD
25
29
  // ---------------------------------------------------------------------------
@@ -30,19 +34,31 @@ function updateCodeFile(graph, parsed) {
30
34
  graph.addNode(id, attrs);
31
35
  }
32
36
  const pendingImports = [];
37
+ const pendingEdges = [];
33
38
  for (const { from, to, attrs } of parsed.edges) {
34
39
  if (!graph.hasNode(to)) {
35
- if (attrs.kind === 'imports')
40
+ if (attrs.kind === 'imports') {
36
41
  pendingImports.push(to);
42
+ }
43
+ else if (attrs.kind === 'extends' || attrs.kind === 'implements') {
44
+ // Target class/interface may be in another file — defer resolution
45
+ const toName = to.split('::').pop();
46
+ pendingEdges.push({ from, toName, kind: attrs.kind });
47
+ }
37
48
  continue;
38
49
  }
39
50
  if (graph.hasNode(from) && !graph.hasEdge(from, to)) {
40
51
  graph.addEdgeWithKey(`${from}→${to}`, from, to, attrs);
41
52
  }
42
53
  }
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);
54
+ // Store pending data on the file node for post-drain resolution
55
+ if (graph.hasNode(parsed.fileId)) {
56
+ if (pendingImports.length > 0) {
57
+ graph.setNodeAttribute(parsed.fileId, 'pendingImports', pendingImports);
58
+ }
59
+ if (pendingEdges.length > 0) {
60
+ graph.setNodeAttribute(parsed.fileId, 'pendingEdges', pendingEdges);
61
+ }
46
62
  }
47
63
  }
48
64
  /**
@@ -71,6 +87,67 @@ function resolvePendingImports(graph) {
71
87
  });
72
88
  return created;
73
89
  }
90
+ /**
91
+ * Resolve pending extends/implements edges after all files have been indexed.
92
+ * When multiple candidates share the same name, prefers the one whose file
93
+ * is imported by the source file (falls back to first match).
94
+ */
95
+ function resolvePendingEdges(graph) {
96
+ const nameIndex = new Map();
97
+ graph.forEachNode((id, attrs) => {
98
+ if (attrs.kind === 'class' || attrs.kind === 'interface') {
99
+ const list = nameIndex.get(attrs.name) ?? [];
100
+ list.push(id);
101
+ nameIndex.set(attrs.name, list);
102
+ }
103
+ });
104
+ // Build file → imported file IDs index for disambiguation
105
+ const fileImports = new Map();
106
+ graph.forEachNode((id, attrs) => {
107
+ if (attrs.kind === 'file') {
108
+ const imported = new Set();
109
+ graph.forEachOutEdge(id, (_edge, edgeAttrs, _src, target) => {
110
+ if (edgeAttrs.kind === 'imports')
111
+ imported.add(target);
112
+ });
113
+ fileImports.set(id, imported);
114
+ }
115
+ });
116
+ let created = 0;
117
+ graph.forEachNode((id, attrs) => {
118
+ if (!attrs.pendingEdges || attrs.pendingEdges.length === 0)
119
+ return;
120
+ const remaining = [];
121
+ for (const edge of attrs.pendingEdges) {
122
+ const candidates = nameIndex.get(edge.toName);
123
+ if (candidates && candidates.length > 0 && graph.hasNode(edge.from)) {
124
+ let toId;
125
+ if (candidates.length === 1) {
126
+ toId = candidates[0];
127
+ }
128
+ else {
129
+ // Disambiguate: prefer candidate whose file is imported by edge.from's file
130
+ const fromFileId = edge.from.split('::')[0];
131
+ const imports = fileImports.get(fromFileId);
132
+ const match = imports && candidates.find(c => {
133
+ const cFileId = c.split('::')[0];
134
+ return imports.has(cFileId);
135
+ });
136
+ toId = match ?? candidates[0];
137
+ }
138
+ if (toId !== edge.from && !graph.hasEdge(edge.from, toId)) {
139
+ graph.addEdgeWithKey(`${edge.from}→${toId}`, edge.from, toId, { kind: edge.kind });
140
+ created++;
141
+ }
142
+ }
143
+ else {
144
+ remaining.push(edge);
145
+ }
146
+ }
147
+ graph.setNodeAttribute(id, 'pendingEdges', remaining.length > 0 ? remaining : undefined);
148
+ });
149
+ return created;
150
+ }
74
151
  /** Remove all nodes (and their incident edges) belonging to a file. */
75
152
  function removeCodeFile(graph, fileId) {
76
153
  const toRemove = graph.filterNodes((_, attrs) => attrs.fileId === fileId);
@@ -91,7 +168,7 @@ function getCodeFileMtime(graph, fileId) {
91
168
  return graph.getNodeAttribute(nodes[0], 'mtime');
92
169
  }
93
170
  /** List all indexed files with symbol counts. */
94
- function listCodeFiles(graph, filter, limit = 20) {
171
+ function listCodeFiles(graph, filter, limit = defaults_1.LIST_LIMIT_SMALL) {
95
172
  const files = new Map();
96
173
  const lowerFilter = filter?.toLowerCase();
97
174
  graph.forEachNode((_, attrs) => {
@@ -112,23 +189,35 @@ function saveCodeGraph(graph, graphMemory, embeddingFingerprint) {
112
189
  fs_1.default.mkdirSync(graphMemory, { recursive: true });
113
190
  const file = path_1.default.join(graphMemory, 'code.json');
114
191
  const tmp = file + '.tmp';
115
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
116
- fs_1.default.renameSync(tmp, file);
192
+ try {
193
+ const exported = graph.export();
194
+ (0, embedding_codec_1.compressEmbeddings)(exported);
195
+ fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
196
+ fs_1.default.renameSync(tmp, file);
197
+ }
198
+ catch (err) {
199
+ try {
200
+ fs_1.default.unlinkSync(tmp);
201
+ }
202
+ catch { /* ignore cleanup error */ }
203
+ throw err;
204
+ }
117
205
  }
118
206
  function loadCodeGraph(graphMemory, fresh = false, embeddingFingerprint) {
119
207
  const graph = (0, code_types_1.createCodeGraph)();
120
208
  if (fresh)
121
209
  return graph;
122
210
  const file = path_1.default.join(graphMemory, 'code.json');
123
- if (!fs_1.default.existsSync(file))
211
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
212
+ if (!data)
124
213
  return graph;
125
214
  try {
126
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
127
215
  const stored = data.embeddingModel;
128
216
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
129
217
  process.stderr.write(`[code-graph] Embedding config changed, re-indexing code graph\n`);
130
218
  return graph;
131
219
  }
220
+ (0, embedding_codec_1.decompressEmbeddings)(data.graph);
132
221
  graph.import(data.graph);
133
222
  process.stderr.write(`[code-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
134
223
  }
@@ -144,7 +233,7 @@ class CodeGraphManager {
144
233
  _graph;
145
234
  embedFns;
146
235
  ext;
147
- _bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment}`);
236
+ _bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment} ${attrs.body.slice(0, defaults_1.BM25_BODY_MAX_CHARS)}`);
148
237
  constructor(_graph, embedFns, ext = {}) {
149
238
  this._graph = _graph;
150
239
  this.embedFns = embedFns;
@@ -20,6 +20,9 @@ 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");
24
+ const graph_persistence_1 = require("../lib/graph-persistence");
25
+ const defaults_1 = require("../lib/defaults");
23
26
  function createGraph() {
24
27
  return new graphology_1.DirectedGraph({ multi: false, allowSelfLoops: false });
25
28
  }
@@ -107,7 +110,7 @@ function getFileMtime(graph, fileId) {
107
110
  return 0;
108
111
  return graph.getNodeAttribute(nodes[0], 'mtime');
109
112
  }
110
- function listFiles(graph, filter, limit = 20) {
113
+ function listFiles(graph, filter, limit = defaults_1.LIST_LIMIT_SMALL) {
111
114
  const files = new Map();
112
115
  const lowerFilter = filter?.toLowerCase();
113
116
  graph.forEachNode((_, attrs) => {
@@ -133,23 +136,35 @@ function saveGraph(graph, graphMemory, embeddingFingerprint) {
133
136
  fs_1.default.mkdirSync(graphMemory, { recursive: true });
134
137
  const file = path_1.default.join(graphMemory, 'docs.json');
135
138
  const tmp = file + '.tmp';
136
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
137
- fs_1.default.renameSync(tmp, file);
139
+ try {
140
+ const exported = graph.export();
141
+ (0, embedding_codec_1.compressEmbeddings)(exported);
142
+ fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
143
+ fs_1.default.renameSync(tmp, file);
144
+ }
145
+ catch (err) {
146
+ try {
147
+ fs_1.default.unlinkSync(tmp);
148
+ }
149
+ catch { /* ignore cleanup error */ }
150
+ throw err;
151
+ }
138
152
  }
139
153
  function loadGraph(graphMemory, fresh = false, embeddingFingerprint) {
140
154
  const graph = createGraph();
141
155
  if (fresh)
142
156
  return graph;
143
157
  const file = path_1.default.join(graphMemory, 'docs.json');
144
- if (!fs_1.default.existsSync(file))
158
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
159
+ if (!data)
145
160
  return graph;
146
161
  try {
147
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
148
162
  const stored = data.embeddingModel;
149
163
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
150
164
  process.stderr.write(`[graph] Embedding config changed, re-indexing docs graph\n`);
151
165
  return graph;
152
166
  }
167
+ (0, embedding_codec_1.decompressEmbeddings)(data.graph);
153
168
  graph.import(data.graph);
154
169
  process.stderr.write(`[graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
155
170
  }
@@ -19,6 +19,9 @@ 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");
23
+ const graph_persistence_1 = require("../lib/graph-persistence");
24
+ const defaults_1 = require("../lib/defaults");
22
25
  // ---------------------------------------------------------------------------
23
26
  // CRUD
24
27
  // ---------------------------------------------------------------------------
@@ -147,7 +150,7 @@ function getFileEntryMtime(graph, filePath) {
147
150
  * Otherwise returns all file nodes matching the filters.
148
151
  */
149
152
  function listAllFiles(graph, options = {}) {
150
- const { directory, extension, language, filter, limit = 50 } = options;
153
+ const { directory, extension, language, filter, limit = defaults_1.LIST_LIMIT_LARGE } = options;
151
154
  const lowerFilter = filter?.toLowerCase();
152
155
  const results = [];
153
156
  if (directory !== undefined) {
@@ -157,7 +160,8 @@ function listAllFiles(graph, options = {}) {
157
160
  return [];
158
161
  graph.forEachOutNeighbor(dirId, (childId) => {
159
162
  const attrs = graph.getNodeAttributes(childId);
160
- if (graph.getEdgeAttribute(graph.edge(dirId, childId), 'kind') !== 'contains')
163
+ const edgeKey = graph.edge(dirId, childId);
164
+ if (!edgeKey || graph.getEdgeAttribute(edgeKey, 'kind') !== 'contains')
161
165
  return;
162
166
  if (extension && attrs.extension !== extension)
163
167
  return;
@@ -240,23 +244,35 @@ function saveFileIndexGraph(graph, graphMemory, embeddingFingerprint) {
240
244
  fs_1.default.mkdirSync(graphMemory, { recursive: true });
241
245
  const file = path_1.default.join(graphMemory, 'file-index.json');
242
246
  const tmp = file + '.tmp';
243
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
244
- fs_1.default.renameSync(tmp, file);
247
+ try {
248
+ const exported = graph.export();
249
+ (0, embedding_codec_1.compressEmbeddings)(exported);
250
+ fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
251
+ fs_1.default.renameSync(tmp, file);
252
+ }
253
+ catch (err) {
254
+ try {
255
+ fs_1.default.unlinkSync(tmp);
256
+ }
257
+ catch { /* ignore cleanup error */ }
258
+ throw err;
259
+ }
245
260
  }
246
261
  function loadFileIndexGraph(graphMemory, fresh = false, embeddingFingerprint) {
247
262
  const graph = (0, file_index_types_1.createFileIndexGraph)();
248
263
  if (fresh)
249
264
  return graph;
250
265
  const file = path_1.default.join(graphMemory, 'file-index.json');
251
- if (!fs_1.default.existsSync(file))
266
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
267
+ if (!data)
252
268
  return graph;
253
269
  try {
254
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
255
270
  const stored = data.embeddingModel;
256
271
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
257
272
  process.stderr.write(`[file-index] Embedding config changed, re-indexing file index\n`);
258
273
  return graph;
259
274
  }
275
+ (0, embedding_codec_1.decompressEmbeddings)(data.graph);
260
276
  graph.import(data.graph);
261
277
  process.stderr.write(`[file-index] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
262
278
  }
@@ -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,9 @@ 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");
32
+ const graph_persistence_1 = require("../lib/graph-persistence");
33
+ const defaults_1 = require("../lib/defaults");
31
34
  const attachment_types_1 = require("../graphs/attachment-types");
32
35
  const file_import_1 = require("../lib/file-import");
33
36
  // ---------------------------------------------------------------------------
@@ -161,7 +164,7 @@ function getNote(graph, noteId) {
161
164
  return { id: noteId, ...graph.getNodeAttributes(noteId) };
162
165
  }
163
166
  /** List notes with optional filter (substring in title/id) and tag filter. Excludes proxy nodes. */
164
- function listNotes(graph, filter, tag, limit = 20) {
167
+ function listNotes(graph, filter, tag, limit = defaults_1.LIST_LIMIT_SMALL) {
165
168
  const lowerFilter = filter?.toLowerCase();
166
169
  const lowerTag = tag?.toLowerCase();
167
170
  const results = [];
@@ -178,7 +181,7 @@ function listNotes(graph, filter, tag, limit = 20) {
178
181
  if (!attrs.tags.some(t => t.toLowerCase() === lowerTag))
179
182
  return;
180
183
  }
181
- results.push({ id, title: attrs.title, content: attrs.content.slice(0, 500), tags: attrs.tags, updatedAt: attrs.updatedAt });
184
+ results.push({ id, title: attrs.title, content: attrs.content.slice(0, defaults_1.CONTENT_PREVIEW_LEN), tags: attrs.tags, updatedAt: attrs.updatedAt });
182
185
  });
183
186
  return results
184
187
  .sort((a, b) => b.updatedAt - a.updatedAt)
@@ -329,23 +332,35 @@ function saveKnowledgeGraph(graph, graphMemory, embeddingFingerprint) {
329
332
  fs_1.default.mkdirSync(graphMemory, { recursive: true });
330
333
  const file = path_1.default.join(graphMemory, 'knowledge.json');
331
334
  const tmp = file + '.tmp';
332
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
333
- fs_1.default.renameSync(tmp, file);
335
+ try {
336
+ const exported = graph.export();
337
+ (0, embedding_codec_1.compressEmbeddings)(exported);
338
+ fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
339
+ fs_1.default.renameSync(tmp, file);
340
+ }
341
+ catch (err) {
342
+ try {
343
+ fs_1.default.unlinkSync(tmp);
344
+ }
345
+ catch { /* ignore cleanup error */ }
346
+ throw err;
347
+ }
334
348
  }
335
349
  function loadKnowledgeGraph(graphMemory, fresh = false, embeddingFingerprint) {
336
350
  const graph = (0, knowledge_types_1.createKnowledgeGraph)();
337
351
  if (fresh)
338
352
  return graph;
339
353
  const file = path_1.default.join(graphMemory, 'knowledge.json');
340
- if (!fs_1.default.existsSync(file))
354
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
355
+ if (!data)
341
356
  return graph;
342
357
  try {
343
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
344
358
  const stored = data.embeddingModel;
345
359
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
346
360
  process.stderr.write(`[knowledge-graph] Embedding config changed, re-indexing knowledge graph\n`);
347
361
  return graph;
348
362
  }
363
+ (0, embedding_codec_1.decompressEmbeddings)(data.graph);
349
364
  graph.import(data.graph);
350
365
  process.stderr.write(`[knowledge-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
351
366
  }
@@ -552,7 +567,9 @@ class KnowledgeGraphManager {
552
567
  try {
553
568
  const actualToId = targetGraph ? proxyId(targetGraph, toId, pid) : toId;
554
569
  if (this._graph.hasEdge(fromId, actualToId)) {
555
- kind = this._graph.getEdgeAttribute(this._graph.edge(fromId, actualToId), 'kind') ?? '';
570
+ const ek = this._graph.edge(fromId, actualToId);
571
+ if (ek)
572
+ kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
556
573
  }
557
574
  }
558
575
  catch { /* ignore */ }
@@ -586,6 +603,11 @@ class KnowledgeGraphManager {
586
603
  return null;
587
604
  if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
588
605
  return null;
606
+ if (data.length > attachment_types_1.MAX_ATTACHMENT_SIZE)
607
+ return null;
608
+ const existing = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, noteId));
609
+ if (existing.length >= attachment_types_1.MAX_ATTACHMENTS_PER_ENTITY)
610
+ return null;
589
611
  const safe = (0, file_mirror_1.sanitizeFilename)(filename);
590
612
  if (!safe)
591
613
  return null;
@@ -628,6 +650,8 @@ class KnowledgeGraphManager {
628
650
  return;
629
651
  const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, noteId));
630
652
  this._graph.setNodeAttribute(noteId, 'attachments', attachments);
653
+ this._graph.setNodeAttribute(noteId, 'updatedAt', Date.now());
654
+ this._graph.setNodeAttribute(noteId, 'version', (this._graph.getNodeAttribute(noteId, 'version') ?? 0) + 1);
631
655
  this.ctx.markDirty();
632
656
  }
633
657
  listAttachments(noteId) {
@@ -30,6 +30,9 @@ 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");
34
+ const graph_persistence_1 = require("../lib/graph-persistence");
35
+ const defaults_1 = require("../lib/defaults");
33
36
  const attachment_types_1 = require("../graphs/attachment-types");
34
37
  const file_import_1 = require("../lib/file-import");
35
38
  // ---------------------------------------------------------------------------
@@ -289,7 +292,7 @@ function getSkill(graph, skillId) {
289
292
  }
290
293
  /** List skills with optional filters. Excludes proxy nodes. */
291
294
  function listSkills(graph, opts = {}) {
292
- const { source, tag, filter, limit = 50 } = opts;
295
+ const { source, tag, filter, limit = defaults_1.LIST_LIMIT_LARGE } = opts;
293
296
  const lowerFilter = filter?.toLowerCase();
294
297
  const lowerTag = tag?.toLowerCase();
295
298
  const results = [];
@@ -309,7 +312,7 @@ function listSkills(graph, opts = {}) {
309
312
  results.push({
310
313
  id,
311
314
  title: attrs.title,
312
- description: attrs.description?.slice(0, 500),
315
+ description: attrs.description?.slice(0, defaults_1.CONTENT_PREVIEW_LEN),
313
316
  steps: attrs.steps,
314
317
  triggers: attrs.triggers,
315
318
  inputHints: attrs.inputHints,
@@ -473,23 +476,35 @@ function saveSkillGraph(graph, graphMemory, embeddingFingerprint) {
473
476
  fs_1.default.mkdirSync(graphMemory, { recursive: true });
474
477
  const file = path_1.default.join(graphMemory, 'skills.json');
475
478
  const tmp = file + '.tmp';
476
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
477
- fs_1.default.renameSync(tmp, file);
479
+ try {
480
+ const exported = graph.export();
481
+ (0, embedding_codec_1.compressEmbeddings)(exported);
482
+ fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
483
+ fs_1.default.renameSync(tmp, file);
484
+ }
485
+ catch (err) {
486
+ try {
487
+ fs_1.default.unlinkSync(tmp);
488
+ }
489
+ catch { /* ignore cleanup error */ }
490
+ throw err;
491
+ }
478
492
  }
479
493
  function loadSkillGraph(graphMemory, fresh = false, embeddingFingerprint) {
480
494
  const graph = (0, skill_types_1.createSkillGraph)();
481
495
  if (fresh)
482
496
  return graph;
483
497
  const file = path_1.default.join(graphMemory, 'skills.json');
484
- if (!fs_1.default.existsSync(file))
498
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
499
+ if (!data)
485
500
  return graph;
486
501
  try {
487
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
488
502
  const stored = data.embeddingModel;
489
503
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
490
504
  process.stderr.write(`[skill-graph] Embedding config changed, re-indexing skill graph\n`);
491
505
  return graph;
492
506
  }
507
+ (0, embedding_codec_1.decompressEmbeddings)(data.graph);
493
508
  graph.import(data.graph);
494
509
  process.stderr.write(`[skill-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
495
510
  }
@@ -760,7 +775,9 @@ class SkillGraphManager {
760
775
  try {
761
776
  const proxyNodeId = proxyId(targetGraph, targetId, pid);
762
777
  if (this._graph.hasEdge(skillId, proxyNodeId)) {
763
- kind = this._graph.getEdgeAttribute(this._graph.edge(skillId, proxyNodeId), 'kind') ?? '';
778
+ const ek = this._graph.edge(skillId, proxyNodeId);
779
+ if (ek)
780
+ kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
764
781
  }
765
782
  }
766
783
  catch { /* ignore */ }
@@ -788,7 +805,9 @@ class SkillGraphManager {
788
805
  let kind = '';
789
806
  try {
790
807
  if (this._graph.hasEdge(fromId, toId)) {
791
- kind = this._graph.getEdgeAttribute(this._graph.edge(fromId, toId), 'kind') ?? '';
808
+ const ek = this._graph.edge(fromId, toId);
809
+ if (ek)
810
+ kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
792
811
  }
793
812
  }
794
813
  catch { /* ignore */ }
@@ -812,10 +831,15 @@ class SkillGraphManager {
812
831
  return null;
813
832
  if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
814
833
  return null;
834
+ if (data.length > attachment_types_1.MAX_ATTACHMENT_SIZE)
835
+ return null;
836
+ const entityDir = path_1.default.join(dir, skillId);
837
+ const existing = (0, attachment_types_1.scanAttachments)(entityDir);
838
+ if (existing.length >= attachment_types_1.MAX_ATTACHMENTS_PER_ENTITY)
839
+ return null;
815
840
  const safe = (0, file_mirror_1.sanitizeFilename)(filename);
816
841
  if (!safe)
817
842
  return null;
818
- const entityDir = path_1.default.join(dir, skillId);
819
843
  (0, file_mirror_1.writeAttachment)(dir, skillId, safe, data);
820
844
  this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'attachments', safe));
821
845
  (0, file_mirror_1.mirrorAttachmentEvent)(entityDir, 'add', safe);
@@ -856,6 +880,8 @@ class SkillGraphManager {
856
880
  return;
857
881
  const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, skillId));
858
882
  this._graph.setNodeAttribute(skillId, 'attachments', attachments);
883
+ this._graph.setNodeAttribute(skillId, 'updatedAt', Date.now());
884
+ this._graph.setNodeAttribute(skillId, 'version', (this._graph.getNodeAttribute(skillId, 'version') ?? 0) + 1);
859
885
  this.ctx.markDirty();
860
886
  }
861
887
  listAttachments(skillId) {
@@ -30,6 +30,9 @@ 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");
34
+ const graph_persistence_1 = require("../lib/graph-persistence");
35
+ const defaults_1 = require("../lib/defaults");
33
36
  const attachment_types_1 = require("../graphs/attachment-types");
34
37
  const file_import_1 = require("../lib/file-import");
35
38
  // ---------------------------------------------------------------------------
@@ -300,7 +303,7 @@ function getTask(graph, taskId) {
300
303
  }
301
304
  /** List tasks with optional filters. Excludes proxy nodes. */
302
305
  function listTasks(graph, opts = {}) {
303
- const { status, priority, tag, filter, assignee, limit = 50 } = opts;
306
+ const { status, priority, tag, filter, assignee, limit = defaults_1.LIST_LIMIT_LARGE } = opts;
304
307
  const lowerFilter = filter?.toLowerCase();
305
308
  const lowerTag = tag?.toLowerCase();
306
309
  const results = [];
@@ -324,7 +327,7 @@ function listTasks(graph, opts = {}) {
324
327
  results.push({
325
328
  id,
326
329
  title: attrs.title,
327
- description: attrs.description?.slice(0, 500),
330
+ description: attrs.description?.slice(0, defaults_1.CONTENT_PREVIEW_LEN),
328
331
  status: attrs.status,
329
332
  priority: attrs.priority,
330
333
  tags: attrs.tags,
@@ -494,23 +497,35 @@ function saveTaskGraph(graph, graphMemory, embeddingFingerprint) {
494
497
  fs_1.default.mkdirSync(graphMemory, { recursive: true });
495
498
  const file = path_1.default.join(graphMemory, 'tasks.json');
496
499
  const tmp = file + '.tmp';
497
- fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: graph.export() }));
498
- fs_1.default.renameSync(tmp, file);
500
+ try {
501
+ const exported = graph.export();
502
+ (0, embedding_codec_1.compressEmbeddings)(exported);
503
+ fs_1.default.writeFileSync(tmp, JSON.stringify({ embeddingModel: embeddingFingerprint, graph: exported }));
504
+ fs_1.default.renameSync(tmp, file);
505
+ }
506
+ catch (err) {
507
+ try {
508
+ fs_1.default.unlinkSync(tmp);
509
+ }
510
+ catch { /* ignore cleanup error */ }
511
+ throw err;
512
+ }
499
513
  }
500
514
  function loadTaskGraph(graphMemory, fresh = false, embeddingFingerprint) {
501
515
  const graph = (0, task_types_1.createTaskGraph)();
502
516
  if (fresh)
503
517
  return graph;
504
518
  const file = path_1.default.join(graphMemory, 'tasks.json');
505
- if (!fs_1.default.existsSync(file))
519
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
520
+ if (!data)
506
521
  return graph;
507
522
  try {
508
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
509
523
  const stored = data.embeddingModel;
510
524
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
511
525
  process.stderr.write(`[task-graph] Embedding config changed, re-indexing task graph\n`);
512
526
  return graph;
513
527
  }
528
+ (0, embedding_codec_1.decompressEmbeddings)(data.graph);
514
529
  graph.import(data.graph);
515
530
  process.stderr.write(`[task-graph] Loaded ${graph.order} nodes, ${graph.size} edges\n`);
516
531
  }
@@ -733,7 +748,9 @@ class TaskGraphManager {
733
748
  try {
734
749
  const proxyNodeId = proxyId(targetGraph, targetId, pid);
735
750
  if (this._graph.hasEdge(taskId, proxyNodeId)) {
736
- kind = this._graph.getEdgeAttribute(this._graph.edge(taskId, proxyNodeId), 'kind') ?? '';
751
+ const ek = this._graph.edge(taskId, proxyNodeId);
752
+ if (ek)
753
+ kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
737
754
  }
738
755
  }
739
756
  catch { /* ignore */ }
@@ -759,7 +776,9 @@ class TaskGraphManager {
759
776
  let kind = '';
760
777
  try {
761
778
  if (this._graph.hasEdge(fromId, toId)) {
762
- kind = this._graph.getEdgeAttribute(this._graph.edge(fromId, toId), 'kind') ?? '';
779
+ const ek = this._graph.edge(fromId, toId);
780
+ if (ek)
781
+ kind = this._graph.getEdgeAttribute(ek, 'kind') ?? '';
763
782
  }
764
783
  }
765
784
  catch { /* ignore */ }
@@ -783,10 +802,15 @@ class TaskGraphManager {
783
802
  return null;
784
803
  if (!this._graph.hasNode(taskId) || isProxy(this._graph, taskId))
785
804
  return null;
805
+ if (data.length > attachment_types_1.MAX_ATTACHMENT_SIZE)
806
+ return null;
807
+ const entityDir = path_1.default.join(dir, taskId);
808
+ const existing = (0, attachment_types_1.scanAttachments)(entityDir);
809
+ if (existing.length >= attachment_types_1.MAX_ATTACHMENTS_PER_ENTITY)
810
+ return null;
786
811
  const safe = (0, file_mirror_1.sanitizeFilename)(filename);
787
812
  if (!safe)
788
813
  return null;
789
- const entityDir = path_1.default.join(dir, taskId);
790
814
  (0, file_mirror_1.writeAttachment)(dir, taskId, safe, data);
791
815
  this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'attachments', safe));
792
816
  (0, file_mirror_1.mirrorAttachmentEvent)(entityDir, 'add', safe);
@@ -827,6 +851,8 @@ class TaskGraphManager {
827
851
  return;
828
852
  const attachments = (0, attachment_types_1.scanAttachments)(path_1.default.join(dir, taskId));
829
853
  this._graph.setNodeAttribute(taskId, 'attachments', attachments);
854
+ this._graph.setNodeAttribute(taskId, 'updatedAt', Date.now());
855
+ this._graph.setNodeAttribute(taskId, 'version', (this._graph.getNodeAttribute(taskId, 'version') ?? 0) + 1);
830
856
  this.ctx.markDirty();
831
857
  }
832
858
  listAttachments(taskId) {