memtrace 0.3.42 → 0.3.44

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.
package/bin/memtrace.js CHANGED
@@ -138,12 +138,17 @@ if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
138
138
  const memtraceCmd = platformBinary("memtrace", process.platform);
139
139
 
140
140
  process.stdout.write("memtrace: fetching latest from npm registry…\n");
141
+ // Tell the postinstall RTK opt-in to stay silent — *we* own the
142
+ // prompt and have a clean TTY (npm's progress spinner shares the
143
+ // user's terminal with the postinstall, which makes its prompt
144
+ // unreadable and readline picks up a phantom EOF, default-yes
145
+ // installing RTK without the user seeing the question).
141
146
  const installResult = spawnSync(
142
147
  npmCmd,
143
148
  ["install", "-g", "memtrace@latest"],
144
149
  spawnOptionsForPlatform(process.platform, {
145
150
  stdio: "inherit",
146
- env: process.env,
151
+ env: { ...process.env, MEMTRACE_INSTALL_PARENT: "1" },
147
152
  })
148
153
  );
149
154
 
package/install.js CHANGED
@@ -197,6 +197,14 @@ if (require.main === module) {
197
197
  const readline = require("readline");
198
198
  const rtk = require("./lib/rtk-integration");
199
199
 
200
+ // Nested under `memtrace install` shim? The parent owns the prompt
201
+ // and has a clean TTY. Two prompt paths racing over the same
202
+ // terminal means npm's progress spinner overwrites the question
203
+ // line and readline observes a phantom EOF — user gets RTK
204
+ // installed without ever seeing or answering Y/n. Bail out here;
205
+ // bin/memtrace.js will run the prompt cleanly after npm exits.
206
+ if (rtk.isPostinstallNestedUnderShim({ env: process.env })) return;
207
+
200
208
  let alreadyInstalled = false;
201
209
  try { alreadyInstalled = rtk.detectRtk(); } catch (_) { /* skip */ }
202
210
 
@@ -53,6 +53,23 @@ function isRtkAutoInstall(input = {}) {
53
53
  return isTruthyEnv(env.MEMTRACE_AUTO_INSTALL_RTK);
54
54
  }
55
55
 
56
+ /**
57
+ * True when this postinstall is running nested under the `memtrace
58
+ * install` shim — i.e. the parent process is bin/memtrace.js spawning
59
+ * `npm install -g memtrace@latest`. The parent owns user interaction
60
+ * (it has a clean TTY, and it runs its own RTK opt-in flow after npm
61
+ * exits). The nested postinstall must therefore stay completely silent
62
+ * for RTK — otherwise both prompt paths race over the same terminal,
63
+ * the spinner overwrites the prompt, and readline picks up a phantom
64
+ * EOF.
65
+ *
66
+ * Coordinated via `MEMTRACE_INSTALL_PARENT=1` in the spawn env.
67
+ */
68
+ function isPostinstallNestedUnderShim(input = {}) {
69
+ const env = input.env || {};
70
+ return isTruthyEnv(env.MEMTRACE_INSTALL_PARENT);
71
+ }
72
+
56
73
  // ── Decision helpers ────────────────────────────────────────────────
57
74
 
58
75
  /**
@@ -186,6 +203,91 @@ function detectHomebrew(opts = {}) {
186
203
  return r.status === 0;
187
204
  }
188
205
 
206
+ // ── rtk init (hook activation) ──────────────────────────────────────
207
+ //
208
+ // `brew install rtk` (or curl|sh) only installs the binary. Without
209
+ // `rtk init -g`, RTK is dormant — Claude Code does NOT auto-rewrite
210
+ // commands through the hook, so the user sees zero token savings.
211
+ // To deliver "maximum token savings" we must chain rtk init right
212
+ // after the binary install.
213
+ //
214
+ // rtk init -g --auto-patch:
215
+ // - installs hook to ~/.claude/hooks/rtk-rewrite.sh
216
+ // - creates ~/.claude/RTK.md (10-line breadcrumb)
217
+ // - adds @RTK.md reference to ~/.claude/CLAUDE.md
218
+ // - patches ~/.claude/settings.json's hooks (with .bak)
219
+ //
220
+ // The CLAUDE.md / settings.json edits coexist with memtrace's own
221
+ // blocks via the sentinel-installer contract (see test/uninstall-
222
+ // cleanliness.test.js's 7 coexistence tests).
223
+
224
+ /**
225
+ * Choose the rtk init command to run, or null to skip.
226
+ *
227
+ * Env-var overrides:
228
+ * MEMTRACE_NO_RTK_INIT=1 → skip init entirely (binary only)
229
+ * MEMTRACE_RTK_INIT_MODE → "global" (default) | "hook-only" | "local"
230
+ */
231
+ function chooseRtkInitCommand(input = {}) {
232
+ const env = (input && input.env) || {};
233
+ if (isTruthyEnv(env.MEMTRACE_NO_RTK_INIT)) return null;
234
+
235
+ const mode = (env.MEMTRACE_RTK_INIT_MODE || "global").trim().toLowerCase();
236
+ switch (mode) {
237
+ case "local":
238
+ return ["rtk", "init", "--auto-patch"];
239
+ case "hook-only":
240
+ return ["rtk", "init", "-g", "--hook-only", "--auto-patch"];
241
+ case "global":
242
+ default:
243
+ return ["rtk", "init", "-g", "--auto-patch"];
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Pure check: has rtk init already been run? Detects by looking for
249
+ * the two artefacts rtk init -g writes. Avoids re-running on every
250
+ * memtrace upgrade.
251
+ */
252
+ function isRtkInitAlreadyDone(input = {}) {
253
+ const home = input && input.home;
254
+ if (!home) return false;
255
+ const fs = (input && input.fs) || require("fs");
256
+ const path = (input && input.path) || require("path");
257
+ const hookPath = path.join(home, ".claude", "hooks", "rtk-rewrite.sh");
258
+ const rtkMdPath = path.join(home, ".claude", "RTK.md");
259
+ return fs.existsSync(hookPath) && fs.existsSync(rtkMdPath);
260
+ }
261
+
262
+ /**
263
+ * Side-effecting: run `rtk init` with the chosen flags. Returns
264
+ * { ok: boolean, ranCommand?: string[], error?: string, skipped?: string }
265
+ */
266
+ function runRtkInit(opts = {}) {
267
+ const env = opts.env || process.env;
268
+ const home = opts.home || require("os").homedir();
269
+ const spawnSync = opts.spawnSync || require("child_process").spawnSync;
270
+
271
+ const cmd = chooseRtkInitCommand({ env });
272
+ if (!cmd) {
273
+ return { ok: true, skipped: "MEMTRACE_NO_RTK_INIT" };
274
+ }
275
+ if (isRtkInitAlreadyDone({ home })) {
276
+ return { ok: true, skipped: "already-initialized" };
277
+ }
278
+
279
+ const [bin, ...args] = cmd;
280
+ const r = spawnSync(bin, args, { stdio: "inherit", timeout: 30000 });
281
+ if (r.status !== 0) {
282
+ return {
283
+ ok: false,
284
+ ranCommand: cmd,
285
+ error: `${bin} ${args.join(" ")} exited with status ${r.status}`,
286
+ };
287
+ }
288
+ return { ok: true, ranCommand: cmd };
289
+ }
290
+
189
291
  /**
190
292
  * Side-effecting RTK installer. Spawns the chosen install command,
191
293
  * inheriting stdio so the user sees progress. Returns
@@ -229,7 +331,28 @@ function installRtk(opts = {}) {
229
331
  };
230
332
  }
231
333
 
232
- return { ok: true, method: strategy.method };
334
+ // CRITICAL: run rtk init -g --auto-patch so the hook actually
335
+ // activates. Without this, the binary is installed but RTK is
336
+ // dormant — Claude Code does not auto-rewrite anything and the
337
+ // user gets zero token savings. This is what delivers the
338
+ // "maximum token savings" promise of the opt-in.
339
+ const initResult = runRtkInit({ spawnSync });
340
+ if (!initResult.ok) {
341
+ return {
342
+ ok: false,
343
+ method: strategy.method,
344
+ error:
345
+ `binary installed but \`rtk init\` failed: ${initResult.error}. ` +
346
+ `Run manually: rtk init -g --auto-patch`,
347
+ };
348
+ }
349
+
350
+ return {
351
+ ok: true,
352
+ method: strategy.method,
353
+ initSkipped: initResult.skipped, // "already-initialized" | "MEMTRACE_NO_RTK_INIT" | undefined
354
+ initCommand: initResult.ranCommand,
355
+ };
233
356
  }
234
357
 
235
358
  /**
@@ -275,19 +398,27 @@ function openTtyStreams(opts = {}) {
275
398
  const fs = opts.fs || require("fs");
276
399
  const tty = opts.tty || require("tty");
277
400
 
278
- let fd;
401
+ // Open TWO fds — one for read, one for write. Sharing a single
402
+ // r+ fd between ReadStream and WriteStream causes readline to
403
+ // immediately observe EOF on some setups (notably when a parent
404
+ // process is also writing progress to the same controlling tty,
405
+ // e.g. npm's spinner during `memtrace install`).
406
+ let inFd, outFd;
279
407
  try {
280
- fd = fs.openSync("/dev/tty", "r+");
408
+ inFd = fs.openSync("/dev/tty", "r");
409
+ outFd = fs.openSync("/dev/tty", "w");
281
410
  } catch (_) {
411
+ if (inFd != null) { try { fs.closeSync(inFd); } catch (_) {} }
282
412
  return null; // no controlling tty (Docker, CI, etc.)
283
413
  }
284
414
 
285
415
  let input, output;
286
416
  try {
287
- input = new tty.ReadStream(fd);
288
- output = new tty.WriteStream(fd);
417
+ input = new tty.ReadStream(inFd);
418
+ output = new tty.WriteStream(outFd);
289
419
  } catch (e) {
290
- try { fs.closeSync(fd); } catch (_) { /* best-effort */ }
420
+ try { fs.closeSync(inFd); } catch (_) {}
421
+ try { fs.closeSync(outFd); } catch (_) {}
291
422
  return null;
292
423
  }
293
424
 
@@ -341,9 +472,12 @@ module.exports = {
341
472
  // Pure helpers
342
473
  isRtkPromptDisabled,
343
474
  isRtkAutoInstall,
475
+ isPostinstallNestedUnderShim,
344
476
  shouldPromptForRtk,
345
477
  effectiveRtkAction,
346
478
  chooseInstallStrategy,
479
+ chooseRtkInitCommand,
480
+ isRtkInitAlreadyDone,
347
481
  rtkHintLine,
348
482
  parseRtkAnswer,
349
483
  rtkPromptText,
@@ -351,6 +485,7 @@ module.exports = {
351
485
  detectRtk,
352
486
  detectHomebrew,
353
487
  installRtk,
488
+ runRtkInit,
354
489
  openTtyStreams,
355
490
  // Constants
356
491
  RTK_INSTALL_URL,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrace",
3
- "version": "0.3.42",
3
+ "version": "0.3.44",
4
4
  "description": "Code intelligence graph — MCP server + AI agent skills + visualization UI",
5
5
  "keywords": [
6
6
  "mcp",
@@ -39,9 +39,9 @@
39
39
  "fs-extra": "^11.0.0"
40
40
  },
41
41
  "optionalDependencies": {
42
- "@memtrace/darwin-arm64": "0.3.42",
43
- "@memtrace/linux-x64": "0.3.42",
44
- "@memtrace/win32-x64": "0.3.42"
42
+ "@memtrace/darwin-arm64": "0.3.44",
43
+ "@memtrace/linux-x64": "0.3.44",
44
+ "@memtrace/win32-x64": "0.3.44"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=18"