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 +26 -8
- package/hooks/posttool-mcp-telemetry.sh +56 -0
- package/hooks/userprompt-claude.sh +102 -0
- package/install.js +48 -3
- package/lib/claude-integration.js +447 -0
- package/lib/skill-metadata.js +303 -0
- package/lib/spawn-helper.js +63 -0
- package/lib/upgrade-skills.js +72 -0
- package/package.json +6 -4
- package/uninstall.js +107 -39
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
platformBinary("npm", process.platform),
|
|
73
81
|
["install", "--no-save", versioned],
|
|
74
|
-
|
|
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.
|
|
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 });
|