memtrace 0.3.41 → 0.3.43

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
@@ -10,6 +10,7 @@ const { getBinaryPath } = require("../install.js");
10
10
  const { platformBinary, spawnOptionsForPlatform } = require("../lib/spawn-helper");
11
11
  const { shouldPromptForUpgrade, isPromptDisabled } = require("../lib/update-prompt");
12
12
  const { fetchLatestVersion, readCachedVersion } = require("../lib/update-check");
13
+ const rtkIntegration = require("../lib/rtk-integration");
13
14
 
14
15
  // ── Handle `memtrace uninstall` before delegating to the Rust binary ────────
15
16
  // npm v7+ does NOT fire preuninstall hooks for global packages (npm/cli#3042).
@@ -17,6 +18,105 @@ const { fetchLatestVersion, readCachedVersion } = require("../lib/update-check")
17
18
 
18
19
  const args = process.argv.slice(2);
19
20
 
21
+ // ── RTK opt-in flow ─────────────────────────────────────────────────────────
22
+ //
23
+ // After a fresh `memtrace install` (or as a standalone `memtrace install-rtk`),
24
+ // offer to install RTK alongside. RTK is a separate token-savings tool
25
+ // covering shell-output filtering — non-overlapping with Memtrace's
26
+ // graph-aware code lookups. Strictly opt-in.
27
+ //
28
+ // Routing matrix (from lib/rtk-integration.effectiveRtkAction):
29
+ // already-installed → skip silently with "RTK already installed"
30
+ // NO_RTK_PROMPT=1 → skip silently
31
+ // AUTO_INSTALL_RTK=1 → install without prompting (CI / scripted setups)
32
+ // interactive TTY → prompt with default-yes
33
+ // no TTY → print one-line hint pointing at `memtrace install-rtk`
34
+
35
+ async function runRtkOptInFlow({ source }) {
36
+ const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
37
+ const alreadyInstalled = rtkIntegration.detectRtk();
38
+ const decision = rtkIntegration.effectiveRtkAction({
39
+ isTty,
40
+ alreadyInstalled,
41
+ env: process.env,
42
+ });
43
+
44
+ switch (decision.action) {
45
+ case "skip":
46
+ if (decision.reason === "already-installed") {
47
+ process.stdout.write("memtrace: RTK already installed — skipping prompt.\n");
48
+ }
49
+ // suppressed / invalid-input → silent
50
+ return { action: decision.action, reason: decision.reason };
51
+
52
+ case "hint":
53
+ process.stdout.write(`\n[memtrace] ${rtkIntegration.rtkHintLine()}\n\n`);
54
+ return { action: decision.action, reason: decision.reason };
55
+
56
+ case "install":
57
+ process.stdout.write("\n[memtrace] Installing RTK (MEMTRACE_AUTO_INSTALL_RTK)…\n");
58
+ return runRtkInstall();
59
+
60
+ case "prompt": {
61
+ const accepted = await promptForRtk({ source });
62
+ if (!accepted) {
63
+ process.stdout.write(
64
+ "[memtrace] OK, skipping RTK. " +
65
+ "Install later with: memtrace install-rtk\n",
66
+ );
67
+ return { action: "skip", reason: "user-declined" };
68
+ }
69
+ return runRtkInstall();
70
+ }
71
+
72
+ default:
73
+ return { action: "skip", reason: "unknown" };
74
+ }
75
+ }
76
+
77
+ async function promptForRtk({ source }) {
78
+ const rl = readline.createInterface({
79
+ input: process.stdin,
80
+ output: process.stdout,
81
+ });
82
+ const answer = await new Promise((resolve) => {
83
+ rl.question(rtkIntegration.rtkPromptText({ source }), resolve);
84
+ });
85
+ rl.close();
86
+ return rtkIntegration.parseRtkAnswer(answer);
87
+ }
88
+
89
+ function runRtkInstall() {
90
+ const result = rtkIntegration.installRtk();
91
+ if (result.ok) {
92
+ process.stdout.write(
93
+ `[memtrace] RTK installed via ${result.method}. ` +
94
+ `Run \`rtk gain\` to see your shell-savings ledger.\n`,
95
+ );
96
+ return { action: "install", reason: "ok" };
97
+ }
98
+ process.stderr.write(
99
+ `[memtrace] RTK install failed (${result.method}): ${result.error}\n` +
100
+ `[memtrace] Memtrace itself is fine. Install RTK manually:\n` +
101
+ ` curl -fsSL ${rtkIntegration.RTK_INSTALL_URL} | sh\n`,
102
+ );
103
+ return { action: "install", reason: "failed", error: result.error };
104
+ }
105
+
106
+ // ── Handle `memtrace install-rtk` — standalone RTK install entry point ──────
107
+
108
+ if (args[0] === "install-rtk") {
109
+ // Run the opt-in flow standalone. Always exits 0 — RTK install
110
+ // failure is non-fatal (memtrace itself is unaffected).
111
+ runRtkOptInFlow({ source: "install-rtk" })
112
+ .then(() => process.exit(0))
113
+ .catch((e) => {
114
+ console.error(`memtrace install-rtk: ${e.message}`);
115
+ process.exit(0);
116
+ });
117
+ return; // (top-level await guard — see below for module-level return shim)
118
+ }
119
+
20
120
  // ── Handle `memtrace install` — always pull latest, then run remaining args ──
21
121
  //
22
122
  // `npm install -g memtrace` fetches latest from the registry on a fresh
@@ -38,12 +138,17 @@ if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
38
138
  const memtraceCmd = platformBinary("memtrace", process.platform);
39
139
 
40
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).
41
146
  const installResult = spawnSync(
42
147
  npmCmd,
43
148
  ["install", "-g", "memtrace@latest"],
44
149
  spawnOptionsForPlatform(process.platform, {
45
150
  stdio: "inherit",
46
- env: process.env,
151
+ env: { ...process.env, MEMTRACE_INSTALL_PARENT: "1" },
47
152
  })
48
153
  );
49
154
 
@@ -62,7 +167,16 @@ if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
62
167
  const rest = args.slice(1);
63
168
  if (rest.length === 0) {
64
169
  process.stdout.write("memtrace: latest installed.\n");
65
- process.exit(0);
170
+ // Offer RTK alongside (interactive default-yes). Awaits the flow
171
+ // so the prompt blocks before the process exits. Failure of the
172
+ // RTK install does NOT fail the memtrace install.
173
+ runRtkOptInFlow({ source: "install" })
174
+ .then(() => process.exit(0))
175
+ .catch((e) => {
176
+ console.error(`memtrace: RTK opt-in flow errored (non-fatal): ${e.message}`);
177
+ process.exit(0);
178
+ });
179
+ return;
66
180
  }
67
181
 
68
182
  // Chain into the upgraded binary. PATH now resolves `memtrace` to the
package/install.js CHANGED
@@ -162,7 +162,9 @@ if (require.main === module) {
162
162
  console.warn(`memtrace: Claude Code integration setup skipped: ${e.message}`);
163
163
  }
164
164
 
165
- // 4. Persist uninstall script to ~/.memtrace/ (survives `npm uninstall -g`)
165
+ // 5. Persist uninstall script to ~/.memtrace/ (survives `npm uninstall -g`)
166
+ // Done BEFORE the RTK opt-in below so the uninstall script lands
167
+ // even if the RTK prompt errors / blocks / takes a long time.
166
168
  try {
167
169
  const memtraceDir = path.join(os.homedir(), ".memtrace");
168
170
  fs.mkdirSync(memtraceDir, { recursive: true });
@@ -174,6 +176,104 @@ if (require.main === module) {
174
176
  } catch (e) {
175
177
  console.warn(`memtrace: failed to persist uninstall script: ${e.message}`);
176
178
  }
179
+
180
+ // 6. RTK opt-in flow.
181
+ //
182
+ // npm postinstall pipes stdin away — process.stdin.isTTY is false
183
+ // even when the user is sitting at a terminal. To still ask the
184
+ // user during a fresh `npm install -g memtrace`, we open /dev/tty
185
+ // directly (same trick create-react-app, playwright, prisma use).
186
+ //
187
+ // Routing:
188
+ // - Already-installed → skip silently
189
+ // - MEMTRACE_NO_RTK_PROMPT=1 → skip silently
190
+ // - MEMTRACE_AUTO_INSTALL_RTK=1 → install no-prompt
191
+ // - /dev/tty opens → prompt with default-yes, install on yes
192
+ // - /dev/tty refused (Docker, CI, win32) → print one-line hint
193
+ //
194
+ // Wrapped in async-IIFE because readline is async; we await it so
195
+ // npm sees a clean exit code only after the user has answered.
196
+ (async () => {
197
+ const readline = require("readline");
198
+ const rtk = require("./lib/rtk-integration");
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
+
208
+ let alreadyInstalled = false;
209
+ try { alreadyInstalled = rtk.detectRtk(); } catch (_) { /* skip */ }
210
+
211
+ if (alreadyInstalled) return; // RTK is here, no prompt
212
+ if (rtk.isRtkPromptDisabled({ env: process.env })) return; // user opted out
213
+
214
+ if (rtk.isRtkAutoInstall({ env: process.env })) {
215
+ console.log("\nmemtrace: installing RTK (MEMTRACE_AUTO_INSTALL_RTK)…");
216
+ const r = rtk.installRtk();
217
+ if (r.ok) console.log(`memtrace: RTK installed via ${r.method}`);
218
+ else console.warn(`memtrace: RTK install failed: ${r.error}`);
219
+ return;
220
+ }
221
+
222
+ // Try /dev/tty for an interactive prompt. Falls back to hint if
223
+ // the controlling terminal is unavailable.
224
+ const ttyStreams = rtk.openTtyStreams();
225
+ if (!ttyStreams) {
226
+ console.log("");
227
+ console.log(rtk.rtkHintLine());
228
+ return;
229
+ }
230
+
231
+ let answered = false;
232
+ try {
233
+ const rl = readline.createInterface({
234
+ input: ttyStreams.input,
235
+ output: ttyStreams.output,
236
+ terminal: true,
237
+ });
238
+ const answer = await new Promise((resolve) => {
239
+ rl.question(rtk.rtkPromptText({ source: "postinstall" }), resolve);
240
+ });
241
+ rl.close();
242
+ answered = true;
243
+
244
+ if (!rtk.parseRtkAnswer(answer)) {
245
+ ttyStreams.output.write(
246
+ "memtrace: OK, skipping RTK. Install later: memtrace install-rtk\n",
247
+ );
248
+ return;
249
+ }
250
+
251
+ ttyStreams.output.write("memtrace: installing RTK…\n");
252
+ const r = rtk.installRtk();
253
+ if (r.ok) {
254
+ ttyStreams.output.write(
255
+ `memtrace: RTK installed via ${r.method}. Run 'rtk gain' for the savings ledger.\n`,
256
+ );
257
+ } else {
258
+ ttyStreams.output.write(
259
+ `memtrace: RTK install failed (${r.method}): ${r.error}\n` +
260
+ `memtrace: Memtrace itself is fine. Manual install:\n` +
261
+ ` curl -fsSL ${rtk.RTK_INSTALL_URL} | sh\n`,
262
+ );
263
+ }
264
+ } catch (e) {
265
+ // Any error in the prompt path is non-fatal for memtrace itself.
266
+ if (!answered) {
267
+ console.log(rtk.rtkHintLine());
268
+ } else {
269
+ console.warn(`memtrace: RTK install errored: ${e.message}`);
270
+ }
271
+ } finally {
272
+ ttyStreams.close();
273
+ }
274
+ })().catch((e) => {
275
+ console.warn(`memtrace: RTK opt-in flow errored: ${e.message}`);
276
+ });
177
277
  }
178
278
 
179
279
  module.exports = { getBinaryPath };
@@ -0,0 +1,383 @@
1
+ "use strict";
2
+
3
+ // Opt-in install of RTK ("Rust Token Killer", https://github.com/rtk-ai/rtk)
4
+ // alongside Memtrace.
5
+ //
6
+ // Why:
7
+ // Memtrace eliminates token waste from code-discovery operations
8
+ // (find_symbol, find_code, get_symbol_context). RTK is a parallel,
9
+ // complementary system that proxies shell commands and filters their
10
+ // output before it reaches the agent — claims 60-90% reduction on
11
+ // dev operations like git/ls/cat output, build logs, etc.
12
+ //
13
+ // The two are non-overlapping in scope (graph-aware code lookups vs
14
+ // shell-output filtering), share no runtime dependencies, and
15
+ // already have a coexistence contract on CLAUDE.md / settings.json
16
+ // sentinel blocks (see test/uninstall-cleanliness.test.js).
17
+ //
18
+ // Bundling them as "want both?" at install time gives users
19
+ // maximum savings without forcing a separate discovery + install
20
+ // cycle. Strictly opt-in.
21
+ //
22
+ // Module shape mirrors lib/update-prompt.js:
23
+ // - Pure decision helpers below (testable without IO)
24
+ // - Side-effecting prompt + install runner separated so callers can
25
+ // unit-test the routing without spawning subprocesses
26
+
27
+ // ── Env-var parsing helpers ─────────────────────────────────────────
28
+
29
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
30
+
31
+ function isTruthyEnv(value) {
32
+ if (typeof value !== "string") return false;
33
+ return TRUTHY.has(value.trim().toLowerCase());
34
+ }
35
+
36
+ /**
37
+ * @param {object} input
38
+ * @param {object} [input.env] defaults to process.env
39
+ * @returns {boolean}
40
+ */
41
+ function isRtkPromptDisabled(input = {}) {
42
+ const env = input.env || {};
43
+ return isTruthyEnv(env.MEMTRACE_NO_RTK_PROMPT);
44
+ }
45
+
46
+ /**
47
+ * @param {object} input
48
+ * @param {object} [input.env] defaults to process.env
49
+ * @returns {boolean}
50
+ */
51
+ function isRtkAutoInstall(input = {}) {
52
+ const env = input.env || {};
53
+ return isTruthyEnv(env.MEMTRACE_AUTO_INSTALL_RTK);
54
+ }
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
+
73
+ // ── Decision helpers ────────────────────────────────────────────────
74
+
75
+ /**
76
+ * Should the caller PROMPT the user for RTK install? Pure: no IO.
77
+ *
78
+ * @param {object} input
79
+ * @param {boolean} input.isTty
80
+ * @param {boolean} input.alreadyInstalled
81
+ * @param {object} [input.env]
82
+ * @returns {boolean}
83
+ */
84
+ function shouldPromptForRtk(input) {
85
+ if (!input || typeof input !== "object") return false;
86
+ if (!input.isTty) return false;
87
+ if (input.alreadyInstalled) return false;
88
+ if (isRtkPromptDisabled(input)) return false;
89
+ if (isRtkAutoInstall(input)) return false; // auto bypasses prompt → install path
90
+ return true;
91
+ }
92
+
93
+ /**
94
+ * Compute the routing decision for the RTK opt-in flow. Returns
95
+ * { action: "install" | "prompt" | "skip" | "hint", reason: string }
96
+ * so callers can both act and produce a consistent log line.
97
+ *
98
+ * Decision precedence (top wins):
99
+ * 1. already-installed → skip (RTK is here, nothing to do)
100
+ * 2. NO_RTK_PROMPT → skip (user explicitly opted out)
101
+ * 3. AUTO_INSTALL_RTK → install (user explicitly opted in)
102
+ * 4. interactive TTY → prompt (default-yes interactive flow)
103
+ * 5. otherwise → hint (no TTY, no env signal — print one-line hint, skip install)
104
+ */
105
+ function effectiveRtkAction(input) {
106
+ if (!input || typeof input !== "object") {
107
+ return { action: "skip", reason: "invalid-input" };
108
+ }
109
+ if (input.alreadyInstalled) {
110
+ return { action: "skip", reason: "already-installed" };
111
+ }
112
+ if (isRtkPromptDisabled(input)) {
113
+ return { action: "skip", reason: "suppressed" };
114
+ }
115
+ if (isRtkAutoInstall(input)) {
116
+ return { action: "install", reason: "auto-env" };
117
+ }
118
+ if (input.isTty) {
119
+ return { action: "prompt", reason: "interactive" };
120
+ }
121
+ return { action: "hint", reason: "no-tty" };
122
+ }
123
+
124
+ // ── Install-strategy helper ────────────────────────────────────────
125
+
126
+ const RTK_INSTALL_URL =
127
+ "https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh";
128
+
129
+ /**
130
+ * Pick the right install method for the host platform. Pure.
131
+ *
132
+ * Returns:
133
+ * { method: "brew" | "curl" | "unsupported",
134
+ * command: string[] // argv to spawnSync (empty for unsupported)
135
+ * reason?: string } // for unsupported, why
136
+ */
137
+ function chooseInstallStrategy(input) {
138
+ const platform = (input && input.platform) || "linux";
139
+ const hasBrew = Boolean(input && input.hasBrew);
140
+
141
+ if (platform === "win32") {
142
+ return {
143
+ method: "unsupported",
144
+ command: [],
145
+ reason: "RTK does not ship a Windows installer; install WSL or skip RTK.",
146
+ };
147
+ }
148
+
149
+ if (platform === "darwin" && hasBrew) {
150
+ return {
151
+ method: "brew",
152
+ command: ["brew", "install", "rtk"],
153
+ };
154
+ }
155
+
156
+ // Linux + macOS-without-brew → official curl|sh installer.
157
+ return {
158
+ method: "curl",
159
+ command: [
160
+ "sh",
161
+ "-c",
162
+ `set -e; curl -fsSL ${RTK_INSTALL_URL} | sh`,
163
+ ],
164
+ };
165
+ }
166
+
167
+ // ── Side-effecting helpers (thin shells; the real work is in argv) ─
168
+
169
+ /**
170
+ * Detect whether RTK is already installed AND is the correct one (rtk-ai
171
+ * version, not the Rust Type Kit collision). Returns true only when
172
+ * `rtk gain` succeeds — per RTK's own README, that's the canonical
173
+ * verification because `rtk --version` also passes for the wrong RTK.
174
+ *
175
+ * Synchronous so it composes with the npm postinstall flow.
176
+ *
177
+ * @param {{spawnSync?: function}} [opts]
178
+ * @returns {boolean}
179
+ */
180
+ function detectRtk(opts = {}) {
181
+ const spawnSync = opts.spawnSync || require("child_process").spawnSync;
182
+ // `rtk gain --help` is a fast no-op that proves we have the
183
+ // rtk-ai binary. We don't actually need to run `rtk gain` itself
184
+ // (which would hit network / read history) — `--help` parsing is
185
+ // enough to fail loudly on the wrong binary.
186
+ const r = spawnSync("rtk", ["gain", "--help"], {
187
+ stdio: "ignore",
188
+ timeout: 5000,
189
+ });
190
+ return r.status === 0;
191
+ }
192
+
193
+ /**
194
+ * Detect whether Homebrew is on PATH. Used to pick install strategy
195
+ * on macOS.
196
+ */
197
+ function detectHomebrew(opts = {}) {
198
+ const spawnSync = opts.spawnSync || require("child_process").spawnSync;
199
+ const r = spawnSync("brew", ["--version"], {
200
+ stdio: "ignore",
201
+ timeout: 3000,
202
+ });
203
+ return r.status === 0;
204
+ }
205
+
206
+ /**
207
+ * Side-effecting RTK installer. Spawns the chosen install command,
208
+ * inheriting stdio so the user sees progress. Returns
209
+ * { ok: boolean, method: string, error?: string }
210
+ */
211
+ function installRtk(opts = {}) {
212
+ const spawnSync = opts.spawnSync || require("child_process").spawnSync;
213
+ const platform = opts.platform || process.platform;
214
+ const hasBrew = opts.hasBrew !== undefined
215
+ ? opts.hasBrew
216
+ : (platform === "darwin" && detectHomebrew({ spawnSync }));
217
+
218
+ const strategy = chooseInstallStrategy({ platform, hasBrew });
219
+
220
+ if (strategy.method === "unsupported") {
221
+ return { ok: false, method: strategy.method, error: strategy.reason };
222
+ }
223
+
224
+ const [cmd, ...args] = strategy.command;
225
+ const r = spawnSync(cmd, args, {
226
+ stdio: "inherit",
227
+ timeout: 120000, // 2 min cap; brew/curl install should be well under this
228
+ });
229
+ if (r.status !== 0) {
230
+ return {
231
+ ok: false,
232
+ method: strategy.method,
233
+ error: `${cmd} exited with status ${r.status}`,
234
+ };
235
+ }
236
+
237
+ // Verify the right RTK landed.
238
+ if (!detectRtk({ spawnSync })) {
239
+ return {
240
+ ok: false,
241
+ method: strategy.method,
242
+ error:
243
+ "rtk install reported success but `rtk gain --help` did not work. " +
244
+ "You may have hit the Rust-Type-Kit name collision — see " +
245
+ "https://github.com/rtk-ai/rtk/blob/master/INSTALL.md",
246
+ };
247
+ }
248
+
249
+ return { ok: true, method: strategy.method };
250
+ }
251
+
252
+ /**
253
+ * Print the one-line hint for the no-TTY path. Separated so callers
254
+ * can route output to wherever (stderr, log file, etc.).
255
+ */
256
+ function rtkHintLine() {
257
+ return (
258
+ "memtrace: RTK (rtk-ai/rtk) is a complementary token-savings tool. " +
259
+ "Install alongside with: memtrace install-rtk"
260
+ );
261
+ }
262
+
263
+ // ── /dev/tty prompting (for npm postinstall, where stdin is piped) ──
264
+ //
265
+ // npm postinstall runs with stdin piped to npm — process.stdin.isTTY
266
+ // is false even when the user is sitting at a terminal. Plenty of
267
+ // well-known npm installers (create-react-app, playwright, prisma)
268
+ // work around this by opening /dev/tty directly. We do the same.
269
+ //
270
+ // On Windows there is no /dev/tty. We return null and the caller
271
+ // falls back to the hint path.
272
+
273
+ /**
274
+ * Open read+write streams on the controlling terminal.
275
+ *
276
+ * @param {object} [opts]
277
+ * @param {string} [opts.platform] defaults to process.platform
278
+ * @param {object} [opts.fs] defaults to require("fs")
279
+ * @param {object} [opts.tty] defaults to require("tty")
280
+ * @returns {{input: ReadStream, output: WriteStream, close: function}|null}
281
+ * null if /dev/tty cannot be opened (Win32, Docker without
282
+ * tty, sandboxed environments).
283
+ */
284
+ function openTtyStreams(opts = {}) {
285
+ const platform = opts.platform || process.platform;
286
+ if (platform === "win32") {
287
+ // Windows has CONIN$/CONOUT$ but the stream wrappers don't
288
+ // round-trip cleanly through readline. Fall back to hint.
289
+ return null;
290
+ }
291
+
292
+ const fs = opts.fs || require("fs");
293
+ const tty = opts.tty || require("tty");
294
+
295
+ // Open TWO fds — one for read, one for write. Sharing a single
296
+ // r+ fd between ReadStream and WriteStream causes readline to
297
+ // immediately observe EOF on some setups (notably when a parent
298
+ // process is also writing progress to the same controlling tty,
299
+ // e.g. npm's spinner during `memtrace install`).
300
+ let inFd, outFd;
301
+ try {
302
+ inFd = fs.openSync("/dev/tty", "r");
303
+ outFd = fs.openSync("/dev/tty", "w");
304
+ } catch (_) {
305
+ if (inFd != null) { try { fs.closeSync(inFd); } catch (_) {} }
306
+ return null; // no controlling tty (Docker, CI, etc.)
307
+ }
308
+
309
+ let input, output;
310
+ try {
311
+ input = new tty.ReadStream(inFd);
312
+ output = new tty.WriteStream(outFd);
313
+ } catch (e) {
314
+ try { fs.closeSync(inFd); } catch (_) {}
315
+ try { fs.closeSync(outFd); } catch (_) {}
316
+ return null;
317
+ }
318
+
319
+ return {
320
+ input,
321
+ output,
322
+ close() {
323
+ try { input.destroy(); } catch (_) { /* best-effort */ }
324
+ try { output.destroy(); } catch (_) { /* best-effort */ }
325
+ },
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Pure parser: did the user accept the [Y/n] prompt?
331
+ *
332
+ * Default-yes: empty input (just Enter), unrecognised input → true.
333
+ * Only explicit "n" / "no" (case-insensitive, trimmed) → false.
334
+ */
335
+ function parseRtkAnswer(answer) {
336
+ if (typeof answer !== "string") return true; // default-yes
337
+ const trimmed = answer.trim().toLowerCase();
338
+ if (trimmed === "") return true;
339
+ if (trimmed === "n" || trimmed === "no") return false;
340
+ return true;
341
+ }
342
+
343
+ /**
344
+ * Build the prompt text. Pure so we can keep the IO wrapper thin.
345
+ */
346
+ function rtkPromptText({ source } = {}) {
347
+ const heading =
348
+ source === "install"
349
+ ? "Memtrace is installed. One more thing —"
350
+ : source === "postinstall"
351
+ ? "Memtrace is installed. One more thing —"
352
+ : "Add RTK to your toolchain?";
353
+ return (
354
+ `\n[memtrace] ${heading}\n` +
355
+ `\n` +
356
+ `RTK (Rust Token Killer, https://rtk-ai.app) is a separate CLI proxy\n` +
357
+ `that filters shell-command output before it reaches your AI assistant.\n` +
358
+ `Memtrace eliminates token waste from code lookups; RTK does the same\n` +
359
+ `for shell output. They're complementary.\n` +
360
+ `\n` +
361
+ `Install RTK alongside Memtrace? [Y/n] `
362
+ );
363
+ }
364
+
365
+ module.exports = {
366
+ // Pure helpers
367
+ isRtkPromptDisabled,
368
+ isRtkAutoInstall,
369
+ isPostinstallNestedUnderShim,
370
+ shouldPromptForRtk,
371
+ effectiveRtkAction,
372
+ chooseInstallStrategy,
373
+ rtkHintLine,
374
+ parseRtkAnswer,
375
+ rtkPromptText,
376
+ // IO helpers (injectable spawnSync / fs / tty for tests)
377
+ detectRtk,
378
+ detectHomebrew,
379
+ installRtk,
380
+ openTtyStreams,
381
+ // Constants
382
+ RTK_INSTALL_URL,
383
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrace",
3
- "version": "0.3.41",
3
+ "version": "0.3.43",
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.41",
43
- "@memtrace/linux-x64": "0.3.41",
44
- "@memtrace/win32-x64": "0.3.41"
42
+ "@memtrace/darwin-arm64": "0.3.42",
43
+ "@memtrace/linux-x64": "0.3.42",
44
+ "@memtrace/win32-x64": "0.3.42"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=18"