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 +1 -115
- package/install.js +1 -101
- package/package.json +4 -4
- package/lib/rtk-integration.js +0 -492
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
|
-
|
|
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.
|
|
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.
|
|
43
|
-
"@memtrace/linux-x64": "0.3.
|
|
44
|
-
"@memtrace/win32-x64": "0.3.
|
|
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"
|
package/lib/rtk-integration.js
DELETED
|
@@ -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
|
-
};
|