memtrace 0.4.2 → 0.4.3

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.
@@ -23,18 +23,54 @@ const MARKETPLACE_SETTING_CONTAINERS = [
23
23
  function isRecord(value) {
24
24
  return typeof value === 'object' && value !== null && !Array.isArray(value);
25
25
  }
26
+ /**
27
+ * Read the existing env block (if any) for the memtrace MCP entry in a Claude
28
+ * settings/mcp.json file. Returns an empty object if the file or entry is
29
+ * missing/malformed. Used to deep-merge user-set env keys
30
+ * (e.g. MEMTRACE_LICENSE_KEY) so they survive `memtrace install`
31
+ * re-registration. Mirrors the codex transformer's env-preservation contract.
32
+ */
33
+ function readExistingMemtraceEnv(filePath, serverKey = 'mcpServers') {
34
+ if (!fs.existsSync(filePath))
35
+ return {};
36
+ const { value, corrupted } = safeReadJson(filePath);
37
+ if (corrupted || !value)
38
+ return {};
39
+ const servers = value[serverKey];
40
+ if (!isRecord(servers))
41
+ return {};
42
+ const entry = servers['memtrace'];
43
+ if (!isRecord(entry))
44
+ return {};
45
+ const env = entry.env;
46
+ if (!isRecord(env))
47
+ return {};
48
+ const out = {};
49
+ for (const [k, v] of Object.entries(env)) {
50
+ if (typeof v === 'string')
51
+ out[k] = v;
52
+ }
53
+ return out;
54
+ }
26
55
  /**
27
56
  * Try to register the memtrace MCP server via `claude mcp add-json`.
28
57
  * Returns true on success, false on timeout/error/missing CLI.
29
58
  * 5-second timeout — we fall back to direct JSON merge fast.
59
+ *
60
+ * The env block deep-merges any user-set env keys discovered in the existing
61
+ * settings.json on top of MEMTRACE_MCP_ENV defaults so that a re-install never
62
+ * stomps user-added keys like MEMTRACE_LICENSE_KEY.
30
63
  */
31
64
  async function tryMcpAddJson(memtraceBinary) {
32
65
  if (!(await commandExists('claude')))
33
66
  return false;
67
+ const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
68
+ const existingEnv = readExistingMemtraceEnv(settingsFile);
69
+ const mergedEnv = { ...existingEnv, ...MEMTRACE_MCP_ENV };
34
70
  const config = JSON.stringify({
35
71
  command: memtraceBinary,
36
72
  args: ['mcp'],
37
- env: MEMTRACE_MCP_ENV,
73
+ env: mergedEnv,
38
74
  });
39
75
  // Shell-quote the JSON. Use single quotes; escape any embedded single quote.
40
76
  const escaped = config.replace(/'/g, `'\\''`);
@@ -164,10 +200,18 @@ export function registerMcpInSettingsAt(settingsPath, memtraceBinary) {
164
200
  }
165
201
  const settings = (value ?? {});
166
202
  settings.mcpServers = settings.mcpServers ?? {};
203
+ // Deep-merge env so user-set keys (e.g. MEMTRACE_LICENSE_KEY) survive
204
+ // re-registration. memtrace-owned keys (MEMTRACE_MCP_ENV) override on
205
+ // conflict so installer-managed values stay authoritative.
206
+ const existing = settings.mcpServers['memtrace'];
207
+ const existingEnv = (isRecord(existing) && isRecord(existing.env))
208
+ ? existing.env
209
+ : {};
210
+ const mergedEnv = { ...existingEnv, ...MEMTRACE_MCP_ENV };
167
211
  settings.mcpServers['memtrace'] = {
168
212
  command: memtraceBinary,
169
213
  args: ['mcp'],
170
- env: MEMTRACE_MCP_ENV,
214
+ env: mergedEnv,
171
215
  };
172
216
  writeJsonAtomic(settingsPath, settings);
173
217
  return { registered: true };
@@ -416,10 +460,16 @@ function writeClaudeLocalMcp(mcpPath, binary) {
416
460
  }
417
461
  const cfg = (value ?? {});
418
462
  cfg.mcpServers = cfg.mcpServers ?? {};
463
+ // Deep-merge env: preserve user-set keys, memtrace defaults win on conflict.
464
+ const existing = cfg.mcpServers['memtrace'];
465
+ const existingEnv = (isRecord(existing) && isRecord(existing.env))
466
+ ? existing.env
467
+ : {};
468
+ const mergedEnv = { ...existingEnv, ...MEMTRACE_MCP_ENV };
419
469
  cfg.mcpServers['memtrace'] = {
420
470
  command: binary,
421
471
  args: ['mcp'],
422
- env: MEMTRACE_MCP_ENV,
472
+ env: mergedEnv,
423
473
  };
424
474
  writeJsonAtomic(mcpPath, cfg);
425
475
  return true;
@@ -38,6 +38,20 @@ function isMemtraceMcpTable(tableName) {
38
38
  || tableName.startsWith(`mcp_servers.${MCP_SERVER_NAME}.`)
39
39
  || tableName.startsWith(`mcp_servers."${MCP_SERVER_NAME}".`);
40
40
  }
41
+ /**
42
+ * Tables we want stripped on re-registration.
43
+ *
44
+ * PRESERVE [mcp_servers.memtrace.env] so user-set env vars
45
+ * (MEMTRACE_LICENSE_KEY, etc.) survive `memtrace install` re-registration.
46
+ * Only the top-level table + non-env sub-tables get stripped.
47
+ */
48
+ function isMemtraceMcpTableToStrip(tableName) {
49
+ if (tableName === `mcp_servers.${MCP_SERVER_NAME}.env`)
50
+ return false;
51
+ if (tableName === `mcp_servers."${MCP_SERVER_NAME}".env`)
52
+ return false;
53
+ return isMemtraceMcpTable(tableName);
54
+ }
41
55
  export function stripCodexMcpServer(raw) {
42
56
  const lines = raw.split(/\r?\n/);
43
57
  const kept = [];
@@ -45,7 +59,7 @@ export function stripCodexMcpServer(raw) {
45
59
  for (const line of lines) {
46
60
  const table = line.match(/^\s*\[([^\]]+)\]\s*$/);
47
61
  if (table) {
48
- skipping = isMemtraceMcpTable(table[1].trim());
62
+ skipping = isMemtraceMcpTableToStrip(table[1].trim());
49
63
  if (skipping)
50
64
  continue;
51
65
  }
@@ -54,16 +68,64 @@ export function stripCodexMcpServer(raw) {
54
68
  }
55
69
  return kept.join('\n').trimEnd();
56
70
  }
71
+ /**
72
+ * Extracts the preserved `[mcp_servers.memtrace.env]` block (if any) and
73
+ * returns:
74
+ * - rest: the input with the env block removed
75
+ * - envBlock: the env block as a string (empty if absent)
76
+ *
77
+ * Used by `registerCodexMcpAt` so the env block can be reattached AFTER the
78
+ * freshly-written `[mcp_servers.memtrace]` parent for readable ordering.
79
+ */
80
+ function extractMemtraceEnvBlock(raw) {
81
+ const lines = raw.split(/\r?\n/);
82
+ const rest = [];
83
+ const envLines = [];
84
+ let inEnv = false;
85
+ for (const line of lines) {
86
+ const table = line.match(/^\s*\[([^\]]+)\]\s*$/);
87
+ if (table) {
88
+ const name = table[1].trim();
89
+ const isEnv = name === `mcp_servers.${MCP_SERVER_NAME}.env`
90
+ || name === `mcp_servers."${MCP_SERVER_NAME}".env`;
91
+ if (isEnv) {
92
+ inEnv = true;
93
+ envLines.push(line);
94
+ continue;
95
+ }
96
+ // Any other table header closes the env capture.
97
+ inEnv = false;
98
+ }
99
+ if (inEnv) {
100
+ envLines.push(line);
101
+ }
102
+ else {
103
+ rest.push(line);
104
+ }
105
+ }
106
+ return {
107
+ rest: rest.join('\n').replace(/\n+$/, ''),
108
+ envBlock: envLines.join('\n').replace(/\n+$/, ''),
109
+ };
110
+ }
57
111
  export function registerCodexMcpAt(configFile, binary) {
58
112
  const existing = fs.existsSync(configFile) ? fs.readFileSync(configFile, 'utf-8') : '';
59
- const base = stripCodexMcpServer(existing);
113
+ // Strip leaves the env sub-table intact; split it out so we can reorder it
114
+ // after the freshly written parent block for readability.
115
+ const stripped = stripCodexMcpServer(existing);
116
+ const { rest, envBlock } = extractMemtraceEnvBlock(stripped);
60
117
  const block = [
61
118
  `[mcp_servers.${MCP_SERVER_NAME}]`,
62
119
  `command = ${tomlString(binary)}`,
63
120
  `args = [${tomlString('mcp')}]`,
64
- '',
65
121
  ].join('\n');
66
- const next = `${base}${base ? '\n\n' : ''}${block}`;
122
+ const parts = [];
123
+ if (rest)
124
+ parts.push(rest);
125
+ parts.push(block);
126
+ if (envBlock)
127
+ parts.push(envBlock);
128
+ const next = `${parts.join('\n\n')}\n`;
67
129
  writeTextAtomic(configFile, next);
68
130
  return { registered: true };
69
131
  }
@@ -20,6 +20,9 @@ function writeSkill(skill, rootDir) {
20
20
  const content = `---\nname: ${name}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
21
21
  fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
22
22
  }
23
+ function isRecord(value) {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
23
26
  export function registerCursorMcpAt(mcpFile, binary) {
24
27
  const { value, corrupted, backupPath } = safeReadJson(mcpFile);
25
28
  if (corrupted) {
@@ -28,10 +31,17 @@ export function registerCursorMcpAt(mcpFile, binary) {
28
31
  }
29
32
  const cfg = (value ?? {});
30
33
  cfg.mcpServers = cfg.mcpServers ?? {};
34
+ // Deep-merge env: preserve user-set keys (e.g. MEMTRACE_LICENSE_KEY) across
35
+ // re-registration; memtrace-owned defaults (MEMTRACE_MCP_ENV) win on conflict.
36
+ const existing = cfg.mcpServers['memtrace'];
37
+ const existingEnv = (isRecord(existing) && isRecord(existing.env))
38
+ ? existing.env
39
+ : {};
40
+ const mergedEnv = { ...existingEnv, ...MEMTRACE_MCP_ENV };
31
41
  cfg.mcpServers['memtrace'] = {
32
42
  command: binary,
33
43
  args: ['mcp'],
34
- env: MEMTRACE_MCP_ENV,
44
+ env: mergedEnv,
35
45
  };
36
46
  writeJsonAtomic(mcpFile, cfg);
37
47
  return { registered: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrace",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Code intelligence graph — MCP server + AI agent skills + visualization UI",
5
5
  "keywords": [
6
6
  "mcp",
@@ -39,11 +39,11 @@
39
39
  "fs-extra": "^11.0.0"
40
40
  },
41
41
  "optionalDependencies": {
42
- "@memtrace/darwin-arm64": "0.4.2",
43
- "@memtrace/linux-x64": "0.4.2",
44
- "@memtrace/win32-x64": "0.4.2",
45
- "@memtrace/linux-x64-noavx2": "0.4.2",
46
- "@memtrace/win32-x64-noavx2": "0.4.2"
42
+ "@memtrace/darwin-arm64": "0.4.3",
43
+ "@memtrace/linux-x64": "0.4.3",
44
+ "@memtrace/win32-x64": "0.4.3",
45
+ "@memtrace/linux-x64-noavx2": "0.4.3",
46
+ "@memtrace/win32-x64-noavx2": "0.4.3"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=18"