memtrace 0.3.33 → 0.3.35

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
@@ -6,6 +6,7 @@ const path = require("path");
6
6
  const fs = require("fs");
7
7
  const { spawnSync, spawn } = require("child_process");
8
8
  const { getBinaryPath } = require("../install.js");
9
+ const { platformBinary, spawnOptionsForPlatform } = require("../lib/spawn-helper");
9
10
 
10
11
  // ── Handle `memtrace uninstall` before delegating to the Rust binary ────────
11
12
  // npm v7+ does NOT fire preuninstall hooks for global packages (npm/cli#3042).
@@ -27,14 +28,20 @@ const args = process.argv.slice(2);
27
28
  // which now resolves to the freshly-installed shim, not this old one.
28
29
 
29
30
  if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
30
- const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
31
- const memtraceCmd = process.platform === "win32" ? "memtrace.cmd" : "memtrace";
31
+ // `npm.cmd` on win32 needs `shell: true` since the CVE-2024-27980
32
+ // mitigation in Node 18.20+ / 20.12+ / 21.7+. The helpers in
33
+ // `lib/spawn-helper.js` are unit + property tested.
34
+ const npmCmd = platformBinary("npm", process.platform);
35
+ const memtraceCmd = platformBinary("memtrace", process.platform);
32
36
 
33
37
  process.stdout.write("memtrace: fetching latest from npm registry…\n");
34
38
  const installResult = spawnSync(
35
39
  npmCmd,
36
40
  ["install", "-g", "memtrace@latest"],
37
- { stdio: "inherit", env: process.env }
41
+ spawnOptionsForPlatform(process.platform, {
42
+ stdio: "inherit",
43
+ env: process.env,
44
+ })
38
45
  );
39
46
 
40
47
  if (installResult.error) {
@@ -60,10 +67,15 @@ if (args[0] === "install" || args[0] === "update" || args[0] === "upgrade") {
60
67
  process.stdout.write(
61
68
  `memtrace: upgrade complete — running 'memtrace ${rest.join(" ")}'\n`
62
69
  );
63
- const runResult = spawnSync(memtraceCmd, rest, {
64
- stdio: "inherit",
65
- env: process.env,
66
- });
70
+ // memtrace.cmd on Windows needs the same shell:true handling.
71
+ const runResult = spawnSync(
72
+ memtraceCmd,
73
+ rest,
74
+ spawnOptionsForPlatform(process.platform, {
75
+ stdio: "inherit",
76
+ env: process.env,
77
+ })
78
+ );
67
79
  if (runResult.error) {
68
80
  console.error(`memtrace: failed to chain command — ${runResult.error.message}`);
69
81
  process.exit(1);
@@ -80,7 +92,13 @@ if (args[0] === "uninstall" || args[0] === "cleanup") {
80
92
  let ran = false;
81
93
  for (const p of candidates) {
82
94
  try {
83
- require(p);
95
+ // Spawn the uninstall script as its own process so its top-level
96
+ // `if (require.main === module)` block fires. We deliberately
97
+ // avoid `require(p)` here: tests for the cleanup helpers
98
+ // import uninstall.js for its exports and would trigger the
99
+ // uninstall pipeline at import time if we relied on side effect.
100
+ const result = spawnSync(process.execPath, [p], { stdio: "inherit" });
101
+ if (result.error) continue;
84
102
  ran = true;
85
103
  break;
86
104
  } catch (e) {
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Memtrace PostToolUse hook for Claude Code — adoption telemetry.
4
+ #
5
+ # Fires after every tool call that matched the registered matcher
6
+ # (`mcp__memtrace__.*`). Logs the call locally to
7
+ # `~/.memtrace/adoption.jsonl` so we can measure whether v0.3.35's
8
+ # enforcement layers actually moved Memtrace tool-invocation rates.
9
+ #
10
+ # This is LOCAL adoption tracking — the file lives in the user's
11
+ # home directory, never leaves the machine. Aggregate counts may be
12
+ # included in opt-in telemetry pings if the user has enabled them
13
+ # (see PRIVACY.md).
14
+ #
15
+ # Hook never blocks. Always exit 0.
16
+ #
17
+ # Override:
18
+ # MEMTRACE_ADOPTION_LOG=off → unconditional no-op
19
+ #
20
+ set -euo pipefail
21
+
22
+ [[ "${MEMTRACE_ADOPTION_LOG:-on}" == "off" ]] && exit 0
23
+
24
+ # Read PostToolUse input from stdin. Shape:
25
+ # { "session_id", "cwd", "hook_event_name", "tool_name", "tool_input", ... }
26
+ input="$(cat)"
27
+
28
+ # Cheap exit if the JSON is malformed.
29
+ [[ -z "$input" ]] && exit 0
30
+
31
+ log_dir="${MEMTRACE_ADOPTION_DIR:-$HOME/.memtrace}"
32
+ log_file="$log_dir/adoption.jsonl"
33
+ mkdir -p "$log_dir" 2>/dev/null || exit 0
34
+
35
+ # Append a single-line JSON record. We strip `tool_input` because it
36
+ # can be large and may contain sensitive arguments. We keep the
37
+ # tool name + a high-resolution timestamp + session id only.
38
+ python3 - "$input" "$log_file" <<'PY' 2>/dev/null || true
39
+ import json, sys, os, time
40
+ input_str = sys.argv[1]
41
+ log_path = sys.argv[2]
42
+ try:
43
+ obj = json.loads(input_str)
44
+ except Exception:
45
+ sys.exit(0)
46
+ record = {
47
+ "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
48
+ "session_id": obj.get("session_id", ""),
49
+ "tool_name": obj.get("tool_name", ""),
50
+ "hook_event_name": obj.get("hook_event_name", ""),
51
+ }
52
+ with open(log_path, "a", encoding="utf-8") as f:
53
+ f.write(json.dumps(record) + "\n")
54
+ PY
55
+
56
+ exit 0
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Memtrace UserPromptSubmit hook for Claude Code.
4
+ #
5
+ # Fires once per user turn (NOT per tool call). Reads the user's
6
+ # prompt; if it looks like a code-discovery question and the
7
+ # Memtrace daemon is reachable, injects an `additionalContext`
8
+ # nudge so Claude considers Memtrace MCP tools BEFORE falling back
9
+ # to Read/Grep/Glob.
10
+ #
11
+ # Why UserPromptSubmit instead of PreToolUse-on-Read|Grep|Glob:
12
+ # - fires once per turn, not per tool call → no per-Read latency
13
+ # - gets the user's actual prompt → can decide based on intent,
14
+ # not on which file the model chose to grep
15
+ # - non-blocking → just adds context, never denies a tool call
16
+ #
17
+ # Exit codes:
18
+ # 0 : success (stdout is parsed for hook output)
19
+ # 2 : would block the prompt (we never want this)
20
+ #
21
+ # Hook output JSON shape (per code.claude.com/docs/en/hooks-guide.md):
22
+ # { "decision": "continue", "additionalContext": "..." }
23
+ #
24
+ # Override:
25
+ # MEMTRACE_HOOK_MODE=off → unconditional no-op
26
+ # MEMTRACE_HEALTH_URL=... → custom health endpoint (default 3030)
27
+ #
28
+ set -euo pipefail
29
+
30
+ mode="${MEMTRACE_HOOK_MODE:-advisory}"
31
+ [[ "$mode" == "off" ]] && exit 0
32
+
33
+ # ── Daemon liveness (portable: works on macOS/Linux/Windows-WSL) ──
34
+ #
35
+ # We use the Memtrace UI's status endpoint instead of `pgrep` so
36
+ # Windows + restricted-shell environments work the same way.
37
+ # 1-second timeout — must not slow Claude down.
38
+ health_url="${MEMTRACE_HEALTH_URL:-http://localhost:3030/api/health}"
39
+ if ! curl -sf --max-time 1 "$health_url" >/dev/null 2>&1; then
40
+ # Daemon unreachable: silent no-op. We never inject memtrace
41
+ # nudges when memtrace itself isn't running.
42
+ exit 0
43
+ fi
44
+
45
+ # ── Read prompt from stdin ──────────────────────────────────────────
46
+ input="$(cat)"
47
+
48
+ # Use python3 for JSON parsing — every macOS/Linux/Windows-with-Python
49
+ # has it; jq is more concise but less universal.
50
+ prompt="$(printf '%s' "$input" | python3 -c '
51
+ import json, sys
52
+ try:
53
+ obj = json.load(sys.stdin)
54
+ print(obj.get("prompt", ""), end="")
55
+ except Exception:
56
+ pass
57
+ ' 2>/dev/null || true)"
58
+
59
+ # If the prompt is empty or unparseable, no-op.
60
+ [[ -z "$prompt" ]] && exit 0
61
+
62
+ # ── Match code-discovery intent ─────────────────────────────────────
63
+ #
64
+ # The match list is intentionally generous on the "ask Memtrace" side
65
+ # and is anchored against directive verbs and possessive phrases that
66
+ # a real user prompt looks like. The agent's planner also uses these
67
+ # (e.g. "trace through", "find the function that") so this catches
68
+ # both human-typed and agent-internal phrasings.
69
+ shopt -s nocasematch
70
+ should_nudge=0
71
+ case "$prompt" in
72
+ *"where is"*|*"where's"*|*"how does"*|*"how is"*) should_nudge=1 ;;
73
+ *"what calls"*|*"who calls"*|*"callers of"*|*"callees of"*) should_nudge=1 ;;
74
+ *"why does"*|*"why is"*|*"why was"*) should_nudge=1 ;;
75
+ *"find the function"*|*"find the class"*|*"find the type"*) should_nudge=1 ;;
76
+ *"trace through"*|*"trace this"*|*"trace the"*) should_nudge=1 ;;
77
+ *"investigate"*|*"debug"*|*"diagnose"*) should_nudge=1 ;;
78
+ *"explain this"*|*"explain the code"*|*"understand this"*) should_nudge=1 ;;
79
+ *"audit"*|*"review"*|*"refactor"*) should_nudge=1 ;;
80
+ *"fix bug"*|*"fix the bug"*|*"broken"*|*"failing"*) should_nudge=1 ;;
81
+ *"impact of"*|*"what breaks if"*|*"safe to remove"*|*"safe to rename"*) should_nudge=1 ;;
82
+ *"locate"*|*"look up"*|*"search for "*) should_nudge=1 ;;
83
+ *"recent changes"*|*"what changed"*|*"evolution of"*) should_nudge=1 ;;
84
+ *"call graph"*|*"dependency"*|*"depend on"*|*"dependencies of"*) should_nudge=1 ;;
85
+ esac
86
+ shopt -u nocasematch
87
+
88
+ [[ "$should_nudge" -eq 0 ]] && exit 0
89
+
90
+ # ── Emit the nudge ──────────────────────────────────────────────────
91
+ #
92
+ # Per Anthropic's hook docs, UserPromptSubmit accepts a top-level
93
+ # `decision: continue` (don't block) plus `additionalContext` (which
94
+ # is injected as a system-style reminder Claude reads alongside the
95
+ # user's prompt). The wording is concise on purpose — this is added
96
+ # to context for every matching prompt, so token cost is real.
97
+ cat <<'EOF'
98
+ {
99
+ "decision": "continue",
100
+ "additionalContext": "Memtrace is active for this repository. For this code-discovery question, prefer the Memtrace MCP tools FIRST — `mcp__memtrace__find_code` for natural-language search, `mcp__memtrace__find_symbol` for exact lookup, `mcp__memtrace__get_symbol_context` for callers/callees, `mcp__memtrace__get_impact` for blast radius. They return exact file:start_line:end_line in one round-trip. Fall back to Read/Grep/Glob only for: (a) config files (.env, package.json, README, raw JSON/YAML/TOML), (b) file inventory questions, (c) paths confirmed outside any indexed repo, (d) reading exact lines you already have from a Memtrace result."
101
+ }
102
+ EOF
package/install.js CHANGED
@@ -5,6 +5,8 @@ const os = require("os");
5
5
  const path = require("path");
6
6
  const fs = require("fs");
7
7
  const { spawnSync } = require("child_process");
8
+ const { platformBinary, spawnOptionsForPlatform } = require("./lib/spawn-helper");
9
+ const { installToFs } = require("./lib/claude-integration");
8
10
 
9
11
  // ── Platform binary resolution (preserved from legacy) ───────────────────────
10
12
 
@@ -68,10 +70,20 @@ function selfHealPlatformPackage() {
68
70
  `memtrace: optional platform dep ${pkg} was not installed; ` +
69
71
  `running 'npm install ${versioned}' to fetch it…`
70
72
  );
73
+ // On Windows, Node.js 18.20+ / 20.12+ / 21.7+ refuse to spawn `.cmd`
74
+ // and `.bat` files without `shell: true` as part of the
75
+ // CVE-2024-27980 mitigation. The platform-aware helpers in
76
+ // `lib/spawn-helper.js` are unit + property tested so the shell
77
+ // flag is set IFF process.platform === "win32" — see
78
+ // `test/spawn-helper.test.js` for the full regression coverage.
71
79
  const result = spawnSync(
72
- process.platform === "win32" ? "npm.cmd" : "npm",
80
+ platformBinary("npm", process.platform),
73
81
  ["install", "--no-save", versioned],
74
- { cwd: __dirname, stdio: "inherit", env: process.env }
82
+ spawnOptionsForPlatform(process.platform, {
83
+ cwd: __dirname,
84
+ stdio: "inherit",
85
+ env: process.env,
86
+ })
75
87
  );
76
88
  if (result.status !== 0) {
77
89
  console.warn(
@@ -117,7 +129,40 @@ if (require.main === module) {
117
129
  console.warn("memtrace: run 'memtrace install' manually after the package is built.");
118
130
  }
119
131
 
120
- // 3. Persist uninstall script to ~/.memtrace/ (survives `npm uninstall -g`)
132
+ // 3. Claude Code adoption layer (v0.3.35+):
133
+ // - write ~/.claude/MEMTRACE.md (sidecar directive we own)
134
+ // - add a breadcrumb block to ~/.claude/CLAUDE.md if it exists
135
+ // - register UserPromptSubmit + PostToolUse hooks in
136
+ // ~/.claude/settings.json under `_managed_by: "memtrace"`
137
+ //
138
+ // This layer is what makes Claude Code consistently reach for
139
+ // Memtrace MCP tools FIRST instead of falling back to Read/Grep
140
+ // on indexed repos. Heuristic skill auto-invocation is too soft
141
+ // on its own — see lib/claude-integration.js for the why.
142
+ try {
143
+ const hooksDir = path.join(__dirname, "hooks");
144
+ // Make hook scripts executable on Unix (npm tarballs lose +x on
145
+ // some pack/unpack paths; doing this defensively).
146
+ if (os.platform() !== "win32" && fs.existsSync(hooksDir)) {
147
+ for (const entry of fs.readdirSync(hooksDir)) {
148
+ if (entry.endsWith(".sh")) {
149
+ try { fs.chmodSync(path.join(hooksDir, entry), 0o755); }
150
+ catch { /* best-effort */ }
151
+ }
152
+ }
153
+ }
154
+ const claudeDir = path.join(os.homedir(), ".claude");
155
+ const summary = installToFs({ claudeDir, hooksDir });
156
+ if (summary.wrote.length > 0) {
157
+ console.log(
158
+ `memtrace: configured Claude Code integration (${summary.wrote.length} file(s) updated)`
159
+ );
160
+ }
161
+ } catch (e) {
162
+ console.warn(`memtrace: Claude Code integration setup skipped: ${e.message}`);
163
+ }
164
+
165
+ // 4. Persist uninstall script to ~/.memtrace/ (survives `npm uninstall -g`)
121
166
  try {
122
167
  const memtraceDir = path.join(os.homedir(), ".memtrace");
123
168
  fs.mkdirSync(memtraceDir, { recursive: true });