memtrace 0.3.44 → 0.3.47

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,7 +10,6 @@ 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");
14
13
 
15
14
  // ── Handle `memtrace uninstall` before delegating to the Rust binary ────────
16
15
  // npm v7+ does NOT fire preuninstall hooks for global packages (npm/cli#3042).
@@ -18,105 +17,6 @@ const rtkIntegration = require("../lib/rtk-integration");
18
17
 
19
18
  const args = process.argv.slice(2);
20
19
 
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
-
120
20
  // ── Handle `memtrace install` — always pull latest, then run remaining args ──
121
21
  //
122
22
  // `npm install -g memtrace` fetches latest from the registry on a fresh
@@ -138,11 +38,6 @@ if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
138
38
  const memtraceCmd = platformBinary("memtrace", process.platform);
139
39
 
140
40
  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).
146
41
  const installResult = spawnSync(
147
42
  npmCmd,
148
43
  ["install", "-g", "memtrace@latest"],
@@ -167,16 +62,7 @@ if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
167
62
  const rest = args.slice(1);
168
63
  if (rest.length === 0) {
169
64
  process.stdout.write("memtrace: latest installed.\n");
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;
65
+ process.exit(0);
180
66
  }
181
67
 
182
68
  // Chain into the upgraded binary. PATH now resolves `memtrace` to the
package/install.js CHANGED
@@ -162,9 +162,7 @@ if (require.main === module) {
162
162
  console.warn(`memtrace: Claude Code integration setup skipped: ${e.message}`);
163
163
  }
164
164
 
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.
165
+ // 5. Persist uninstall script to ~/.memtrace/ (survives `npm uninstall -g`).
168
166
  try {
169
167
  const memtraceDir = path.join(os.homedir(), ".memtrace");
170
168
  fs.mkdirSync(memtraceDir, { recursive: true });
@@ -176,104 +174,6 @@ if (require.main === module) {
176
174
  } catch (e) {
177
175
  console.warn(`memtrace: failed to persist uninstall script: ${e.message}`);
178
176
  }
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
- });
277
177
  }
278
178
 
279
179
  module.exports = { getBinaryPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrace",
3
- "version": "0.3.44",
3
+ "version": "0.3.47",
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.44",
43
- "@memtrace/linux-x64": "0.3.44",
44
- "@memtrace/win32-x64": "0.3.44"
42
+ "@memtrace/darwin-arm64": "0.3.47",
43
+ "@memtrace/linux-x64": "0.3.47",
44
+ "@memtrace/win32-x64": "0.3.47"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=18"
@@ -1,492 +0,0 @@
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
- // ── 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
-
291
- /**
292
- * Side-effecting RTK installer. Spawns the chosen install command,
293
- * inheriting stdio so the user sees progress. Returns
294
- * { ok: boolean, method: string, error?: string }
295
- */
296
- function installRtk(opts = {}) {
297
- const spawnSync = opts.spawnSync || require("child_process").spawnSync;
298
- const platform = opts.platform || process.platform;
299
- const hasBrew = opts.hasBrew !== undefined
300
- ? opts.hasBrew
301
- : (platform === "darwin" && detectHomebrew({ spawnSync }));
302
-
303
- const strategy = chooseInstallStrategy({ platform, hasBrew });
304
-
305
- if (strategy.method === "unsupported") {
306
- return { ok: false, method: strategy.method, error: strategy.reason };
307
- }
308
-
309
- const [cmd, ...args] = strategy.command;
310
- const r = spawnSync(cmd, args, {
311
- stdio: "inherit",
312
- timeout: 120000, // 2 min cap; brew/curl install should be well under this
313
- });
314
- if (r.status !== 0) {
315
- return {
316
- ok: false,
317
- method: strategy.method,
318
- error: `${cmd} exited with status ${r.status}`,
319
- };
320
- }
321
-
322
- // Verify the right RTK landed.
323
- if (!detectRtk({ spawnSync })) {
324
- return {
325
- ok: false,
326
- method: strategy.method,
327
- error:
328
- "rtk install reported success but `rtk gain --help` did not work. " +
329
- "You may have hit the Rust-Type-Kit name collision — see " +
330
- "https://github.com/rtk-ai/rtk/blob/master/INSTALL.md",
331
- };
332
- }
333
-
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
- };
356
- }
357
-
358
- /**
359
- * Print the one-line hint for the no-TTY path. Separated so callers
360
- * can route output to wherever (stderr, log file, etc.).
361
- */
362
- function rtkHintLine() {
363
- return (
364
- "memtrace: RTK (rtk-ai/rtk) is a complementary token-savings tool. " +
365
- "Install alongside with: memtrace install-rtk"
366
- );
367
- }
368
-
369
- // ── /dev/tty prompting (for npm postinstall, where stdin is piped) ──
370
- //
371
- // npm postinstall runs with stdin piped to npm — process.stdin.isTTY
372
- // is false even when the user is sitting at a terminal. Plenty of
373
- // well-known npm installers (create-react-app, playwright, prisma)
374
- // work around this by opening /dev/tty directly. We do the same.
375
- //
376
- // On Windows there is no /dev/tty. We return null and the caller
377
- // falls back to the hint path.
378
-
379
- /**
380
- * Open read+write streams on the controlling terminal.
381
- *
382
- * @param {object} [opts]
383
- * @param {string} [opts.platform] defaults to process.platform
384
- * @param {object} [opts.fs] defaults to require("fs")
385
- * @param {object} [opts.tty] defaults to require("tty")
386
- * @returns {{input: ReadStream, output: WriteStream, close: function}|null}
387
- * null if /dev/tty cannot be opened (Win32, Docker without
388
- * tty, sandboxed environments).
389
- */
390
- function openTtyStreams(opts = {}) {
391
- const platform = opts.platform || process.platform;
392
- if (platform === "win32") {
393
- // Windows has CONIN$/CONOUT$ but the stream wrappers don't
394
- // round-trip cleanly through readline. Fall back to hint.
395
- return null;
396
- }
397
-
398
- const fs = opts.fs || require("fs");
399
- const tty = opts.tty || require("tty");
400
-
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;
407
- try {
408
- inFd = fs.openSync("/dev/tty", "r");
409
- outFd = fs.openSync("/dev/tty", "w");
410
- } catch (_) {
411
- if (inFd != null) { try { fs.closeSync(inFd); } catch (_) {} }
412
- return null; // no controlling tty (Docker, CI, etc.)
413
- }
414
-
415
- let input, output;
416
- try {
417
- input = new tty.ReadStream(inFd);
418
- output = new tty.WriteStream(outFd);
419
- } catch (e) {
420
- try { fs.closeSync(inFd); } catch (_) {}
421
- try { fs.closeSync(outFd); } catch (_) {}
422
- return null;
423
- }
424
-
425
- return {
426
- input,
427
- output,
428
- close() {
429
- try { input.destroy(); } catch (_) { /* best-effort */ }
430
- try { output.destroy(); } catch (_) { /* best-effort */ }
431
- },
432
- };
433
- }
434
-
435
- /**
436
- * Pure parser: did the user accept the [Y/n] prompt?
437
- *
438
- * Default-yes: empty input (just Enter), unrecognised input → true.
439
- * Only explicit "n" / "no" (case-insensitive, trimmed) → false.
440
- */
441
- function parseRtkAnswer(answer) {
442
- if (typeof answer !== "string") return true; // default-yes
443
- const trimmed = answer.trim().toLowerCase();
444
- if (trimmed === "") return true;
445
- if (trimmed === "n" || trimmed === "no") return false;
446
- return true;
447
- }
448
-
449
- /**
450
- * Build the prompt text. Pure so we can keep the IO wrapper thin.
451
- */
452
- function rtkPromptText({ source } = {}) {
453
- const heading =
454
- source === "install"
455
- ? "Memtrace is installed. One more thing —"
456
- : source === "postinstall"
457
- ? "Memtrace is installed. One more thing —"
458
- : "Add RTK to your toolchain?";
459
- return (
460
- `\n[memtrace] ${heading}\n` +
461
- `\n` +
462
- `RTK (Rust Token Killer, https://rtk-ai.app) is a separate CLI proxy\n` +
463
- `that filters shell-command output before it reaches your AI assistant.\n` +
464
- `Memtrace eliminates token waste from code lookups; RTK does the same\n` +
465
- `for shell output. They're complementary.\n` +
466
- `\n` +
467
- `Install RTK alongside Memtrace? [Y/n] `
468
- );
469
- }
470
-
471
- module.exports = {
472
- // Pure helpers
473
- isRtkPromptDisabled,
474
- isRtkAutoInstall,
475
- isPostinstallNestedUnderShim,
476
- shouldPromptForRtk,
477
- effectiveRtkAction,
478
- chooseInstallStrategy,
479
- chooseRtkInitCommand,
480
- isRtkInitAlreadyDone,
481
- rtkHintLine,
482
- parseRtkAnswer,
483
- rtkPromptText,
484
- // IO helpers (injectable spawnSync / fs / tty for tests)
485
- detectRtk,
486
- detectHomebrew,
487
- installRtk,
488
- runRtkInit,
489
- openTtyStreams,
490
- // Constants
491
- RTK_INSTALL_URL,
492
- };