@graphmemory/server 1.3.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 (39) 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 +4 -3
  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 +5 -4
  15. package/dist/cli/indexer.js +2 -1
  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/mirror-watcher.js +4 -3
  28. package/dist/lib/parsers/docs.js +2 -1
  29. package/dist/lib/parsers/languages/typescript.js +34 -17
  30. package/dist/lib/project-manager.js +7 -1
  31. package/dist/lib/search/bm25.js +5 -4
  32. package/dist/lib/search/code.js +2 -1
  33. package/dist/lib/search/docs.js +2 -1
  34. package/dist/lib/search/file-index.js +2 -1
  35. package/dist/lib/search/files.js +3 -2
  36. package/dist/lib/search/knowledge.js +2 -1
  37. package/dist/lib/search/skills.js +2 -1
  38. package/dist/lib/search/tasks.js +2 -1
  39. package/package.json +5 -2
@@ -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) {
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readJsonWithTmpFallback = readJsonWithTmpFallback;
37
+ const fs = __importStar(require("fs"));
38
+ /**
39
+ * Try to read and parse a JSON file, falling back to .tmp if main file
40
+ * is missing or corrupted (recovery from interrupted save).
41
+ */
42
+ function readJsonWithTmpFallback(file) {
43
+ const tmp = file + '.tmp';
44
+ // If main file missing but .tmp exists, recover it
45
+ if (!fs.existsSync(file) && fs.existsSync(tmp)) {
46
+ try {
47
+ fs.renameSync(tmp, file);
48
+ }
49
+ catch { /* ignore */ }
50
+ }
51
+ if (fs.existsSync(file)) {
52
+ try {
53
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
54
+ }
55
+ catch {
56
+ // Main file corrupted — try .tmp as fallback
57
+ if (fs.existsSync(tmp)) {
58
+ try {
59
+ const data = JSON.parse(fs.readFileSync(tmp, 'utf-8'));
60
+ process.stderr.write(`[graph] Recovered from .tmp file: ${file}\n`);
61
+ return data;
62
+ }
63
+ catch { /* .tmp also bad */ }
64
+ }
65
+ }
66
+ }
67
+ return null;
68
+ }
@@ -45,6 +45,7 @@ const chokidar_1 = __importDefault(require("chokidar"));
45
45
  const file_import_1 = require("./file-import");
46
46
  const events_log_1 = require("./events-log");
47
47
  const frontmatter_1 = require("./frontmatter");
48
+ const defaults_1 = require("../lib/defaults");
48
49
  /**
49
50
  * Tracks recent mirror writes to suppress re-import (feedback loop prevention).
50
51
  * When mirrorNote/mirrorTask writes a file, the watcher will fire —
@@ -53,8 +54,8 @@ const frontmatter_1 = require("./frontmatter");
53
54
  class MirrorWriteTracker {
54
55
  /** Map from filePath → { mtimeMs (for comparison), recordedAt (for eviction) } */
55
56
  recentWrites = new Map();
56
- static STALE_MS = 10_000; // entries older than 10s are stale
57
- static MAX_ENTRIES = 10_000;
57
+ static STALE_MS = defaults_1.MIRROR_STALE_MS;
58
+ static MAX_ENTRIES = defaults_1.MIRROR_MAX_ENTRIES;
58
59
  /** Called by mirrorNote/mirrorTask after writing a file. */
59
60
  recordWrite(filePath) {
60
61
  try {
@@ -76,7 +77,7 @@ class MirrorWriteTracker {
76
77
  const stat = fs.statSync(filePath, { throwIfNoEntry: false });
77
78
  if (!stat)
78
79
  return false;
79
- if (Math.abs(stat.mtimeMs - recorded.mtimeMs) < 100) {
80
+ if (Math.abs(stat.mtimeMs - recorded.mtimeMs) < defaults_1.MIRROR_MTIME_TOLERANCE_MS) {
80
81
  this.recentWrites.delete(filePath);
81
82
  return true;
82
83
  }
@@ -8,6 +8,7 @@ exports.clearWikiIndexCache = clearWikiIndexCache;
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const codeblock_1 = require("../../lib/parsers/codeblock");
11
+ const defaults_1 = require("../../lib/defaults");
11
12
  // Parse a markdown file into chunks split by headings
12
13
  async function parseFile(content, absolutePath, projectDir, chunkDepth) {
13
14
  const fileId = path_1.default.relative(projectDir, absolutePath);
@@ -179,7 +180,7 @@ function getWikiIndex(projectDir) {
179
180
  if (_wikiIndex.has(projectDir))
180
181
  return _wikiIndex.get(projectDir);
181
182
  const index = new Map();
182
- const MAX_DEPTH = 10;
183
+ const MAX_DEPTH = defaults_1.WIKI_MAX_DEPTH;
183
184
  function walk(dir, depth) {
184
185
  if (depth >= MAX_DEPTH)
185
186
  return;
@@ -2,11 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerTypescript = registerTypescript;
4
4
  const registry_1 = require("./registry");
5
+ const defaults_1 = require("../../../lib/defaults");
5
6
  /** Get the previous named sibling that is a JSDoc comment. */
6
7
  function getDocComment(node) {
7
- let prev = node.previousSibling;
8
+ let prev = node.previousNamedSibling;
8
9
  while (prev && prev.type === 'comment' && !prev.text.startsWith('/**')) {
9
- prev = prev.previousSibling;
10
+ prev = prev.previousNamedSibling;
10
11
  }
11
12
  if (prev && prev.type === 'comment' && prev.text.startsWith('/**')) {
12
13
  return prev.text.trim();
@@ -14,28 +15,45 @@ function getDocComment(node) {
14
15
  return '';
15
16
  }
16
17
  /** Collapse whitespace and truncate. */
17
- function truncate(text, maxLen = 300) {
18
+ function truncate(text, maxLen = defaults_1.SIGNATURE_MAX_LEN) {
18
19
  const collapsed = text.replace(/\s+/g, ' ').trim();
19
20
  return collapsed.length > maxLen ? collapsed.slice(0, maxLen) + '…' : collapsed;
20
21
  }
22
+ /**
23
+ * Slice outerNode.text up to where bodyNode begins, using line-based
24
+ * slicing to avoid tree-sitter byte-offset vs JS char-offset mismatch
25
+ * (startIndex is UTF-8 bytes, String.slice uses UTF-16 code units).
26
+ */
27
+ function sliceBeforeBody(outerNode, bodyNode) {
28
+ const text = outerNode.text ?? '';
29
+ const outerStartRow = outerNode.startPosition.row;
30
+ const bodyStartRow = bodyNode.startPosition.row;
31
+ if (bodyStartRow > outerStartRow) {
32
+ const lines = text.split('\n');
33
+ const relativeRow = bodyStartRow - outerStartRow;
34
+ const beforeBody = lines.slice(0, relativeRow);
35
+ const bodyLine = lines[relativeRow] ?? '';
36
+ const braceIdx = bodyLine.indexOf('{');
37
+ if (braceIdx >= 0)
38
+ beforeBody.push(bodyLine.slice(0, braceIdx));
39
+ return beforeBody.join('\n');
40
+ }
41
+ const braceIdx = text.indexOf('{');
42
+ if (braceIdx > 0)
43
+ return text.slice(0, braceIdx);
44
+ return null;
45
+ }
21
46
  /**
22
47
  * Build signature by taking everything before the body node.
23
- * For code (ASCII-dominated), byte offset ≈ char offset.
24
48
  * Falls back to first line if no body found.
25
49
  */
26
50
  function buildSignature(outerNode, innerNode) {
27
51
  const bodyNode = innerNode.childForFieldName('body');
28
52
  const text = outerNode.text ?? '';
29
- if (!bodyNode) {
30
- // No body (type alias, ambient declaration, etc.) — use full text
53
+ if (!bodyNode)
31
54
  return truncate(text);
32
- }
33
- // Slice text from outer start up to body start
34
- const headerBytes = bodyNode.startIndex - outerNode.startIndex;
35
- if (headerBytes > 0) {
36
- return truncate(text.slice(0, headerBytes));
37
- }
38
- return truncate(text.split('\n')[0]);
55
+ const header = sliceBeforeBody(outerNode, bodyNode);
56
+ return truncate(header ?? text.split('\n')[0]);
39
57
  }
40
58
  /**
41
59
  * Build signature for variable declarations.
@@ -46,10 +64,9 @@ function buildVariableSignature(outerNode, declarator) {
46
64
  if (value) {
47
65
  const valueBody = value.childForFieldName('body');
48
66
  if (valueBody) {
49
- const fullText = outerNode.text ?? '';
50
- const bodyOffset = valueBody.startIndex - outerNode.startIndex;
51
- if (bodyOffset > 0)
52
- return truncate(fullText.slice(0, bodyOffset));
67
+ const header = sliceBeforeBody(outerNode, valueBody);
68
+ if (header)
69
+ return truncate(header);
53
70
  }
54
71
  }
55
72
  return truncate(outerNode.text ?? '');
@@ -13,11 +13,14 @@ const file_index_1 = require("../graphs/file-index");
13
13
  const task_1 = require("../graphs/task");
14
14
  const skill_1 = require("../graphs/skill");
15
15
  const indexer_1 = require("../cli/indexer");
16
+ const code_2 = require("../lib/parsers/code");
17
+ const docs_2 = require("../lib/parsers/docs");
16
18
  const promise_queue_1 = require("../lib/promise-queue");
17
19
  const multi_config_1 = require("../lib/multi-config");
18
20
  const mirror_watcher_1 = require("../lib/mirror-watcher");
19
21
  const team_1 = require("../lib/team");
20
22
  const path_1 = __importDefault(require("path"));
23
+ const defaults_1 = require("../lib/defaults");
21
24
  // ---------------------------------------------------------------------------
22
25
  // ProjectManager
23
26
  // ---------------------------------------------------------------------------
@@ -264,6 +267,9 @@ class ProjectManager extends events_1.EventEmitter {
264
267
  const instance = this.projects.get(id);
265
268
  if (!instance)
266
269
  throw new Error(`Project "${id}" not found`);
270
+ // Clear parser caches to prevent cross-project leaks in multi-project mode
271
+ (0, code_2.clearPathMappingsCache)();
272
+ (0, docs_2.clearWikiIndexCache)();
267
273
  const gc = instance.config.graphConfigs;
268
274
  const indexer = (0, indexer_1.createProjectIndexer)(instance.docGraph, instance.codeGraph, {
269
275
  projectId: id,
@@ -358,7 +364,7 @@ class ProjectManager extends events_1.EventEmitter {
358
364
  /**
359
365
  * Start auto-save interval (every intervalMs, save dirty projects).
360
366
  */
361
- startAutoSave(intervalMs = 30_000) {
367
+ startAutoSave(intervalMs = defaults_1.AUTO_SAVE_INTERVAL_MS) {
362
368
  this.autoSaveInterval = setInterval(() => {
363
369
  for (const instance of this.projects.values()) {
364
370
  if (instance.dirty) {