@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
@@ -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
  }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Encode/decode embedding vectors as Base64 for compact JSON serialization.
4
+ * Float32Array → Base64 string saves ~3x vs JSON number arrays.
5
+ * Backwards compatible: detects old format (number[]) on load.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.float32ToBase64 = float32ToBase64;
9
+ exports.base64ToFloat32 = base64ToFloat32;
10
+ exports.compressEmbeddings = compressEmbeddings;
11
+ exports.decompressEmbeddings = decompressEmbeddings;
12
+ const EMBEDDING_FIELDS = ['embedding', 'fileEmbedding'];
13
+ /** Convert a number[] to a Base64-encoded Float32Array. */
14
+ function float32ToBase64(arr) {
15
+ const f32 = new Float32Array(arr);
16
+ return Buffer.from(f32.buffer).toString('base64');
17
+ }
18
+ /** Convert a Base64-encoded Float32Array back to number[]. */
19
+ function base64ToFloat32(b64) {
20
+ const buf = Buffer.from(b64, 'base64');
21
+ // Copy to aligned buffer to guarantee 4-byte alignment for Float32Array
22
+ const aligned = new Uint8Array(buf.byteLength);
23
+ aligned.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
24
+ const f32 = new Float32Array(aligned.buffer, 0, aligned.byteLength / 4);
25
+ return Array.from(f32);
26
+ }
27
+ /**
28
+ * Compress embedding fields in a graphology export object (mutates in place).
29
+ * Converts number[] → Base64 string for fields named 'embedding' or 'fileEmbedding'.
30
+ */
31
+ function compressEmbeddings(exported) {
32
+ if (!exported?.nodes)
33
+ return;
34
+ for (const node of exported.nodes) {
35
+ const attrs = node.attributes;
36
+ if (!attrs)
37
+ continue;
38
+ for (const field of EMBEDDING_FIELDS) {
39
+ if (Array.isArray(attrs[field]) && attrs[field].length > 0) {
40
+ attrs[field] = float32ToBase64(attrs[field]);
41
+ }
42
+ }
43
+ }
44
+ }
45
+ /**
46
+ * Decompress embedding fields in a graphology export object (mutates in place).
47
+ * Converts Base64 string → number[]. Handles both old format (number[]) and new (string).
48
+ */
49
+ function decompressEmbeddings(exported) {
50
+ if (!exported?.nodes)
51
+ return;
52
+ for (const node of exported.nodes) {
53
+ const attrs = node.attributes;
54
+ if (!attrs)
55
+ continue;
56
+ for (const field of EMBEDDING_FIELDS) {
57
+ if (typeof attrs[field] === 'string' && attrs[field].length > 0) {
58
+ attrs[field] = base64ToFloat32(attrs[field]);
59
+ }
60
+ // number[] stays as-is (backwards compatible with old format)
61
+ }
62
+ }
63
+ }
@@ -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
+ }
package/dist/lib/jwt.js CHANGED
@@ -85,15 +85,15 @@ function parseTtl(ttl) {
85
85
  }
86
86
  function signAccessToken(userId, secret, ttl) {
87
87
  const payload = { userId, type: 'access' };
88
- return jsonwebtoken_1.default.sign(payload, secret, { expiresIn: parseTtl(ttl) });
88
+ return jsonwebtoken_1.default.sign(payload, secret, { algorithm: 'HS256', expiresIn: parseTtl(ttl) });
89
89
  }
90
90
  function signRefreshToken(userId, secret, ttl) {
91
91
  const payload = { userId, type: 'refresh' };
92
- return jsonwebtoken_1.default.sign(payload, secret, { expiresIn: parseTtl(ttl) });
92
+ return jsonwebtoken_1.default.sign(payload, secret, { algorithm: 'HS256', expiresIn: parseTtl(ttl) });
93
93
  }
94
94
  function verifyToken(token, secret) {
95
95
  try {
96
- const decoded = jsonwebtoken_1.default.verify(token, secret);
96
+ const decoded = jsonwebtoken_1.default.verify(token, secret, { algorithms: ['HS256'] });
97
97
  if (!decoded.userId || !decoded.type)
98
98
  return null;
99
99
  return { userId: decoded.userId, type: decoded.type };
@@ -108,7 +108,7 @@ function verifyToken(token, secret) {
108
108
  const ACCESS_COOKIE = 'mgm_access';
109
109
  const REFRESH_COOKIE = 'mgm_refresh';
110
110
  function setAuthCookies(res, accessToken, refreshToken, accessTtl, refreshTtl) {
111
- const secure = process.env.NODE_ENV === 'production';
111
+ const secure = process.env.NODE_ENV !== 'development';
112
112
  res.cookie(ACCESS_COOKIE, accessToken, {
113
113
  httpOnly: true,
114
114
  secure,
@@ -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
  }
@@ -50,6 +50,7 @@ const userSchema = zod_1.z.object({
50
50
  });
51
51
  const graphConfigSchema = zod_1.z.object({
52
52
  enabled: zod_1.z.boolean().optional(),
53
+ readonly: zod_1.z.boolean().optional(),
53
54
  include: zod_1.z.string().optional(),
54
55
  exclude: excludeSchema,
55
56
  model: modelConfigSchema.optional(),
@@ -98,7 +99,7 @@ const serverSchema = zod_1.z.object({
98
99
  embeddingApi: embeddingApiSchema.optional(),
99
100
  defaultAccess: accessLevelSchema.optional(),
100
101
  access: accessMapSchema,
101
- jwtSecret: zod_1.z.string().optional(),
102
+ jwtSecret: zod_1.z.string().min(16).optional(),
102
103
  accessTokenTtl: zod_1.z.string().optional(),
103
104
  refreshTokenTtl: zod_1.z.string().optional(),
104
105
  rateLimit: rateLimitSchema.optional(),
@@ -107,6 +108,7 @@ const serverSchema = zod_1.z.object({
107
108
  });
108
109
  const wsGraphConfigSchema = zod_1.z.object({
109
110
  enabled: zod_1.z.boolean().optional(),
111
+ readonly: zod_1.z.boolean().optional(),
110
112
  exclude: excludeSchema,
111
113
  model: modelConfigSchema.optional(),
112
114
  embedding: embeddingConfigSchema.optional(),
@@ -301,6 +303,7 @@ function loadMultiConfig(yamlPath) {
301
303
  const graphExclude = [...projectExclude, ...parseExclude(gc?.exclude)];
302
304
  graphConfigs[gn] = {
303
305
  enabled: gc?.enabled ?? true,
306
+ readonly: gc?.readonly ?? false,
304
307
  include: gc?.include ?? (gn === 'docs' ? PROJECT_DEFAULTS.docsInclude : gn === 'code' ? PROJECT_DEFAULTS.codeInclude : undefined),
305
308
  exclude: graphExclude,
306
309
  model: resolveModel(gc?.model, projectModel),
@@ -348,6 +351,7 @@ function loadMultiConfig(yamlPath) {
348
351
  const gc = rawGraphs[gn];
349
352
  graphConfigs[gn] = {
350
353
  enabled: gc?.enabled ?? true,
354
+ readonly: gc?.readonly ?? false,
351
355
  exclude: [...wsExclude, ...parseExclude(gc?.exclude)],
352
356
  model: resolveModel(gc?.model, wsModel),
353
357
  embedding: resolveEmbedding(gc?.embedding, wsEmbedding),
@@ -418,6 +422,7 @@ function defaultConfig(projectDir) {
418
422
  for (const gn of exports.GRAPH_NAMES) {
419
423
  graphConfigs[gn] = {
420
424
  enabled: true,
425
+ readonly: false,
421
426
  include: gn === 'docs' ? PROJECT_DEFAULTS.docsInclude : gn === 'code' ? PROJECT_DEFAULTS.codeInclude : undefined,
422
427
  exclude: [...server.exclude],
423
428
  model: MODEL_DEFAULTS,
@@ -3,43 +3,157 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.clearPathMappingsCache = clearPathMappingsCache;
6
7
  exports.parseCodeFile = parseCodeFile;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const languages_1 = require("../../lib/parsers/languages");
10
11
  const file_lang_1 = require("../../graphs/file-lang");
12
+ // Strip line and block comments from JSONC, preserving string contents.
13
+ function stripJsoncComments(text) {
14
+ let result = '';
15
+ let i = 0;
16
+ while (i < text.length) {
17
+ // String literal — copy verbatim
18
+ if (text[i] === '"') {
19
+ const start = i++;
20
+ while (i < text.length && text[i] !== '"') {
21
+ if (text[i] === '\\')
22
+ i++; // skip escaped char
23
+ i++;
24
+ }
25
+ i++; // closing quote
26
+ result += text.slice(start, i);
27
+ // Line comment
28
+ }
29
+ else if (text[i] === '/' && text[i + 1] === '/') {
30
+ while (i < text.length && text[i] !== '\n')
31
+ i++;
32
+ // Block comment
33
+ }
34
+ else if (text[i] === '/' && text[i + 1] === '*') {
35
+ i += 2;
36
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
37
+ i++;
38
+ i += 2;
39
+ }
40
+ else {
41
+ result += text[i++];
42
+ }
43
+ }
44
+ return result;
45
+ }
11
46
  // ---------------------------------------------------------------------------
12
47
  // Import resolution — replaces ts-morph's getModuleSpecifierSourceFile()
13
48
  // ---------------------------------------------------------------------------
14
49
  const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs'];
15
- /** Resolve a relative import specifier to an absolute file path, or null. */
16
- function resolveRelativeImport(fromFile, specifier) {
17
- const dir = path_1.default.dirname(fromFile);
18
- const base = path_1.default.resolve(dir, specifier);
19
- // Exact match (e.g. './foo.ts')
50
+ function hasFile(p) {
51
+ try {
52
+ return fs_1.default.statSync(p).isFile();
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ /** Try resolving a base path with extensions and index files. */
59
+ function tryResolve(base) {
20
60
  if (hasFile(base))
21
61
  return base;
22
- // Try adding extensions (e.g. './foo' → './foo.ts')
23
62
  for (const ext of RESOLVE_EXTENSIONS) {
24
- const candidate = base + ext;
25
- if (hasFile(candidate))
26
- return candidate;
63
+ if (hasFile(base + ext))
64
+ return base + ext;
27
65
  }
28
- // Try index files (e.g. './foo' → './foo/index.ts')
29
66
  for (const ext of RESOLVE_EXTENSIONS) {
30
- const candidate = path_1.default.join(base, 'index' + ext);
31
- if (hasFile(candidate))
32
- return candidate;
67
+ const idx = path_1.default.join(base, 'index' + ext);
68
+ if (hasFile(idx))
69
+ return idx;
33
70
  }
34
71
  return null;
35
72
  }
36
- function hasFile(p) {
37
- try {
38
- return fs_1.default.statSync(p).isFile();
73
+ /** Resolve a relative import specifier to an absolute file path, or null. */
74
+ function resolveRelativeImport(fromFile, specifier) {
75
+ const dir = path_1.default.dirname(fromFile);
76
+ return tryResolve(path_1.default.resolve(dir, specifier));
77
+ }
78
+ /** Cache: directory → parsed path mappings (null = no tsconfig found up to root). */
79
+ const _pathMappings = new Map();
80
+ /** Clear cached path mappings (call between projects or on config change). */
81
+ function clearPathMappingsCache() { _pathMappings.clear(); }
82
+ /**
83
+ * Find the nearest tsconfig.json / jsconfig.json walking up from `dir` to `root`.
84
+ * Cached per directory — each directory remembers its resolved mappings.
85
+ */
86
+ function findPathMappings(dir, root) {
87
+ if (_pathMappings.has(dir))
88
+ return _pathMappings.get(dir);
89
+ // Try this directory
90
+ const result = _parseTsConfig(dir);
91
+ if (result) {
92
+ _pathMappings.set(dir, result);
93
+ return result;
39
94
  }
40
- catch {
41
- return false;
95
+ // Walk up unless we've reached the project root
96
+ const parent = path_1.default.dirname(dir);
97
+ if (dir === root || parent === dir) {
98
+ _pathMappings.set(dir, null);
99
+ return null;
100
+ }
101
+ const parentResult = findPathMappings(parent, root);
102
+ _pathMappings.set(dir, parentResult);
103
+ return parentResult;
104
+ }
105
+ function _parseTsConfig(dir) {
106
+ for (const name of ['tsconfig.json', 'jsconfig.json']) {
107
+ const configPath = path_1.default.join(dir, name);
108
+ if (!hasFile(configPath))
109
+ continue;
110
+ try {
111
+ // Strip JSONC comments while preserving string contents
112
+ const raw = stripJsoncComments(fs_1.default.readFileSync(configPath, 'utf-8'));
113
+ const config = JSON.parse(raw);
114
+ const compilerOptions = config.compilerOptions;
115
+ if (!compilerOptions?.paths)
116
+ continue;
117
+ const baseUrl = compilerOptions.baseUrl
118
+ ? path_1.default.resolve(dir, compilerOptions.baseUrl)
119
+ : dir;
120
+ const mappings = [];
121
+ for (const [pattern, targets] of Object.entries(compilerOptions.paths)) {
122
+ // Pattern like "@/*" → prefix "@/", or "utils/*" → prefix "utils/"
123
+ const prefix = pattern.endsWith('/*') ? pattern.slice(0, -1) : pattern;
124
+ const resolvedTargets = targets
125
+ .map(t => {
126
+ const target = t.endsWith('/*') ? t.slice(0, -1) : t;
127
+ return path_1.default.resolve(baseUrl, target);
128
+ });
129
+ mappings.push({ prefix, targets: resolvedTargets });
130
+ }
131
+ if (mappings.length > 0)
132
+ return mappings;
133
+ }
134
+ catch {
135
+ // Malformed config — skip
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+ /** Resolve a path-aliased import (e.g. @/lib/foo) using nearest tsconfig paths. */
141
+ function resolveAliasImport(specifier, fromFile, projectDir) {
142
+ const fileDir = path_1.default.dirname(fromFile);
143
+ const mappings = findPathMappings(fileDir, projectDir);
144
+ if (!mappings)
145
+ return null;
146
+ for (const mapping of mappings) {
147
+ if (specifier.startsWith(mapping.prefix)) {
148
+ const rest = specifier.slice(mapping.prefix.length);
149
+ for (const targetDir of mapping.targets) {
150
+ const resolved = tryResolve(path_1.default.join(targetDir, rest));
151
+ if (resolved)
152
+ return resolved;
153
+ }
154
+ }
42
155
  }
156
+ return null;
43
157
  }
44
158
  // ---------------------------------------------------------------------------
45
159
  // Main parser
@@ -62,8 +176,8 @@ async function parseCodeFile(absolutePath, codeDir, mtime) {
62
176
  };
63
177
  }
64
178
  const source = fs_1.default.readFileSync(absolutePath, 'utf-8');
65
- const rootNode = await (0, languages_1.parseSource)(source, language);
66
- if (!rootNode) {
179
+ const tree = await (0, languages_1.parseSource)(source, language);
180
+ if (!tree) {
67
181
  return {
68
182
  fileId,
69
183
  mtime,
@@ -74,17 +188,23 @@ async function parseCodeFile(absolutePath, codeDir, mtime) {
74
188
  edges: [],
75
189
  };
76
190
  }
191
+ const rootNode = tree.rootNode;
77
192
  const mapper = (0, languages_1.getMapper)(language);
78
- const symbols = mapper.extractSymbols(rootNode);
79
- const edgeInfos = mapper.extractEdges(rootNode);
80
- const imports = mapper.extractImports(rootNode);
193
+ let symbols, edgeInfos, imports, fileDocComment, importSummary, lastLine;
194
+ try {
195
+ symbols = mapper.extractSymbols(rootNode);
196
+ edgeInfos = mapper.extractEdges(rootNode);
197
+ imports = mapper.extractImports(rootNode);
198
+ fileDocComment = extractFileDocComment(rootNode);
199
+ importSummary = buildImportSummary(rootNode);
200
+ lastLine = (rootNode.endPosition?.row ?? 0) + 1;
201
+ }
202
+ finally {
203
+ tree.delete();
204
+ }
81
205
  const nodes = [];
82
206
  const edges = [];
83
207
  const fileNodeId = fileId;
84
- // --- File root node ---
85
- const fileDocComment = extractFileDocComment(rootNode);
86
- const importSummary = buildImportSummary(rootNode);
87
- const lastLine = (rootNode.endPosition?.row ?? 0) + 1;
88
208
  nodes.push({
89
209
  id: fileNodeId,
90
210
  attrs: makeFileAttrs(fileId, fileDocComment, importSummary, lastLine, mtime),
@@ -147,13 +267,20 @@ async function parseCodeFile(absolutePath, codeDir, mtime) {
147
267
  }
148
268
  // --- Import edges: file → imported file ---
149
269
  for (const imp of imports) {
150
- const targetAbsolute = resolveRelativeImport(absolutePath, imp.specifier);
270
+ let targetAbsolute = null;
271
+ if (imp.specifier.startsWith('./') || imp.specifier.startsWith('../')) {
272
+ // Relative import
273
+ targetAbsolute = resolveRelativeImport(absolutePath, imp.specifier);
274
+ }
275
+ else {
276
+ // Try path alias resolution (e.g. @/lib/foo, ~/utils)
277
+ targetAbsolute = resolveAliasImport(imp.specifier, absolutePath, codeDir);
278
+ }
151
279
  if (!targetAbsolute)
152
280
  continue;
153
- const targetRel = path_1.default.relative(codeDir, targetAbsolute);
154
- if (targetRel.startsWith('..') || path_1.default.isAbsolute(targetRel))
155
- continue;
156
281
  const targetFileId = path_1.default.relative(codeDir, targetAbsolute);
282
+ if (targetFileId.startsWith('..') || path_1.default.isAbsolute(targetFileId))
283
+ continue;
157
284
  if (targetFileId !== fileNodeId) {
158
285
  edges.push({
159
286
  from: fileNodeId,
@@ -6,7 +6,7 @@ const languages_1 = require("../../lib/parsers/languages");
6
6
  const TAG_TO_LANGUAGE = {
7
7
  ts: 'typescript',
8
8
  typescript: 'typescript',
9
- tsx: 'typescript',
9
+ tsx: 'tsx',
10
10
  js: 'javascript',
11
11
  javascript: 'javascript',
12
12
  jsx: 'javascript',
@@ -20,12 +20,17 @@ async function extractSymbols(code, language) {
20
20
  if (!lang || !(0, languages_1.isLanguageSupported)(lang))
21
21
  return [];
22
22
  try {
23
- const rootNode = await (0, languages_1.parseSource)(code, lang);
24
- if (!rootNode)
23
+ const tree = await (0, languages_1.parseSource)(code, lang);
24
+ if (!tree)
25
25
  return [];
26
- const mapper = (0, languages_1.getMapper)(lang);
27
- const symbols = mapper.extractSymbols(rootNode);
28
- return symbols.map(s => s.name).filter(Boolean);
26
+ try {
27
+ const mapper = (0, languages_1.getMapper)(lang);
28
+ const symbols = mapper.extractSymbols(tree.rootNode);
29
+ return symbols.map(s => s.name).filter(Boolean);
30
+ }
31
+ finally {
32
+ tree.delete();
33
+ }
29
34
  }
30
35
  catch {
31
36
  return [];