@graphmemory/server 1.3.0 → 1.3.2

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 (73) hide show
  1. package/dist/api/index.js +4 -4
  2. package/dist/api/rest/code.js +2 -1
  3. package/dist/api/rest/docs.js +2 -1
  4. package/dist/api/rest/embed.js +8 -1
  5. package/dist/api/rest/index.js +25 -7
  6. package/dist/api/rest/knowledge.js +4 -2
  7. package/dist/api/rest/skills.js +2 -1
  8. package/dist/api/rest/tasks.js +2 -1
  9. package/dist/api/rest/validation.js +41 -40
  10. package/dist/api/rest/websocket.js +24 -7
  11. package/dist/api/tools/knowledge/add-attachment.js +2 -1
  12. package/dist/api/tools/skills/add-attachment.js +2 -1
  13. package/dist/api/tools/tasks/add-attachment.js +2 -1
  14. package/dist/cli/index.js +7 -4
  15. package/dist/cli/indexer.js +38 -25
  16. package/dist/graphs/attachment-types.js +5 -0
  17. package/dist/graphs/code.js +34 -8
  18. package/dist/graphs/docs.js +5 -3
  19. package/dist/graphs/file-index.js +5 -3
  20. package/dist/graphs/knowledge.js +11 -4
  21. package/dist/graphs/skill.js +12 -5
  22. package/dist/graphs/task.js +12 -5
  23. package/dist/lib/defaults.js +78 -0
  24. package/dist/lib/embedder.js +11 -12
  25. package/dist/lib/embedding-codec.js +3 -5
  26. package/dist/lib/graph-persistence.js +68 -0
  27. package/dist/lib/jwt.js +2 -2
  28. package/dist/lib/mirror-watcher.js +4 -3
  29. package/dist/lib/multi-config.js +3 -1
  30. package/dist/lib/parsers/docs.js +2 -1
  31. package/dist/lib/parsers/languages/typescript.js +39 -17
  32. package/dist/lib/project-manager.js +7 -1
  33. package/dist/lib/search/bm25.js +5 -4
  34. package/dist/lib/search/code.js +2 -1
  35. package/dist/lib/search/docs.js +2 -1
  36. package/dist/lib/search/file-index.js +2 -1
  37. package/dist/lib/search/files.js +3 -2
  38. package/dist/lib/search/knowledge.js +2 -1
  39. package/dist/lib/search/skills.js +2 -1
  40. package/dist/lib/search/tasks.js +2 -1
  41. package/dist/ui/assets/{NoteForm-aZX9f6-3.js → NoteForm-SQ0b93i0.js} +1 -1
  42. package/dist/ui/assets/{SkillForm-KYa3o92l.js → SkillForm-BVsGrNPb.js} +1 -1
  43. package/dist/ui/assets/{TaskForm-Bl5nkybO.js → TaskForm-DgPVeiI9.js} +1 -1
  44. package/dist/ui/assets/{_articleId_-DjbCByxM.js → _articleId_-FqdaSeYS.js} +1 -1
  45. package/dist/ui/assets/{_docId_-hdCDjclV.js → _docId_-Q0Wmjtp6.js} +1 -1
  46. package/dist/ui/assets/{_filePath_-CpG836v4.js → _filePath_-BR0gOT_z.js} +1 -1
  47. package/dist/ui/assets/{_noteId_-C1enaQd1.js → _noteId_-BMWd415J.js} +1 -1
  48. package/dist/ui/assets/{_skillId_-hPoCet7J.js → _skillId_-CsHgildJ.js} +1 -1
  49. package/dist/ui/assets/{_taskId_-DSB3dLVz.js → _taskId_-xDHTfbQw.js} +1 -1
  50. package/dist/ui/assets/{_toolName_-3SmCfxZy.js → _toolName_-BSa2uNSu.js} +1 -1
  51. package/dist/ui/assets/{attachments-CEQ-2nMo.js → attachments-NSvN5_0A.js} +1 -1
  52. package/dist/ui/assets/{docs-CrXsRcOG.js → docs-iUK8E40J.js} +1 -1
  53. package/dist/ui/assets/{edit-TUIIpUMF.js → edit-BzIJy_Oo.js} +1 -1
  54. package/dist/ui/assets/{edit-BYiy1FZy.js → edit-Dnc067B2.js} +1 -1
  55. package/dist/ui/assets/{edit-hc-ZWz3y.js → edit-U_UEI361.js} +1 -1
  56. package/dist/ui/assets/{files-0bPg6NH9.js → files-B4svJUZh.js} +1 -1
  57. package/dist/ui/assets/{graph-DXGud_wF.js → graph-CcNP1ckP.js} +1 -1
  58. package/dist/ui/assets/{help-DJ52_fxN.js → help-BJZZtKAR.js} +1 -1
  59. package/dist/ui/assets/{help-CEMQqZUR.js → help-D6XKMuzk.js} +16 -4
  60. package/dist/ui/assets/index-CEweXD9O.js +2 -0
  61. package/dist/ui/assets/{knowledge-DeygeGGH.js → knowledge-CV99ToEV.js} +1 -1
  62. package/dist/ui/assets/{new-CpD7hOBA.js → new-BypesKiP.js} +1 -1
  63. package/dist/ui/assets/{new-s8c0M75X.js → new-Dcx8wlp4.js} +1 -1
  64. package/dist/ui/assets/{new-DHTg3Dqq.js → new-Sq3NY2oa.js} +1 -1
  65. package/dist/ui/assets/{prompts-BgOmdxgM.js → prompts-DbsIe3Pm.js} +1 -1
  66. package/dist/ui/assets/{search-EpJhdP2a.js → search-D87r7lIL.js} +1 -1
  67. package/dist/ui/assets/{skill-y9pizyqE.js → skill-BltAsz7M.js} +1 -1
  68. package/dist/ui/assets/{skills-Cga9iUZN.js → skills-Dtmg2kDA.js} +1 -1
  69. package/dist/ui/assets/{tasks-CobouTKV.js → tasks-BRqIwKCG.js} +1 -1
  70. package/dist/ui/assets/{tools-JxKH5BDF.js → tools-CM3gQ4TK.js} +1 -1
  71. package/dist/ui/index.html +1 -1
  72. package/package.json +5 -2
  73. package/dist/ui/assets/index-BCZDAYZi.js +0 -2
@@ -13,6 +13,7 @@ const docs_2 = require("../graphs/docs");
13
13
  const code_1 = require("../lib/parsers/code");
14
14
  const code_2 = require("../graphs/code");
15
15
  const watcher_1 = require("../lib/watcher");
16
+ const defaults_1 = require("../lib/defaults");
16
17
  const knowledge_1 = require("../graphs/knowledge");
17
18
  const task_1 = require("../graphs/task");
18
19
  const skill_1 = require("../graphs/skill");
@@ -103,7 +104,7 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
103
104
  const rootChunk = chunks.find(c => c.level === 1);
104
105
  const embedText = rootChunk?.title
105
106
  ? `${fileId} ${rootChunk.title}`
106
- : `${fileId} ${rootChunk?.content.slice(0, 200) ?? ''}`;
107
+ : `${fileId} ${rootChunk?.content.slice(0, defaults_1.INDEXER_PREVIEW_LEN) ?? ''}`;
107
108
  batchInputs.push({ title: embedText, content: '' });
108
109
  const embeddings = await (0, embedder_1.embedBatch)(batchInputs, config.docsModelName);
109
110
  for (let i = 0; i < chunks.length; i++) {
@@ -207,7 +208,9 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
207
208
  }
208
209
  function dispatchAdd(absolutePath) {
209
210
  const rel = path_1.default.relative(config.projectDir, absolutePath);
210
- if (config.docsInclude && !isExcluded(rel, docsExclude) && micromatch_1.default.isMatch(rel, config.docsInclude)) {
211
+ if (docGraph && config.docsInclude && !isExcluded(rel, docsExclude) && micromatch_1.default.isMatch(rel, config.docsInclude)) {
212
+ if (rel.endsWith('.md'))
213
+ (0, docs_1.clearWikiIndexCache)(config.projectDir);
211
214
  enqueueDoc(() => indexDocFile(absolutePath));
212
215
  }
213
216
  if (codeGraph && config.codeInclude && !isExcluded(rel, codeExclude) && micromatch_1.default.isMatch(rel, config.codeInclude)) {
@@ -220,33 +223,43 @@ function createProjectIndexer(docGraph, codeGraph, config, knowledgeGraph, fileI
220
223
  function dispatchRemove(absolutePath) {
221
224
  const rel = path_1.default.relative(config.projectDir, absolutePath);
222
225
  if (docGraph && config.docsInclude && !isExcluded(rel, docsExclude) && micromatch_1.default.isMatch(rel, config.docsInclude)) {
223
- (0, docs_2.removeFile)(docGraph, rel);
224
- if (knowledgeGraph)
225
- (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'docs', docGraph, config.projectId);
226
- if (taskGraph)
227
- (0, task_1.cleanupProxies)(taskGraph, 'docs', docGraph, config.projectId);
228
- if (skillGraph)
229
- (0, skill_1.cleanupProxies)(skillGraph, 'docs', docGraph, config.projectId);
230
- process.stderr.write(`[indexer] removed doc ${rel}\n`);
226
+ if (rel.endsWith('.md'))
227
+ (0, docs_1.clearWikiIndexCache)(config.projectDir);
228
+ // Enqueue removal to avoid racing with in-flight indexDocFile tasks
229
+ enqueueDoc(async () => {
230
+ (0, docs_2.removeFile)(docGraph, rel);
231
+ if (knowledgeGraph)
232
+ (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'docs', docGraph, config.projectId);
233
+ if (taskGraph)
234
+ (0, task_1.cleanupProxies)(taskGraph, 'docs', docGraph, config.projectId);
235
+ if (skillGraph)
236
+ (0, skill_1.cleanupProxies)(skillGraph, 'docs', docGraph, config.projectId);
237
+ process.stderr.write(`[indexer] removed doc ${rel}\n`);
238
+ });
231
239
  }
232
240
  if (codeGraph && config.codeInclude && !isExcluded(rel, codeExclude) && micromatch_1.default.isMatch(rel, config.codeInclude)) {
233
- (0, code_2.removeCodeFile)(codeGraph, rel);
234
- if (knowledgeGraph)
235
- (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'code', codeGraph, config.projectId);
236
- if (taskGraph)
237
- (0, task_1.cleanupProxies)(taskGraph, 'code', codeGraph, config.projectId);
238
- if (skillGraph)
239
- (0, skill_1.cleanupProxies)(skillGraph, 'code', codeGraph, config.projectId);
240
- process.stderr.write(`[indexer] removed code ${rel}\n`);
241
+ enqueueCode(async () => {
242
+ (0, code_2.removeCodeFile)(codeGraph, rel);
243
+ if (knowledgeGraph)
244
+ (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'code', codeGraph, config.projectId);
245
+ if (taskGraph)
246
+ (0, task_1.cleanupProxies)(taskGraph, 'code', codeGraph, config.projectId);
247
+ if (skillGraph)
248
+ (0, skill_1.cleanupProxies)(skillGraph, 'code', codeGraph, config.projectId);
249
+ process.stderr.write(`[indexer] removed code ${rel}\n`);
250
+ });
241
251
  }
242
252
  if (fileIndexGraph && !isExcluded(rel, filesExclude)) {
243
- (0, file_index_1.removeFileEntry)(fileIndexGraph, rel);
244
- if (knowledgeGraph)
245
- (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'files', fileIndexGraph, config.projectId);
246
- if (taskGraph)
247
- (0, task_1.cleanupProxies)(taskGraph, 'files', fileIndexGraph, config.projectId);
248
- if (skillGraph)
249
- (0, skill_1.cleanupProxies)(skillGraph, 'files', fileIndexGraph, config.projectId);
253
+ enqueueFile(async () => {
254
+ (0, file_index_1.removeFileEntry)(fileIndexGraph, rel);
255
+ if (knowledgeGraph)
256
+ (0, knowledge_1.cleanupProxies)(knowledgeGraph, 'files', fileIndexGraph, config.projectId);
257
+ if (taskGraph)
258
+ (0, task_1.cleanupProxies)(taskGraph, 'files', fileIndexGraph, config.projectId);
259
+ if (skillGraph)
260
+ (0, skill_1.cleanupProxies)(skillGraph, 'files', fileIndexGraph, config.projectId);
261
+ process.stderr.write(`[indexer] removed file ${rel}\n`);
262
+ });
250
263
  }
251
264
  }
252
265
  // ---------------------------------------------------------------------------
@@ -36,10 +36,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.MAX_ATTACHMENTS_PER_ENTITY = exports.MAX_ATTACHMENT_SIZE = void 0;
39
40
  exports.scanAttachments = scanAttachments;
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  const mime_1 = __importDefault(require("mime"));
44
+ /** Maximum size of a single attachment in bytes (10 MB). */
45
+ exports.MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
46
+ /** Maximum number of attachments per entity (note/task/skill). */
47
+ exports.MAX_ATTACHMENTS_PER_ENTITY = 20;
43
48
  /**
44
49
  * Scan the attachments/ subdirectory of an entity directory.
45
50
  * Returns metadata for each file found.
@@ -22,6 +22,8 @@ const code_1 = require("../lib/search/code");
22
22
  const files_1 = require("../lib/search/files");
23
23
  const bm25_1 = require("../lib/search/bm25");
24
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");
25
27
  // ---------------------------------------------------------------------------
26
28
  // CRUD
27
29
  // ---------------------------------------------------------------------------
@@ -87,10 +89,10 @@ function resolvePendingImports(graph) {
87
89
  }
88
90
  /**
89
91
  * Resolve pending extends/implements edges after all files have been indexed.
90
- * Searches the entire graph for nodes matching toName (by symbol name).
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).
91
94
  */
92
95
  function resolvePendingEdges(graph) {
93
- // Build name → nodeId index for fast lookup
94
96
  const nameIndex = new Map();
95
97
  graph.forEachNode((id, attrs) => {
96
98
  if (attrs.kind === 'class' || attrs.kind === 'interface') {
@@ -99,6 +101,18 @@ function resolvePendingEdges(graph) {
99
101
  nameIndex.set(attrs.name, list);
100
102
  }
101
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
+ });
102
116
  let created = 0;
103
117
  graph.forEachNode((id, attrs) => {
104
118
  if (!attrs.pendingEdges || attrs.pendingEdges.length === 0)
@@ -107,8 +121,20 @@ function resolvePendingEdges(graph) {
107
121
  for (const edge of attrs.pendingEdges) {
108
122
  const candidates = nameIndex.get(edge.toName);
109
123
  if (candidates && candidates.length > 0 && graph.hasNode(edge.from)) {
110
- // Use first match (ambiguity is rare in practice)
111
- const toId = candidates[0];
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
+ }
112
138
  if (toId !== edge.from && !graph.hasEdge(edge.from, toId)) {
113
139
  graph.addEdgeWithKey(`${edge.from}→${toId}`, edge.from, toId, { kind: edge.kind });
114
140
  created++;
@@ -142,7 +168,7 @@ function getCodeFileMtime(graph, fileId) {
142
168
  return graph.getNodeAttribute(nodes[0], 'mtime');
143
169
  }
144
170
  /** List all indexed files with symbol counts. */
145
- function listCodeFiles(graph, filter, limit = 20) {
171
+ function listCodeFiles(graph, filter, limit = defaults_1.LIST_LIMIT_SMALL) {
146
172
  const files = new Map();
147
173
  const lowerFilter = filter?.toLowerCase();
148
174
  graph.forEachNode((_, attrs) => {
@@ -182,10 +208,10 @@ function loadCodeGraph(graphMemory, fresh = false, embeddingFingerprint) {
182
208
  if (fresh)
183
209
  return graph;
184
210
  const file = path_1.default.join(graphMemory, 'code.json');
185
- if (!fs_1.default.existsSync(file))
211
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
212
+ if (!data)
186
213
  return graph;
187
214
  try {
188
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
189
215
  const stored = data.embeddingModel;
190
216
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
191
217
  process.stderr.write(`[code-graph] Embedding config changed, re-indexing code graph\n`);
@@ -207,7 +233,7 @@ class CodeGraphManager {
207
233
  _graph;
208
234
  embedFns;
209
235
  ext;
210
- _bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment} ${attrs.body}`);
236
+ _bm25Index = new bm25_1.BM25Index((attrs) => `${attrs.name} ${attrs.signature} ${attrs.docComment} ${attrs.body.slice(0, defaults_1.BM25_BODY_MAX_CHARS)}`);
211
237
  constructor(_graph, embedFns, ext = {}) {
212
238
  this._graph = _graph;
213
239
  this.embedFns = embedFns;
@@ -21,6 +21,8 @@ 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
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");
24
26
  function createGraph() {
25
27
  return new graphology_1.DirectedGraph({ multi: false, allowSelfLoops: false });
26
28
  }
@@ -108,7 +110,7 @@ function getFileMtime(graph, fileId) {
108
110
  return 0;
109
111
  return graph.getNodeAttribute(nodes[0], 'mtime');
110
112
  }
111
- function listFiles(graph, filter, limit = 20) {
113
+ function listFiles(graph, filter, limit = defaults_1.LIST_LIMIT_SMALL) {
112
114
  const files = new Map();
113
115
  const lowerFilter = filter?.toLowerCase();
114
116
  graph.forEachNode((_, attrs) => {
@@ -153,10 +155,10 @@ function loadGraph(graphMemory, fresh = false, embeddingFingerprint) {
153
155
  if (fresh)
154
156
  return graph;
155
157
  const file = path_1.default.join(graphMemory, 'docs.json');
156
- if (!fs_1.default.existsSync(file))
158
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
159
+ if (!data)
157
160
  return graph;
158
161
  try {
159
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
160
162
  const stored = data.embeddingModel;
161
163
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
162
164
  process.stderr.write(`[graph] Embedding config changed, re-indexing docs graph\n`);
@@ -20,6 +20,8 @@ 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
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");
23
25
  // ---------------------------------------------------------------------------
24
26
  // CRUD
25
27
  // ---------------------------------------------------------------------------
@@ -148,7 +150,7 @@ function getFileEntryMtime(graph, filePath) {
148
150
  * Otherwise returns all file nodes matching the filters.
149
151
  */
150
152
  function listAllFiles(graph, options = {}) {
151
- const { directory, extension, language, filter, limit = 50 } = options;
153
+ const { directory, extension, language, filter, limit = defaults_1.LIST_LIMIT_LARGE } = options;
152
154
  const lowerFilter = filter?.toLowerCase();
153
155
  const results = [];
154
156
  if (directory !== undefined) {
@@ -261,10 +263,10 @@ function loadFileIndexGraph(graphMemory, fresh = false, embeddingFingerprint) {
261
263
  if (fresh)
262
264
  return graph;
263
265
  const file = path_1.default.join(graphMemory, 'file-index.json');
264
- if (!fs_1.default.existsSync(file))
266
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
267
+ if (!data)
265
268
  return graph;
266
269
  try {
267
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
268
270
  const stored = data.embeddingModel;
269
271
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
270
272
  process.stderr.write(`[file-index] Embedding config changed, re-indexing file index\n`);
@@ -29,6 +29,8 @@ 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
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");
32
34
  const attachment_types_1 = require("../graphs/attachment-types");
33
35
  const file_import_1 = require("../lib/file-import");
34
36
  // ---------------------------------------------------------------------------
@@ -162,7 +164,7 @@ function getNote(graph, noteId) {
162
164
  return { id: noteId, ...graph.getNodeAttributes(noteId) };
163
165
  }
164
166
  /** List notes with optional filter (substring in title/id) and tag filter. Excludes proxy nodes. */
165
- function listNotes(graph, filter, tag, limit = 20) {
167
+ function listNotes(graph, filter, tag, limit = defaults_1.LIST_LIMIT_SMALL) {
166
168
  const lowerFilter = filter?.toLowerCase();
167
169
  const lowerTag = tag?.toLowerCase();
168
170
  const results = [];
@@ -179,7 +181,7 @@ function listNotes(graph, filter, tag, limit = 20) {
179
181
  if (!attrs.tags.some(t => t.toLowerCase() === lowerTag))
180
182
  return;
181
183
  }
182
- 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 });
183
185
  });
184
186
  return results
185
187
  .sort((a, b) => b.updatedAt - a.updatedAt)
@@ -349,10 +351,10 @@ function loadKnowledgeGraph(graphMemory, fresh = false, embeddingFingerprint) {
349
351
  if (fresh)
350
352
  return graph;
351
353
  const file = path_1.default.join(graphMemory, 'knowledge.json');
352
- if (!fs_1.default.existsSync(file))
354
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
355
+ if (!data)
353
356
  return graph;
354
357
  try {
355
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
356
358
  const stored = data.embeddingModel;
357
359
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
358
360
  process.stderr.write(`[knowledge-graph] Embedding config changed, re-indexing knowledge graph\n`);
@@ -601,6 +603,11 @@ class KnowledgeGraphManager {
601
603
  return null;
602
604
  if (!this._graph.hasNode(noteId) || isProxy(this._graph, noteId))
603
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;
604
611
  const safe = (0, file_mirror_1.sanitizeFilename)(filename);
605
612
  if (!safe)
606
613
  return null;
@@ -31,6 +31,8 @@ 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
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");
34
36
  const attachment_types_1 = require("../graphs/attachment-types");
35
37
  const file_import_1 = require("../lib/file-import");
36
38
  // ---------------------------------------------------------------------------
@@ -290,7 +292,7 @@ function getSkill(graph, skillId) {
290
292
  }
291
293
  /** List skills with optional filters. Excludes proxy nodes. */
292
294
  function listSkills(graph, opts = {}) {
293
- const { source, tag, filter, limit = 50 } = opts;
295
+ const { source, tag, filter, limit = defaults_1.LIST_LIMIT_LARGE } = opts;
294
296
  const lowerFilter = filter?.toLowerCase();
295
297
  const lowerTag = tag?.toLowerCase();
296
298
  const results = [];
@@ -310,7 +312,7 @@ function listSkills(graph, opts = {}) {
310
312
  results.push({
311
313
  id,
312
314
  title: attrs.title,
313
- description: attrs.description?.slice(0, 500),
315
+ description: attrs.description?.slice(0, defaults_1.CONTENT_PREVIEW_LEN),
314
316
  steps: attrs.steps,
315
317
  triggers: attrs.triggers,
316
318
  inputHints: attrs.inputHints,
@@ -493,10 +495,10 @@ function loadSkillGraph(graphMemory, fresh = false, embeddingFingerprint) {
493
495
  if (fresh)
494
496
  return graph;
495
497
  const file = path_1.default.join(graphMemory, 'skills.json');
496
- if (!fs_1.default.existsSync(file))
498
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
499
+ if (!data)
497
500
  return graph;
498
501
  try {
499
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
500
502
  const stored = data.embeddingModel;
501
503
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
502
504
  process.stderr.write(`[skill-graph] Embedding config changed, re-indexing skill graph\n`);
@@ -829,10 +831,15 @@ class SkillGraphManager {
829
831
  return null;
830
832
  if (!this._graph.hasNode(skillId) || isProxy(this._graph, skillId))
831
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;
832
840
  const safe = (0, file_mirror_1.sanitizeFilename)(filename);
833
841
  if (!safe)
834
842
  return null;
835
- const entityDir = path_1.default.join(dir, skillId);
836
843
  (0, file_mirror_1.writeAttachment)(dir, skillId, safe, data);
837
844
  this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'attachments', safe));
838
845
  (0, file_mirror_1.mirrorAttachmentEvent)(entityDir, 'add', safe);
@@ -31,6 +31,8 @@ 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
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");
34
36
  const attachment_types_1 = require("../graphs/attachment-types");
35
37
  const file_import_1 = require("../lib/file-import");
36
38
  // ---------------------------------------------------------------------------
@@ -301,7 +303,7 @@ function getTask(graph, taskId) {
301
303
  }
302
304
  /** List tasks with optional filters. Excludes proxy nodes. */
303
305
  function listTasks(graph, opts = {}) {
304
- const { status, priority, tag, filter, assignee, limit = 50 } = opts;
306
+ const { status, priority, tag, filter, assignee, limit = defaults_1.LIST_LIMIT_LARGE } = opts;
305
307
  const lowerFilter = filter?.toLowerCase();
306
308
  const lowerTag = tag?.toLowerCase();
307
309
  const results = [];
@@ -325,7 +327,7 @@ function listTasks(graph, opts = {}) {
325
327
  results.push({
326
328
  id,
327
329
  title: attrs.title,
328
- description: attrs.description?.slice(0, 500),
330
+ description: attrs.description?.slice(0, defaults_1.CONTENT_PREVIEW_LEN),
329
331
  status: attrs.status,
330
332
  priority: attrs.priority,
331
333
  tags: attrs.tags,
@@ -514,10 +516,10 @@ function loadTaskGraph(graphMemory, fresh = false, embeddingFingerprint) {
514
516
  if (fresh)
515
517
  return graph;
516
518
  const file = path_1.default.join(graphMemory, 'tasks.json');
517
- if (!fs_1.default.existsSync(file))
519
+ const data = (0, graph_persistence_1.readJsonWithTmpFallback)(file);
520
+ if (!data)
518
521
  return graph;
519
522
  try {
520
- const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
521
523
  const stored = data.embeddingModel;
522
524
  if (embeddingFingerprint && stored !== embeddingFingerprint) {
523
525
  process.stderr.write(`[task-graph] Embedding config changed, re-indexing task graph\n`);
@@ -800,10 +802,15 @@ class TaskGraphManager {
800
802
  return null;
801
803
  if (!this._graph.hasNode(taskId) || isProxy(this._graph, taskId))
802
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;
803
811
  const safe = (0, file_mirror_1.sanitizeFilename)(filename);
804
812
  if (!safe)
805
813
  return null;
806
- const entityDir = path_1.default.join(dir, taskId);
807
814
  (0, file_mirror_1.writeAttachment)(dir, taskId, safe, data);
808
815
  this.mirrorTracker?.recordWrite(path_1.default.join(entityDir, 'attachments', safe));
809
816
  (0, file_mirror_1.mirrorAttachmentEvent)(entityDir, 'add', safe);
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Central named constants for all tunable values.
4
+ * Import from here instead of hardcoding magic numbers.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.WIKI_MAX_DEPTH = exports.DEFAULT_EMBEDDING_CACHE_SIZE = exports.MIRROR_MTIME_TOLERANCE_MS = exports.MIRROR_MAX_ENTRIES = exports.MIRROR_STALE_MS = exports.ERROR_BODY_LIMIT = exports.GRACEFUL_SHUTDOWN_TIMEOUT_MS = exports.SESSION_SWEEP_INTERVAL_MS = exports.RATE_LIMIT_WINDOW_MS = exports.REMOTE_BASE_DELAY_MS = exports.REMOTE_MAX_RETRIES = exports.WS_DEBOUNCE_MS = exports.AUTO_SAVE_INTERVAL_MS = exports.MAX_PASSWORD_LEN = exports.MAX_ATTACHMENT_FILENAME_LEN = exports.MAX_PROJECT_ID_LEN = exports.MAX_LINK_KIND_LEN = exports.MAX_TARGET_NODE_ID_LEN = exports.MAX_SKILL_TRIGGERS_COUNT = exports.MAX_SKILL_TRIGGER_LEN = exports.MAX_SKILL_STEPS_COUNT = exports.MAX_SKILL_STEP_LEN = exports.MAX_ASSIGNEE_LEN = exports.MAX_DESCRIPTION_LEN = exports.MAX_SEARCH_TOP_K = exports.MAX_SEARCH_QUERY_LEN = exports.MAX_TAGS_COUNT = exports.MAX_TAG_LEN = exports.MAX_NOTE_CONTENT_LEN = exports.MAX_TITLE_LEN = exports.INDEXER_PREVIEW_LEN = exports.CONTENT_PREVIEW_LEN = exports.LIST_LIMIT_LARGE = exports.LIST_LIMIT_SMALL = exports.SIGNATURE_MAX_LEN = exports.MAX_UPLOAD_SIZE = exports.MAX_BODY_SIZE = exports.BM25_BODY_MAX_CHARS = exports.FILE_SEARCH_TOP_K = exports.SEARCH_MIN_SCORE_FILES = exports.SEARCH_MIN_SCORE_CODE = exports.SEARCH_MIN_SCORE = exports.SEARCH_BFS_DECAY = exports.SEARCH_MAX_RESULTS = exports.SEARCH_BFS_DEPTH = exports.SEARCH_TOP_K = exports.RRF_K = exports.BM25_IDF_OFFSET = exports.BM25_B = exports.BM25_K1 = void 0;
8
+ // ---------------------------------------------------------------------------
9
+ // Search — BM25, RRF, BFS
10
+ // ---------------------------------------------------------------------------
11
+ exports.BM25_K1 = 1.2;
12
+ exports.BM25_B = 0.75;
13
+ exports.BM25_IDF_OFFSET = 0.5;
14
+ exports.RRF_K = 60;
15
+ exports.SEARCH_TOP_K = 5;
16
+ exports.SEARCH_BFS_DEPTH = 1;
17
+ exports.SEARCH_MAX_RESULTS = 20;
18
+ exports.SEARCH_BFS_DECAY = 0.8;
19
+ exports.SEARCH_MIN_SCORE = 0.5;
20
+ exports.SEARCH_MIN_SCORE_CODE = 0.3;
21
+ exports.SEARCH_MIN_SCORE_FILES = 0.3;
22
+ exports.FILE_SEARCH_TOP_K = 10;
23
+ exports.BM25_BODY_MAX_CHARS = 2000;
24
+ // ---------------------------------------------------------------------------
25
+ // Limits — sizes, counts, truncation
26
+ // ---------------------------------------------------------------------------
27
+ exports.MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB
28
+ exports.MAX_UPLOAD_SIZE = 50 * 1024 * 1024; // 50 MB (multer)
29
+ exports.SIGNATURE_MAX_LEN = 300;
30
+ exports.LIST_LIMIT_SMALL = 20;
31
+ exports.LIST_LIMIT_LARGE = 50;
32
+ exports.CONTENT_PREVIEW_LEN = 500;
33
+ exports.INDEXER_PREVIEW_LEN = 200;
34
+ // ---------------------------------------------------------------------------
35
+ // Validation — REST API schema limits
36
+ // ---------------------------------------------------------------------------
37
+ exports.MAX_TITLE_LEN = 500;
38
+ exports.MAX_NOTE_CONTENT_LEN = 1_000_000;
39
+ exports.MAX_TAG_LEN = 100;
40
+ exports.MAX_TAGS_COUNT = 100;
41
+ exports.MAX_SEARCH_QUERY_LEN = 2000;
42
+ exports.MAX_SEARCH_TOP_K = 500;
43
+ exports.MAX_DESCRIPTION_LEN = 500_000;
44
+ exports.MAX_ASSIGNEE_LEN = 100;
45
+ exports.MAX_SKILL_STEP_LEN = 10_000;
46
+ exports.MAX_SKILL_STEPS_COUNT = 100;
47
+ exports.MAX_SKILL_TRIGGER_LEN = 500;
48
+ exports.MAX_SKILL_TRIGGERS_COUNT = 50;
49
+ exports.MAX_TARGET_NODE_ID_LEN = 500;
50
+ exports.MAX_LINK_KIND_LEN = 100;
51
+ exports.MAX_PROJECT_ID_LEN = 200;
52
+ exports.MAX_ATTACHMENT_FILENAME_LEN = 255;
53
+ exports.MAX_PASSWORD_LEN = 256;
54
+ // ---------------------------------------------------------------------------
55
+ // Timing — intervals, retries, timeouts
56
+ // ---------------------------------------------------------------------------
57
+ exports.AUTO_SAVE_INTERVAL_MS = 30_000;
58
+ exports.WS_DEBOUNCE_MS = 1000;
59
+ exports.REMOTE_MAX_RETRIES = 3;
60
+ exports.REMOTE_BASE_DELAY_MS = 200;
61
+ exports.RATE_LIMIT_WINDOW_MS = 60_000;
62
+ exports.SESSION_SWEEP_INTERVAL_MS = 60_000;
63
+ exports.GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5000;
64
+ exports.ERROR_BODY_LIMIT = 500;
65
+ // ---------------------------------------------------------------------------
66
+ // Mirror
67
+ // ---------------------------------------------------------------------------
68
+ exports.MIRROR_STALE_MS = 10_000;
69
+ exports.MIRROR_MAX_ENTRIES = 10_000;
70
+ exports.MIRROR_MTIME_TOLERANCE_MS = 100;
71
+ // ---------------------------------------------------------------------------
72
+ // Embedder
73
+ // ---------------------------------------------------------------------------
74
+ exports.DEFAULT_EMBEDDING_CACHE_SIZE = 10_000;
75
+ // ---------------------------------------------------------------------------
76
+ // Parser
77
+ // ---------------------------------------------------------------------------
78
+ exports.WIKI_MAX_DEPTH = 10;
@@ -12,10 +12,11 @@ exports.cosineSimilarity = cosineSimilarity;
12
12
  const transformers_1 = require("@huggingface/transformers");
13
13
  const fs_1 = __importDefault(require("fs"));
14
14
  const path_1 = __importDefault(require("path"));
15
+ const defaults_1 = require("../lib/defaults");
15
16
  // ---------------------------------------------------------------------------
16
17
  // LRU cache for embedding vectors (avoids re-computing identical texts)
17
18
  // ---------------------------------------------------------------------------
18
- const DEFAULT_CACHE_SIZE = 10_000;
19
+ const DEFAULT_CACHE_SIZE = defaults_1.DEFAULT_EMBEDDING_CACHE_SIZE;
19
20
  class LruCache {
20
21
  maxSize;
21
22
  map = new Map();
@@ -97,26 +98,24 @@ async function loadModel(model, embedding, modelsDir, name = 'default') {
97
98
  // ---------------------------------------------------------------------------
98
99
  // Remote embedding HTTP client
99
100
  // ---------------------------------------------------------------------------
100
- const REMOTE_MAX_RETRIES = 3;
101
- const REMOTE_BASE_DELAY_MS = 200;
102
101
  async function remoteEmbed(url, texts, apiKey) {
103
102
  const headers = { 'Content-Type': 'application/json' };
104
103
  if (apiKey)
105
104
  headers['Authorization'] = `Bearer ${apiKey}`;
106
105
  const body = JSON.stringify({ texts });
107
- for (let attempt = 0; attempt < REMOTE_MAX_RETRIES; attempt++) {
106
+ for (let attempt = 0; attempt < defaults_1.REMOTE_MAX_RETRIES; attempt++) {
108
107
  let resp;
109
108
  try {
110
109
  resp = await fetch(url, { method: 'POST', headers, body });
111
110
  }
112
111
  catch (err) {
113
112
  // Network error — retry
114
- if (attempt < REMOTE_MAX_RETRIES - 1) {
115
- const delay = REMOTE_BASE_DELAY_MS * 2 ** attempt;
113
+ if (attempt < defaults_1.REMOTE_MAX_RETRIES - 1) {
114
+ const delay = defaults_1.REMOTE_BASE_DELAY_MS * 2 ** attempt;
116
115
  await new Promise(r => setTimeout(r, delay));
117
116
  continue;
118
117
  }
119
- throw new Error(`Remote embed network error after ${REMOTE_MAX_RETRIES} attempts: ${err}`);
118
+ throw new Error(`Remote embed network error after ${defaults_1.REMOTE_MAX_RETRIES} attempts: ${err}`);
120
119
  }
121
120
  if (resp.ok) {
122
121
  const data = await resp.json();
@@ -124,17 +123,17 @@ async function remoteEmbed(url, texts, apiKey) {
124
123
  }
125
124
  // Client errors (4xx) — don't retry
126
125
  if (resp.status < 500) {
127
- const respBody = (await resp.text()).slice(0, 500);
126
+ const respBody = (await resp.text()).slice(0, defaults_1.ERROR_BODY_LIMIT);
128
127
  throw new Error(`Remote embed failed (${resp.status}): ${respBody}`);
129
128
  }
130
129
  // Server errors (5xx) — retry
131
- if (attempt < REMOTE_MAX_RETRIES - 1) {
132
- const delay = REMOTE_BASE_DELAY_MS * 2 ** attempt;
130
+ if (attempt < defaults_1.REMOTE_MAX_RETRIES - 1) {
131
+ const delay = defaults_1.REMOTE_BASE_DELAY_MS * 2 ** attempt;
133
132
  await new Promise(r => setTimeout(r, delay));
134
133
  continue;
135
134
  }
136
- const respBody = (await resp.text()).slice(0, 500);
137
- throw new Error(`Remote embed failed after ${REMOTE_MAX_RETRIES} attempts (${resp.status}): ${respBody}`);
135
+ const respBody = (await resp.text()).slice(0, defaults_1.ERROR_BODY_LIMIT);
136
+ throw new Error(`Remote embed failed after ${defaults_1.REMOTE_MAX_RETRIES} attempts (${resp.status}): ${respBody}`);
138
137
  }
139
138
  throw new Error('Remote embed: unreachable');
140
139
  }
@@ -5,17 +5,15 @@
5
5
  * Backwards compatible: detects old format (number[]) on load.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.float32ToBase64 = float32ToBase64;
9
+ exports.base64ToFloat32 = base64ToFloat32;
8
10
  exports.compressEmbeddings = compressEmbeddings;
9
11
  exports.decompressEmbeddings = decompressEmbeddings;
10
12
  const EMBEDDING_FIELDS = ['embedding', 'fileEmbedding'];
11
13
  /** Convert a number[] to a Base64-encoded Float32Array. */
12
14
  function float32ToBase64(arr) {
13
15
  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');
16
+ return Buffer.from(f32.buffer).toString('base64');
19
17
  }
20
18
  /** Convert a Base64-encoded Float32Array back to number[]. */
21
19
  function base64ToFloat32(b64) {