memtrace 0.3.41 → 0.3.42

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
@@ -62,7 +162,16 @@ if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
62
162
  const rest = args.slice(1);
63
163
  if (rest.length === 0) {
64
164
  process.stdout.write("memtrace: latest installed.\n");
65
- process.exit(0);
165
+ // Offer RTK alongside (interactive default-yes). Awaits the flow
166
+ // so the prompt blocks before the process exits. Failure of the
167
+ // RTK install does NOT fail the memtrace install.
168
+ runRtkOptInFlow({ source: "install" })
169
+ .then(() => process.exit(0))
170
+ .catch((e) => {
171
+ console.error(`memtrace: RTK opt-in flow errored (non-fatal): ${e.message}`);
172
+ process.exit(0);
173
+ });
174
+ return;
66
175
  }
67
176
 
68
177
  // 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,96 @@ 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
+ let alreadyInstalled = false;
201
+ try { alreadyInstalled = rtk.detectRtk(); } catch (_) { /* skip */ }
202
+
203
+ if (alreadyInstalled) return; // RTK is here, no prompt
204
+ if (rtk.isRtkPromptDisabled({ env: process.env })) return; // user opted out
205
+
206
+ if (rtk.isRtkAutoInstall({ env: process.env })) {
207
+ console.log("\nmemtrace: installing RTK (MEMTRACE_AUTO_INSTALL_RTK)…");
208
+ const r = rtk.installRtk();
209
+ if (r.ok) console.log(`memtrace: RTK installed via ${r.method}`);
210
+ else console.warn(`memtrace: RTK install failed: ${r.error}`);
211
+ return;
212
+ }
213
+
214
+ // Try /dev/tty for an interactive prompt. Falls back to hint if
215
+ // the controlling terminal is unavailable.
216
+ const ttyStreams = rtk.openTtyStreams();
217
+ if (!ttyStreams) {
218
+ console.log("");
219
+ console.log(rtk.rtkHintLine());
220
+ return;
221
+ }
222
+
223
+ let answered = false;
224
+ try {
225
+ const rl = readline.createInterface({
226
+ input: ttyStreams.input,
227
+ output: ttyStreams.output,
228
+ terminal: true,
229
+ });
230
+ const answer = await new Promise((resolve) => {
231
+ rl.question(rtk.rtkPromptText({ source: "postinstall" }), resolve);
232
+ });
233
+ rl.close();
234
+ answered = true;
235
+
236
+ if (!rtk.parseRtkAnswer(answer)) {
237
+ ttyStreams.output.write(
238
+ "memtrace: OK, skipping RTK. Install later: memtrace install-rtk\n",
239
+ );
240
+ return;
241
+ }
242
+
243
+ ttyStreams.output.write("memtrace: installing RTK…\n");
244
+ const r = rtk.installRtk();
245
+ if (r.ok) {
246
+ ttyStreams.output.write(
247
+ `memtrace: RTK installed via ${r.method}. Run 'rtk gain' for the savings ledger.\n`,
248
+ );
249
+ } else {
250
+ ttyStreams.output.write(
251
+ `memtrace: RTK install failed (${r.method}): ${r.error}\n` +
252
+ `memtrace: Memtrace itself is fine. Manual install:\n` +
253
+ ` curl -fsSL ${rtk.RTK_INSTALL_URL} | sh\n`,
254
+ );
255
+ }
256
+ } catch (e) {
257
+ // Any error in the prompt path is non-fatal for memtrace itself.
258
+ if (!answered) {
259
+ console.log(rtk.rtkHintLine());
260
+ } else {
261
+ console.warn(`memtrace: RTK install errored: ${e.message}`);
262
+ }
263
+ } finally {
264
+ ttyStreams.close();
265
+ }
266
+ })().catch((e) => {
267
+ console.warn(`memtrace: RTK opt-in flow errored: ${e.message}`);
268
+ });
177
269
  }
178
270
 
179
271
  module.exports = { getBinaryPath };
@@ -0,0 +1,357 @@
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
+ // ── Decision helpers ────────────────────────────────────────────────
57
+
58
+ /**
59
+ * Should the caller PROMPT the user for RTK install? Pure: no IO.
60
+ *
61
+ * @param {object} input
62
+ * @param {boolean} input.isTty
63
+ * @param {boolean} input.alreadyInstalled
64
+ * @param {object} [input.env]
65
+ * @returns {boolean}
66
+ */
67
+ function shouldPromptForRtk(input) {
68
+ if (!input || typeof input !== "object") return false;
69
+ if (!input.isTty) return false;
70
+ if (input.alreadyInstalled) return false;
71
+ if (isRtkPromptDisabled(input)) return false;
72
+ if (isRtkAutoInstall(input)) return false; // auto bypasses prompt → install path
73
+ return true;
74
+ }
75
+
76
+ /**
77
+ * Compute the routing decision for the RTK opt-in flow. Returns
78
+ * { action: "install" | "prompt" | "skip" | "hint", reason: string }
79
+ * so callers can both act and produce a consistent log line.
80
+ *
81
+ * Decision precedence (top wins):
82
+ * 1. already-installed → skip (RTK is here, nothing to do)
83
+ * 2. NO_RTK_PROMPT → skip (user explicitly opted out)
84
+ * 3. AUTO_INSTALL_RTK → install (user explicitly opted in)
85
+ * 4. interactive TTY → prompt (default-yes interactive flow)
86
+ * 5. otherwise → hint (no TTY, no env signal — print one-line hint, skip install)
87
+ */
88
+ function effectiveRtkAction(input) {
89
+ if (!input || typeof input !== "object") {
90
+ return { action: "skip", reason: "invalid-input" };
91
+ }
92
+ if (input.alreadyInstalled) {
93
+ return { action: "skip", reason: "already-installed" };
94
+ }
95
+ if (isRtkPromptDisabled(input)) {
96
+ return { action: "skip", reason: "suppressed" };
97
+ }
98
+ if (isRtkAutoInstall(input)) {
99
+ return { action: "install", reason: "auto-env" };
100
+ }
101
+ if (input.isTty) {
102
+ return { action: "prompt", reason: "interactive" };
103
+ }
104
+ return { action: "hint", reason: "no-tty" };
105
+ }
106
+
107
+ // ── Install-strategy helper ────────────────────────────────────────
108
+
109
+ const RTK_INSTALL_URL =
110
+ "https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh";
111
+
112
+ /**
113
+ * Pick the right install method for the host platform. Pure.
114
+ *
115
+ * Returns:
116
+ * { method: "brew" | "curl" | "unsupported",
117
+ * command: string[] // argv to spawnSync (empty for unsupported)
118
+ * reason?: string } // for unsupported, why
119
+ */
120
+ function chooseInstallStrategy(input) {
121
+ const platform = (input && input.platform) || "linux";
122
+ const hasBrew = Boolean(input && input.hasBrew);
123
+
124
+ if (platform === "win32") {
125
+ return {
126
+ method: "unsupported",
127
+ command: [],
128
+ reason: "RTK does not ship a Windows installer; install WSL or skip RTK.",
129
+ };
130
+ }
131
+
132
+ if (platform === "darwin" && hasBrew) {
133
+ return {
134
+ method: "brew",
135
+ command: ["brew", "install", "rtk"],
136
+ };
137
+ }
138
+
139
+ // Linux + macOS-without-brew → official curl|sh installer.
140
+ return {
141
+ method: "curl",
142
+ command: [
143
+ "sh",
144
+ "-c",
145
+ `set -e; curl -fsSL ${RTK_INSTALL_URL} | sh`,
146
+ ],
147
+ };
148
+ }
149
+
150
+ // ── Side-effecting helpers (thin shells; the real work is in argv) ─
151
+
152
+ /**
153
+ * Detect whether RTK is already installed AND is the correct one (rtk-ai
154
+ * version, not the Rust Type Kit collision). Returns true only when
155
+ * `rtk gain` succeeds — per RTK's own README, that's the canonical
156
+ * verification because `rtk --version` also passes for the wrong RTK.
157
+ *
158
+ * Synchronous so it composes with the npm postinstall flow.
159
+ *
160
+ * @param {{spawnSync?: function}} [opts]
161
+ * @returns {boolean}
162
+ */
163
+ function detectRtk(opts = {}) {
164
+ const spawnSync = opts.spawnSync || require("child_process").spawnSync;
165
+ // `rtk gain --help` is a fast no-op that proves we have the
166
+ // rtk-ai binary. We don't actually need to run `rtk gain` itself
167
+ // (which would hit network / read history) — `--help` parsing is
168
+ // enough to fail loudly on the wrong binary.
169
+ const r = spawnSync("rtk", ["gain", "--help"], {
170
+ stdio: "ignore",
171
+ timeout: 5000,
172
+ });
173
+ return r.status === 0;
174
+ }
175
+
176
+ /**
177
+ * Detect whether Homebrew is on PATH. Used to pick install strategy
178
+ * on macOS.
179
+ */
180
+ function detectHomebrew(opts = {}) {
181
+ const spawnSync = opts.spawnSync || require("child_process").spawnSync;
182
+ const r = spawnSync("brew", ["--version"], {
183
+ stdio: "ignore",
184
+ timeout: 3000,
185
+ });
186
+ return r.status === 0;
187
+ }
188
+
189
+ /**
190
+ * Side-effecting RTK installer. Spawns the chosen install command,
191
+ * inheriting stdio so the user sees progress. Returns
192
+ * { ok: boolean, method: string, error?: string }
193
+ */
194
+ function installRtk(opts = {}) {
195
+ const spawnSync = opts.spawnSync || require("child_process").spawnSync;
196
+ const platform = opts.platform || process.platform;
197
+ const hasBrew = opts.hasBrew !== undefined
198
+ ? opts.hasBrew
199
+ : (platform === "darwin" && detectHomebrew({ spawnSync }));
200
+
201
+ const strategy = chooseInstallStrategy({ platform, hasBrew });
202
+
203
+ if (strategy.method === "unsupported") {
204
+ return { ok: false, method: strategy.method, error: strategy.reason };
205
+ }
206
+
207
+ const [cmd, ...args] = strategy.command;
208
+ const r = spawnSync(cmd, args, {
209
+ stdio: "inherit",
210
+ timeout: 120000, // 2 min cap; brew/curl install should be well under this
211
+ });
212
+ if (r.status !== 0) {
213
+ return {
214
+ ok: false,
215
+ method: strategy.method,
216
+ error: `${cmd} exited with status ${r.status}`,
217
+ };
218
+ }
219
+
220
+ // Verify the right RTK landed.
221
+ if (!detectRtk({ spawnSync })) {
222
+ return {
223
+ ok: false,
224
+ method: strategy.method,
225
+ error:
226
+ "rtk install reported success but `rtk gain --help` did not work. " +
227
+ "You may have hit the Rust-Type-Kit name collision — see " +
228
+ "https://github.com/rtk-ai/rtk/blob/master/INSTALL.md",
229
+ };
230
+ }
231
+
232
+ return { ok: true, method: strategy.method };
233
+ }
234
+
235
+ /**
236
+ * Print the one-line hint for the no-TTY path. Separated so callers
237
+ * can route output to wherever (stderr, log file, etc.).
238
+ */
239
+ function rtkHintLine() {
240
+ return (
241
+ "memtrace: RTK (rtk-ai/rtk) is a complementary token-savings tool. " +
242
+ "Install alongside with: memtrace install-rtk"
243
+ );
244
+ }
245
+
246
+ // ── /dev/tty prompting (for npm postinstall, where stdin is piped) ──
247
+ //
248
+ // npm postinstall runs with stdin piped to npm — process.stdin.isTTY
249
+ // is false even when the user is sitting at a terminal. Plenty of
250
+ // well-known npm installers (create-react-app, playwright, prisma)
251
+ // work around this by opening /dev/tty directly. We do the same.
252
+ //
253
+ // On Windows there is no /dev/tty. We return null and the caller
254
+ // falls back to the hint path.
255
+
256
+ /**
257
+ * Open read+write streams on the controlling terminal.
258
+ *
259
+ * @param {object} [opts]
260
+ * @param {string} [opts.platform] defaults to process.platform
261
+ * @param {object} [opts.fs] defaults to require("fs")
262
+ * @param {object} [opts.tty] defaults to require("tty")
263
+ * @returns {{input: ReadStream, output: WriteStream, close: function}|null}
264
+ * null if /dev/tty cannot be opened (Win32, Docker without
265
+ * tty, sandboxed environments).
266
+ */
267
+ function openTtyStreams(opts = {}) {
268
+ const platform = opts.platform || process.platform;
269
+ if (platform === "win32") {
270
+ // Windows has CONIN$/CONOUT$ but the stream wrappers don't
271
+ // round-trip cleanly through readline. Fall back to hint.
272
+ return null;
273
+ }
274
+
275
+ const fs = opts.fs || require("fs");
276
+ const tty = opts.tty || require("tty");
277
+
278
+ let fd;
279
+ try {
280
+ fd = fs.openSync("/dev/tty", "r+");
281
+ } catch (_) {
282
+ return null; // no controlling tty (Docker, CI, etc.)
283
+ }
284
+
285
+ let input, output;
286
+ try {
287
+ input = new tty.ReadStream(fd);
288
+ output = new tty.WriteStream(fd);
289
+ } catch (e) {
290
+ try { fs.closeSync(fd); } catch (_) { /* best-effort */ }
291
+ return null;
292
+ }
293
+
294
+ return {
295
+ input,
296
+ output,
297
+ close() {
298
+ try { input.destroy(); } catch (_) { /* best-effort */ }
299
+ try { output.destroy(); } catch (_) { /* best-effort */ }
300
+ },
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Pure parser: did the user accept the [Y/n] prompt?
306
+ *
307
+ * Default-yes: empty input (just Enter), unrecognised input → true.
308
+ * Only explicit "n" / "no" (case-insensitive, trimmed) → false.
309
+ */
310
+ function parseRtkAnswer(answer) {
311
+ if (typeof answer !== "string") return true; // default-yes
312
+ const trimmed = answer.trim().toLowerCase();
313
+ if (trimmed === "") return true;
314
+ if (trimmed === "n" || trimmed === "no") return false;
315
+ return true;
316
+ }
317
+
318
+ /**
319
+ * Build the prompt text. Pure so we can keep the IO wrapper thin.
320
+ */
321
+ function rtkPromptText({ source } = {}) {
322
+ const heading =
323
+ source === "install"
324
+ ? "Memtrace is installed. One more thing —"
325
+ : source === "postinstall"
326
+ ? "Memtrace is installed. One more thing —"
327
+ : "Add RTK to your toolchain?";
328
+ return (
329
+ `\n[memtrace] ${heading}\n` +
330
+ `\n` +
331
+ `RTK (Rust Token Killer, https://rtk-ai.app) is a separate CLI proxy\n` +
332
+ `that filters shell-command output before it reaches your AI assistant.\n` +
333
+ `Memtrace eliminates token waste from code lookups; RTK does the same\n` +
334
+ `for shell output. They're complementary.\n` +
335
+ `\n` +
336
+ `Install RTK alongside Memtrace? [Y/n] `
337
+ );
338
+ }
339
+
340
+ module.exports = {
341
+ // Pure helpers
342
+ isRtkPromptDisabled,
343
+ isRtkAutoInstall,
344
+ shouldPromptForRtk,
345
+ effectiveRtkAction,
346
+ chooseInstallStrategy,
347
+ rtkHintLine,
348
+ parseRtkAnswer,
349
+ rtkPromptText,
350
+ // IO helpers (injectable spawnSync / fs / tty for tests)
351
+ detectRtk,
352
+ detectHomebrew,
353
+ installRtk,
354
+ openTtyStreams,
355
+ // Constants
356
+ RTK_INSTALL_URL,
357
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrace",
3
- "version": "0.3.41",
3
+ "version": "0.3.42",
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"