claude-code-cache-fix 3.5.4 → 3.6.0

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
@@ -167,6 +167,43 @@ node "$(npm root -g)\claude-code-cache-fix\proxy\server.mjs"
167
167
 
168
168
  Stderr will print `[upstream] using proxy http://proxy.corp.example:8080 ...` on first request when the agent is wired correctly. With no proxy/CA env vars set, behavior is unchanged from earlier versions (Node default agent, system trust store).
169
169
 
170
+ ### Embedding the proxy in your own process
171
+
172
+ If you ship a Node or Bun binary that wants the cache-fix proxy in-process (e.g. a Bun-compiled agent that avoids forking a Node child), import the factory from `claude-code-cache-fix/proxy/server`:
173
+
174
+ ```js
175
+ import { startProxy } from "claude-code-cache-fix/proxy/server";
176
+
177
+ const handle = await startProxy({
178
+ port: 0, // OS-assigned ephemeral port; pass a number to pin
179
+ bind: "127.0.0.1",
180
+ watch: false, // skip fs.watch — recommended for compiled binaries
181
+ });
182
+
183
+ console.log(`proxy listening on ${handle.address}:${handle.port}`);
184
+
185
+ // ...later...
186
+ await handle.close();
187
+ ```
188
+
189
+ **`createProxyServer()` → `http.Server`** builds the request handler wired into an `http.Server`. The returned server is *not* listening and the extension pipeline has not been loaded — use this when you want to manage the lifecycle yourself.
190
+
191
+ **`startProxy(options?)` → `Promise<{ server, port, address, close }>`** loads the extension pipeline, optionally starts the file watcher, and starts listening. Returns a handle with the bound port (resolved when `port: 0` is requested) and a `close()` that releases the server and the watcher.
192
+
193
+ Options (all optional; all fall back to the same env vars used by the CLI):
194
+
195
+ | Option | Default | Effect |
196
+ |--------|---------|--------|
197
+ | `port` | `CACHE_FIX_PROXY_PORT` env, else `9801` | Listen port. Pass `0` for an OS-assigned ephemeral port. |
198
+ | `bind` | `CACHE_FIX_PROXY_BIND` env, else `127.0.0.1` | Bind address. |
199
+ | `extensionsDir` | package `proxy/extensions/` | Directory to load `.mjs` extensions from. |
200
+ | `extensionsConfig` | package `proxy/extensions.json` | Path to extension config. |
201
+ | `watch` | `true` | Whether to start `fs.watch` on the extensions config. Set `false` for embedded / compiled-binary use. |
202
+
203
+ **One extension registry per process.** The pipeline maintains a single shared extension registry at module scope. Hosting two `startProxy()` instances in the same process is supported (different ports, different bind addresses), but they share that registry — a subsequent `loadExtensions` call replaces it for both. If you need divergent extension configs per instance, run them in separate processes.
204
+
205
+ **CLI invocation is unchanged.** `node proxy/server.mjs`, `cache-fix-proxy server`, and the wrapper's child-fork path all auto-listen and install SIGTERM/SIGINT handlers as before. Library imports never trigger that behavior — the auto-listen is gated behind a main-module check.
206
+
170
207
  ## Quick Start: Preload (CC v2.1.112 and earlier)
171
208
 
172
209
  If you're on a Node.js-based CC version (v2.1.112 or earlier), the preload interceptor works without a proxy:
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "claude-code-cache-fix",
3
- "version": "3.5.4",
3
+ "version": "3.6.0",
4
4
  "description": "Cache optimization proxy and interceptor for Claude Code. Fixes prompt cache bugs, stabilizes prefix, reduces quota burn.",
5
5
  "type": "module",
6
- "exports": "./preload.mjs",
6
+ "exports": {
7
+ ".": "./preload.mjs",
8
+ "./proxy/server": "./proxy/server.mjs"
9
+ },
7
10
  "main": "./preload.mjs",
8
11
  "bin": {
9
12
  "cache-fix-proxy": "./bin/claude-via-proxy.mjs"
@@ -71,11 +71,15 @@ function parseHeaders(headers) {
71
71
  const overage_status = get("anthropic-ratelimit-unified-overage-status");
72
72
  const overage_util = num("anthropic-ratelimit-unified-overage-utilization");
73
73
  const overage_reset = parseInt(get("anthropic-ratelimit-unified-overage-reset")) || 0;
74
+ const unified_reset = parseInt(get("anthropic-ratelimit-unified-reset")) || 0;
74
75
  const fallback_pct = get("anthropic-ratelimit-unified-fallback-percentage");
75
76
  const representative = get("anthropic-ratelimit-unified-representative-claim");
76
77
  const surpassed = get("anthropic-ratelimit-unified-7d-surpassed-threshold");
77
78
 
78
- if (!q5h_reset && !q7d_reset) return null;
79
+ // Accept any reset timestamp — accounts without 5h/7d quota windows (overage
80
+ // billing) return anthropic-ratelimit-unified-reset and/or
81
+ // anthropic-ratelimit-unified-overage-reset instead.
82
+ if (!q5h_reset && !q7d_reset && !unified_reset && !overage_reset) return null;
79
83
 
80
84
  const now = new Date();
81
85
  const hour = now.getUTCHours();
package/proxy/server.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import http from "node:http";
2
+ import { pathToFileURL } from "node:url";
2
3
  import config from "./config.mjs";
3
4
  import { forwardRequest } from "./upstream.mjs";
4
5
  import { streamResponse, createTelemetryRecord } from "./stream.mjs";
@@ -138,36 +139,106 @@ function handleNotFound(_req, res) {
138
139
  res.end(JSON.stringify({ error: "not_found" }));
139
140
  }
140
141
 
141
- const server = http.createServer((req, res) => {
142
- if (req.method === "GET" && req.url === "/health") {
143
- return handleHealth(req, res);
144
- }
145
- if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
146
- return handleMessages(req, res);
147
- }
148
- handleNotFound(req, res);
149
- });
150
-
151
- function shutdown() {
152
- server.close(() => process.exit(0));
153
- setTimeout(() => process.exit(1), 5000);
142
+ /**
143
+ * Builds an http.Server with the proxy's request handler wired in. The
144
+ * returned server is **not** listening and the extension pipeline has not
145
+ * been initialized — callers wanting a one-call setup should use
146
+ * `startProxy()` instead.
147
+ *
148
+ * Exposed so callers can embed the proxy in their own process (e.g.
149
+ * Bun-compiled binaries, test harnesses) without forking a child or
150
+ * shelling out to the `cache-fix-proxy` bin.
151
+ */
152
+ export function createProxyServer() {
153
+ return http.createServer((req, res) => {
154
+ if (req.method === "GET" && req.url === "/health") {
155
+ return handleHealth(req, res);
156
+ }
157
+ if (req.method === "POST" && req.url?.startsWith("/v1/messages")) {
158
+ return handleMessages(req, res);
159
+ }
160
+ handleNotFound(req, res);
161
+ });
154
162
  }
155
163
 
156
- process.on("SIGTERM", shutdown);
157
- process.on("SIGINT", shutdown);
158
-
159
- async function initPipeline() {
164
+ /**
165
+ * Builds the server, loads the extension pipeline, optionally starts the
166
+ * extensions-config file watcher, and starts listening. Returns a handle
167
+ * with the bound port (resolved when port 0 is requested) and a `close`
168
+ * function for graceful shutdown.
169
+ *
170
+ * All options fall back to the same env vars / defaults used by the CLI
171
+ * entrypoint, so existing deployments behave identically.
172
+ *
173
+ * await startProxy() // env-driven, CLI parity
174
+ * await startProxy({ port: 0 }) // OS-assigned port
175
+ * await startProxy({ port: 0, watch: false }) // embedded, no fs.watch
176
+ */
177
+ export async function startProxy(options = {}) {
178
+ const port = options.port ?? config.port;
179
+ const bind = options.bind ?? config.bind;
180
+ const extensionsDir = options.extensionsDir ?? config.extensionsDir;
181
+ const extensionsConfig = options.extensionsConfig ?? config.extensionsConfig;
182
+ const watch = options.watch !== false;
183
+
184
+ let watcher = null;
160
185
  try {
161
- await loadExtensions(config.extensionsDir, config.extensionsConfig);
162
- startWatcher(config.extensionsDir, config.extensionsConfig);
186
+ await loadExtensions(extensionsDir, extensionsConfig);
187
+ if (watch) watcher = startWatcher(extensionsDir, extensionsConfig);
163
188
  } catch {}
164
- }
165
189
 
166
- initPipeline().then(() => {
167
- server.listen(config.port, config.bind, () => {
168
- const addr = server.address();
169
- process.stdout.write(`proxy listening on ${addr.address}:${addr.port}\n`);
190
+ const server = createProxyServer();
191
+ await new Promise((resolve, reject) => {
192
+ server.once("error", reject);
193
+ server.listen(port, bind, () => {
194
+ server.off("error", reject);
195
+ resolve();
196
+ });
170
197
  });
171
- });
172
198
 
173
- export { server };
199
+ const addr = server.address();
200
+ return {
201
+ server,
202
+ port: addr.port,
203
+ address: addr.address,
204
+ close: () =>
205
+ new Promise((resolve, reject) => {
206
+ try {
207
+ if (watcher) watcher.close();
208
+ } catch {}
209
+ server.close((err) => (err ? reject(err) : resolve()));
210
+ }),
211
+ };
212
+ }
213
+
214
+ // CLI entrypoint — preserves the v3.x behavior of `node proxy/server.mjs`
215
+ // (used by `cache-fix-proxy server` and by `fork(SERVER_PATH)` in the
216
+ // wrapper). When this module is imported as a library, none of this runs.
217
+ const invokedAsScript =
218
+ typeof process !== "undefined" &&
219
+ process.argv[1] &&
220
+ import.meta.url === pathToFileURL(process.argv[1]).href;
221
+
222
+ if (invokedAsScript) {
223
+ let active;
224
+ startProxy()
225
+ .then((handle) => {
226
+ active = handle;
227
+ process.stdout.write(`proxy listening on ${handle.address}:${handle.port}\n`);
228
+ })
229
+ .catch((err) => {
230
+ process.stderr.write(`proxy failed to start: ${err.message}\n`);
231
+ process.exit(1);
232
+ });
233
+
234
+ const shutdown = () => {
235
+ if (!active) {
236
+ process.exit(0);
237
+ return;
238
+ }
239
+ active.close().finally(() => process.exit(0));
240
+ setTimeout(() => process.exit(1), 5000).unref();
241
+ };
242
+ process.on("SIGTERM", shutdown);
243
+ process.on("SIGINT", shutdown);
244
+ }