claude-code-cache-fix 1.9.0 → 1.9.2

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/README.md CHANGED
@@ -104,6 +104,70 @@ The wrapper dynamically resolves your npm global root, constructs a `file:///` U
104
104
 
105
105
  Credit: [@TomTheMenace](https://github.com/anthropics/claude-code/issues/38335) contributed the Windows wrapper and validated the interceptor across a 7.5-hour, 536-call Opus 4.6 session on Windows — 98.4% cache hit rate, 81% of calls had fingerprint instability that the interceptor corrected.
106
106
 
107
+ ## VS Code Extension
108
+
109
+ ### Option A: VSIX extension (recommended)
110
+
111
+ The easiest path — a VS Code extension that handles everything automatically:
112
+
113
+ 1. Install the interceptor: `npm install -g claude-code-cache-fix`
114
+ 2. Download the VSIX from [GitHub Releases](https://github.com/cnighswonger/claude-code-cache-fix-vscode/releases/latest)
115
+ 3. Install: `code --install-extension claude-code-cache-fix-0.1.0.vsix`
116
+ (or in VS Code: Extensions → `...` menu → "Install from VSIX...")
117
+ 4. Restart any active Claude Code session
118
+
119
+ The extension auto-configures `claudeCode.claudeProcessWrapper` on activation. No manual settings needed. Works on Windows, macOS, and Linux.
120
+
121
+ Commands available in the VS Code command palette:
122
+ - **Claude Code Cache Fix: Enable** / **Disable** / **Show Status**
123
+
124
+ ### Option B: Manual wrapper (if you prefer not to install the VSIX)
125
+
126
+ The VS Code Claude Code extension spawns `claude.exe` / `claude` as a subprocess. The `claude-code.environmentVariables` setting does **not** propagate `NODE_OPTIONS`, so a process wrapper is required.
127
+
128
+ **Linux / macOS** — create `~/bin/claude-vscode-wrapper`:
129
+
130
+ ```bash
131
+ #!/bin/bash
132
+ NPM_ROOT="$(npm root -g 2>/dev/null)"
133
+ PRELOAD="$NPM_ROOT/claude-code-cache-fix/preload.mjs"
134
+ shift # VS Code passes the original claude path as $1
135
+ export NODE_OPTIONS="--import $PRELOAD"
136
+ exec node "$NPM_ROOT/@anthropic-ai/claude-code/cli.js" "$@"
137
+ ```
138
+
139
+ ```bash
140
+ chmod +x ~/bin/claude-vscode-wrapper
141
+ ```
142
+
143
+ Add to VS Code `settings.json`:
144
+
145
+ ```json
146
+ {
147
+ "claudeCode.claudeProcessWrapper": "/home/YOUR_USERNAME/bin/claude-vscode-wrapper"
148
+ }
149
+ ```
150
+
151
+ **Windows** — `.bat`/`.cmd` wrappers fail because the extension uses `child_process.spawn()` without `shell: true`. Use the C wrapper source included in this package (`tools/claude-vscode-wrapper.c`):
152
+
153
+ ```cmd
154
+ cl tools\claude-vscode-wrapper.c /Fe:claude-vscode-wrapper.exe
155
+ ```
156
+
157
+ Then set in VS Code `settings.json`:
158
+
159
+ ```json
160
+ {
161
+ "claudeCode.claudeProcessWrapper": "C:\\path\\to\\claude-vscode-wrapper.exe"
162
+ }
163
+ ```
164
+
165
+ ### Known limitations (VS Code)
166
+
167
+ - **Fingerprint fix auto-disables**: The VS Code extension constructs `messages[0]` differently than the CLI, causing the fingerprint safety check to trip. Use `CACHE_FIX_SKIP_FINGERPRINT=1` as a workaround. All other fixes (relocate, tool sort, TTL, /clear artifact strip) work normally.
168
+
169
+ Credit: [@JEONG-JIWOO](https://github.com/JEONG-JIWOO) and [@X-15](https://github.com/X-15) for the VS Code extension investigation and C wrapper ([#16](https://github.com/cnighswonger/claude-code-cache-fix/issues/16)).
170
+
107
171
  ## How it works
108
172
 
109
173
  The module intercepts `globalThis.fetch` before Claude Code makes API calls to `/v1/messages`. On each call it:
@@ -522,6 +586,9 @@ measurable signature of cache-efficiency degradation.
522
586
  - **[@fgrosswig](https://github.com/fgrosswig)** — [claude-usage-dashboard](https://github.com/fgrosswig/claude-usage-dashboard) forensic methodology: cost-factor overhead ratio metric, `anthropic-*` header capture pattern, proxy NDJSON schema that informed our dashboard interop layer
523
587
  - **[@TomTheMenace](https://github.com/TomTheMenace)** — Windows `.bat` wrapper for the interceptor, first Windows platform validation (7.5h/536-call Opus 4.6 session, 98.4% cache hit rate, 81% fingerprint instability corrected)
524
588
  - **[@arjansingh](https://github.com/arjansingh)** — nvm-compatible wrapper script with dynamic `npm root -g` path resolution (PR #15)
589
+ - **[@beekamai](https://github.com/beekamai)** — Windows URL-encoding fix for `claude-fixed.bat` when npm root contains spaces (PR #17)
590
+ - **[@JEONG-JIWOO](https://github.com/JEONG-JIWOO)** — VS Code extension investigation: discovered `claudeCode.claudeProcessWrapper` as the working integration path, wrote the C wrapper for Windows (#16)
591
+ - **[@X-15](https://github.com/X-15)** — VS Code extension validation, per-fix health status analysis confirming safety check behavior on v2.1.105 (#16)
525
592
 
526
593
  If you contributed to the community effort on these issues and aren't listed here, please open an issue or PR — we want to credit everyone properly.
527
594
 
package/claude-fixed.bat CHANGED
@@ -17,6 +17,10 @@ REM
17
17
  REM Credit: @TomTheMenace (https://github.com/anthropics/claude-code/issues/38335)
18
18
  REM Part of claude-code-cache-fix: https://github.com/cnighswonger/claude-code-cache-fix
19
19
 
20
+ REM Resolve npm global root and URL-encode it so spaces (e.g. "C:\Program Files\nodejs")
21
+ REM don't break NODE_OPTIONS parsing. Without encoding, Node splits --import file:/// on
22
+ REM the literal space and fails with ERR_MODULE_NOT_FOUND: Cannot find module 'C:\Program'.
20
23
  for /f "delims=" %%G in ('npm root -g') do set "NPM_GLOBAL=%%G"
21
- set NODE_OPTIONS=--import file:///%NPM_GLOBAL:\=/%/claude-code-cache-fix/preload.mjs
24
+ for /f "delims=" %%U in ('powershell -NoProfile -Command "[System.Uri]::EscapeUriString(('%NPM_GLOBAL:\=/%' + '/claude-code-cache-fix/preload.mjs'))"') do set "PRELOAD_URL=%%U"
25
+ set NODE_OPTIONS=--import file:///%PRELOAD_URL%
22
26
  node "%NPM_GLOBAL%\@anthropic-ai\claude-code\cli.js" %*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-cache-fix",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "Fixes prompt cache regression in Claude Code that causes up to 20x cost increase on resumed sessions",
5
5
  "type": "module",
6
6
  "exports": "./preload.mjs",
@@ -32,6 +32,10 @@
32
32
  "url": "https://github.com/cnighswonger/claude-code-cache-fix/issues"
33
33
  },
34
34
  "homepage": "https://github.com/cnighswonger/claude-code-cache-fix#readme",
35
+ "funding": {
36
+ "type": "individual",
37
+ "url": "https://buymeacoffee.com/vsits"
38
+ },
35
39
  "license": "MIT",
36
- "author": "Chris Nighswonger"
40
+ "author": "Chris Nighswonger <chris@veritassuperaitsolutions.com> (https://veritassuperaitsolutions.com)"
37
41
  }
package/preload.mjs CHANGED
@@ -189,6 +189,20 @@ function isRelocatableBlock(text) {
189
189
  isMcpBlock(text)
190
190
  );
191
191
  }
192
+ /**
193
+ * Detect /clear command artifacts that bleed into the next session's messages[0].
194
+ * These blocks break prefix cache because a post-/clear session has different
195
+ * messages[0] content than a truly fresh session.
196
+ * Bug: anthropics/claude-code#47756
197
+ */
198
+ function isClearArtifact(text) {
199
+ if (typeof text !== "string") return false;
200
+ return (
201
+ text.startsWith("<local-command-caveat>") ||
202
+ text.startsWith("<command-name>") ||
203
+ text.startsWith("<local-command-stdout>")
204
+ );
205
+ }
192
206
 
193
207
  /**
194
208
  * Sort skill listing entries for deterministic ordering (prevents cache bust
@@ -332,6 +346,18 @@ function normalizeResumeMessages(messages) {
332
346
  const firstMsg = messages[firstUserIdx];
333
347
  if (!Array.isArray(firstMsg?.content)) return messages;
334
348
 
349
+ // FIX: Strip /clear command artifacts from messages[0] (anthropics/claude-code#47756).
350
+ // After /clear, CC leaves <local-command-caveat>, <command-name>/clear, and
351
+ // <local-command-stdout> blocks in messages[0] of the new session, breaking
352
+ // prefix match vs a truly fresh session.
353
+ const beforeClearStrip = firstMsg.content.length;
354
+ firstMsg.content = firstMsg.content.filter((block) => !isClearArtifact(block.text || ""));
355
+ if (firstMsg.content.length < beforeClearStrip) {
356
+ const stripped = beforeClearStrip - firstMsg.content.length;
357
+ debugLog(`APPLIED: stripped ${stripped} /clear artifact block(s) from messages[0]`);
358
+ recordFixResult("relocate", "applied");
359
+ }
360
+
335
361
  // FIX: Check if ANY relocatable blocks are scattered outside first user msg.
336
362
  // The old check (firstAlreadyHas → skip) missed partial scatter where some
337
363
  // blocks stay in messages[0] but others drift to later messages (v2.1.89+).
@@ -1677,6 +1703,7 @@ export {
1677
1703
  isHooksBlock,
1678
1704
  isMcpBlock,
1679
1705
  isRelocatableBlock,
1706
+ isClearArtifact,
1680
1707
  rewriteOutputEfficiencyInstruction,
1681
1708
  normalizeOutputEfficiencyReplacement,
1682
1709
  _pinnedBlocks, // exported so tests can reset between runs
@@ -0,0 +1,76 @@
1
+ /*
2
+ * claude-vscode-wrapper.c — Native wrapper for Claude Code + cache-fix on VS Code (Windows)
3
+ *
4
+ * The VS Code Claude extension uses child_process.spawn() without shell: true,
5
+ * so .bat/.cmd wrappers fail with EINVAL. This native exe sets NODE_OPTIONS
6
+ * and spawns node cli.js with the interceptor loaded.
7
+ *
8
+ * Compile: cl claude-vscode-wrapper.c /Fe:claude-vscode-wrapper.exe
9
+ * or: gcc -o claude-vscode-wrapper.exe claude-vscode-wrapper.c
10
+ *
11
+ * Usage in VS Code settings.json:
12
+ * { "claudeCode.claudeProcessWrapper": "C:\\path\\to\\claude-vscode-wrapper.exe" }
13
+ *
14
+ * Credit: @JEONG-JIWOO (original implementation, #16)
15
+ */
16
+
17
+ #include <stdio.h>
18
+ #include <stdlib.h>
19
+ #include <string.h>
20
+ #include <process.h>
21
+ #include <windows.h>
22
+
23
+ int main(int argc, char *argv[]) {
24
+ char *appdata = getenv("APPDATA");
25
+ if (!appdata) {
26
+ fprintf(stderr, "APPDATA not set\n");
27
+ return 1;
28
+ }
29
+
30
+ /* Build the preload path with forward slashes for file:// URL */
31
+ char preload[MAX_PATH];
32
+ snprintf(preload, sizeof(preload),
33
+ "%s\\npm\\node_modules\\claude-code-cache-fix\\preload.mjs", appdata);
34
+
35
+ char preload_url[MAX_PATH];
36
+ strcpy(preload_url, preload);
37
+ for (char *p = preload_url; *p; p++) {
38
+ if (*p == '\\') *p = '/';
39
+ }
40
+
41
+ /* URL-encode spaces for NODE_OPTIONS parsing */
42
+ char encoded_url[MAX_PATH * 3];
43
+ char *dst = encoded_url;
44
+ for (const char *src = preload_url; *src && dst < encoded_url + sizeof(encoded_url) - 4; src++) {
45
+ if (*src == ' ') {
46
+ *dst++ = '%'; *dst++ = '2'; *dst++ = '0';
47
+ } else {
48
+ *dst++ = *src;
49
+ }
50
+ }
51
+ *dst = '\0';
52
+
53
+ char node_opts[MAX_PATH * 4];
54
+ snprintf(node_opts, sizeof(node_opts),
55
+ "NODE_OPTIONS=--import file:///%s", encoded_url);
56
+ _putenv(node_opts);
57
+
58
+ /* Path to Claude Code CLI */
59
+ char cli_path[MAX_PATH];
60
+ snprintf(cli_path, sizeof(cli_path),
61
+ "%s\\npm\\node_modules\\@anthropic-ai\\claude-code\\cli.js", appdata);
62
+
63
+ /* Build argv: skip argv[1] (original claude path passed by extension) */
64
+ char **new_argv = malloc(sizeof(char *) * (argc + 2));
65
+ if (!new_argv) return 1;
66
+
67
+ new_argv[0] = "node";
68
+ new_argv[1] = cli_path;
69
+ int j = 2;
70
+ for (int i = 2; i < argc; i++) {
71
+ new_argv[j++] = argv[i];
72
+ }
73
+ new_argv[j] = NULL;
74
+
75
+ return _spawnvp(_P_WAIT, "node", (const char *const *)new_argv);
76
+ }
@@ -6,9 +6,33 @@
6
6
  input=$(cat)
7
7
 
8
8
  JSONL="$HOME/.claude/claude-meter.jsonl"
9
+ QS="$HOME/.claude/quota-status.json"
9
10
 
11
+ # Primary source: claude-meter.jsonl (requires claude-code-meter package)
12
+ # Fallback: quota-status.json (written by claude-code-cache-fix interceptor)
10
13
  if [ -f "$JSONL" ]; then
11
14
  last=$(tail -1 "$JSONL" 2>/dev/null)
15
+ elif [ -f "$QS" ]; then
16
+ # Translate quota-status.json into the same shape the Python expects
17
+ last=$(python3 -c "
18
+ import json, pathlib
19
+ qs = json.load(open(pathlib.Path.home() / '.claude' / 'quota-status.json'))
20
+ fh = qs.get('five_hour', {})
21
+ sd = qs.get('seven_day', {})
22
+ print(json.dumps({
23
+ 'q5h': fh.get('utilization', 0),
24
+ 'q7d': sd.get('utilization', 0),
25
+ 'q5h_reset': fh.get('resets_at', 0),
26
+ 'q7d_reset': sd.get('resets_at', 0),
27
+ 'qoverage': qs.get('overage_status', ''),
28
+ 'ts': qs.get('timestamp', ''),
29
+ }))
30
+ " 2>/dev/null)
31
+ else
32
+ exit 0
33
+ fi
34
+
35
+ if [ -z "$last" ]; then exit 0; fi
12
36
 
13
37
  result=$(echo "$last" | python3 -c "
14
38
  import sys, json
@@ -83,4 +107,3 @@ print(label)
83
107
  " 2>/dev/null)
84
108
 
85
109
  [ -n "$result" ] && echo "$result"
86
- fi