claude-code-cache-fix 3.5.5 → 3.6.1
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 +73 -2
- package/package.json +5 -2
- package/proxy/extensions/thinking-display.mjs +79 -0
- package/proxy/extensions.json +1 -0
- package/proxy/server.mjs +97 -26
package/README.md
CHANGED
|
@@ -43,6 +43,8 @@ On every `/v1/messages` request, 7 extensions run in order:
|
|
|
43
43
|
|
|
44
44
|
Extensions are hot-reloadable — add, remove, or modify `.mjs` files in `proxy/extensions/` and changes apply to the next request without restarting. Configuration in `proxy/extensions.json`.
|
|
45
45
|
|
|
46
|
+
**Developing a new extension?** See [docs/parallel-proxy-test-harness.md](docs/parallel-proxy-test-harness.md) for the pattern we use to test extensions end-to-end against real `claude -p` traffic without disturbing the production proxy.
|
|
47
|
+
|
|
46
48
|
### Running as a service
|
|
47
49
|
|
|
48
50
|
**Recommended (Linux/macOS) — `install-service` subcommand:**
|
|
@@ -167,6 +169,45 @@ node "$(npm root -g)\claude-code-cache-fix\proxy\server.mjs"
|
|
|
167
169
|
|
|
168
170
|
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
171
|
|
|
172
|
+
### Embedding the proxy in your own process
|
|
173
|
+
|
|
174
|
+
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`:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import { startProxy } from "claude-code-cache-fix/proxy/server";
|
|
178
|
+
|
|
179
|
+
const handle = await startProxy({
|
|
180
|
+
port: 0, // OS-assigned ephemeral port; pass a number to pin
|
|
181
|
+
bind: "127.0.0.1",
|
|
182
|
+
watch: false, // skip fs.watch — recommended for compiled binaries
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
console.log(`proxy listening on ${handle.address}:${handle.port}`);
|
|
186
|
+
|
|
187
|
+
// ...later...
|
|
188
|
+
await handle.close();
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**`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.
|
|
192
|
+
|
|
193
|
+
**`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.
|
|
194
|
+
|
|
195
|
+
Options (all optional; all fall back to the same env vars used by the CLI):
|
|
196
|
+
|
|
197
|
+
| Option | Default | Effect |
|
|
198
|
+
|--------|---------|--------|
|
|
199
|
+
| `port` | `CACHE_FIX_PROXY_PORT` env, else `9801` | Listen port. Pass `0` for an OS-assigned ephemeral port. |
|
|
200
|
+
| `bind` | `CACHE_FIX_PROXY_BIND` env, else `127.0.0.1` | Bind address. |
|
|
201
|
+
| `extensionsDir` | package `proxy/extensions/` | Directory to load `.mjs` extensions from. |
|
|
202
|
+
| `extensionsConfig` | package `proxy/extensions.json` | Path to extension config. |
|
|
203
|
+
| `watch` | `true` | Whether to start `fs.watch` on the extensions config. Set `false` for embedded / compiled-binary use. |
|
|
204
|
+
|
|
205
|
+
**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.
|
|
206
|
+
|
|
207
|
+
**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.
|
|
208
|
+
|
|
209
|
+
*The embeddable factory was contributed by [@bilby91](https://github.com/bilby91) at [Crunchloop DAP](https://dap.crunchloop.ai) — see [PR #123](https://github.com/cnighswonger/claude-code-cache-fix/pull/123).*
|
|
210
|
+
|
|
170
211
|
## Quick Start: Preload (CC v2.1.112 and earlier)
|
|
171
212
|
|
|
172
213
|
If you're on a Node.js-based CC version (v2.1.112 or earlier), the preload interceptor works without a proxy:
|
|
@@ -576,6 +617,35 @@ The Mode A/B separation protects against cases where the sentinel might be follo
|
|
|
576
617
|
| `CACHE_FIX_MICROCOMPACT_REDACT_LEN` | `64` | Mode B prefix length in dump records. Set to `0` to suppress the prefix entirely. |
|
|
577
618
|
| `CACHE_FIX_DUMP_MICROCOMPACT_INCLUDE_NORMALIZED` | unset | Add post-normalization text alongside (not replacing) raw `sentinel_text` in dump records. |
|
|
578
619
|
|
|
620
|
+
## Thinking summaries (proxy mode, opt-in, Opus 4.7+)
|
|
621
|
+
|
|
622
|
+
On Opus 4.7, Anthropic flipped the API default for `thinking.display` from `"summarized"` to `"omitted"`. In parallel, Claude Code's CLI has a `!getIsNonInteractiveSession()` gate that propagates `display: "summarized"` only when the session is interactive. The combination means every CC subprocess spawned with `--input-format stream-json` — the VS Code chat panel, the Antigravity panel, the SDK, `claude --print` — sends a thinking-enabled request (`thinking.type` is either `"enabled"` or `"adaptive"` depending on CC version) without `display`, and the API responds with thinking blocks whose `thinking` field is empty (plus a multi-KB signature). The UI shows a static "Thinking" stub while the agent runs but never any reasoning content.
|
|
623
|
+
|
|
624
|
+
Upstream root cause and patch proposed in [anthropics/claude-code#59844](https://github.com/anthropics/claude-code/issues/59844) (credit: [@ojura](https://github.com/ojura)). This extension is the proxy-side complement: when a request to an Opus 4.7 endpoint has thinking enabled but `display` unset, inject the configured mode at the API boundary. Works on any CC version routed through cache-fix-proxy, no waiting on Anthropic to ship the CLI fix.
|
|
625
|
+
|
|
626
|
+
```sh
|
|
627
|
+
# Restore summaries (the built-in default — non-interactive surfaces get reasoning content)
|
|
628
|
+
export CACHE_FIX_THINKING_DISPLAY=summarized
|
|
629
|
+
|
|
630
|
+
# Force-suppress override (agent runtimes that don't want thinking blocks at all)
|
|
631
|
+
export CACHE_FIX_THINKING_DISPLAY=omitted
|
|
632
|
+
|
|
633
|
+
# Explicit no-op (extension passes through unchanged)
|
|
634
|
+
export CACHE_FIX_THINKING_DISPLAY=disabled
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
The extension is **default-on** as of v3.6.1. The cache-prefix test measured 0% absolute drop in steady-state `cache_read` ratio when injection is active on Opus 4.7 (5 sequential `claude -p` calls per window, baseline vs injected — both windows held 1.000 cache_read ratio from call 2 onward). Adding `thinking.display` to the request body changes the bytes Anthropic hashes, but Anthropic's cache layer accepts and indexes the injected-prefix the same way it does any other prefix. Users who want the older "no injection" behavior (e.g. to avoid any request-body mutation at all) explicitly set `CACHE_FIX_THINKING_DISPLAY=disabled`.
|
|
638
|
+
|
|
639
|
+
Scoping rules baked into the extension:
|
|
640
|
+
|
|
641
|
+
- **Model-gated.** Only fires on requests whose `model` matches `/^claude-opus-4-7/` — covers `claude-opus-4-7` and `claude-opus-4-7-1m`. Sonnet 4.7 needs separate verification (the API default-flip may differ); future versions (4.8+) require an explicit cache-fix bump rather than auto-applying unverified behavior.
|
|
642
|
+
- **User opt-out preserved.** If the request already has `thinking.display` set (either `"summarized"` or `"omitted"`), the extension never overwrites. Explicit user choice always wins.
|
|
643
|
+
- **Thinking-active types only.** The extension fires on `thinking.type` ∈ `{ "enabled", "adaptive" }` — the two active modes that produce thinking blocks on Opus 4.7. Other values (`"disabled"`, future modes) are skipped. Conservative: if Anthropic ships a new thinking type with different display semantics, we'd rather miss the fix than auto-apply incorrect behavior.
|
|
644
|
+
|
|
645
|
+
| Env var | Default | Purpose |
|
|
646
|
+
|---------|---------|---------|
|
|
647
|
+
| `CACHE_FIX_THINKING_DISPLAY` | `summarized` (built-in) | One of `summarized` / `omitted` / `disabled`. `summarized` restores thinking summaries (default). `omitted` force-suppresses thinking blocks. `disabled` opts the extension out entirely. |
|
|
648
|
+
|
|
579
649
|
## System prompt rewrite (preload mode, optional)
|
|
580
650
|
|
|
581
651
|
The interceptor can rewrite Claude Code's `# Output efficiency` system-prompt section. Disabled by default. Enable with `CACHE_FIX_OUTPUT_EFFICIENCY_REPLACEMENT`. See [docs/output-efficiency-prompts.md](docs/output-efficiency-prompts.md) for the three known prompt variants and usage instructions.
|
|
@@ -606,13 +676,13 @@ We monitor 30+ upstream Claude Code issues related to cache, quota, and context
|
|
|
606
676
|
|
|
607
677
|
## Used in production
|
|
608
678
|
|
|
609
|
-
- **[Crunchloop DAP](https://dap.crunchloop.ai)** — Agent SDK / DAP development environment. First production team to merge the interceptor to trunk for team-wide deployment (2026-04-10). Identified two distinct cache regression patterns through real-world testing — tool ordering jitter and the fresh-session sort gap — and contributed debug traces that drove the v1.5.1 and v1.6.2 fixes.
|
|
679
|
+
- **[Crunchloop DAP](https://dap.crunchloop.ai)** — Agent SDK / DAP development environment. First production team to merge the interceptor to trunk for team-wide deployment (2026-04-10). Identified two distinct cache regression patterns through real-world testing — tool ordering jitter and the fresh-session sort gap — and contributed debug traces that drove the v1.5.1 and v1.6.2 fixes. Contributed the embeddable proxy factory (v3.6.0) that lets the proxy run in-process inside Bun-compiled and DAP-style agent binaries without forking a Node child.
|
|
610
680
|
- **[VM Farms](https://vmfarms.com)** ([@vmfarms](https://github.com/vmfarms)) — Agent development environment running concurrent multi-runner workloads with `--resume --fork-session`. Surfaced three cache-fix proxy-mode bugs: the resume-marker regex no-op (#96), TTL tier detection gap vs preload mode (#97), and image-strip stderr leak past `CACHE_FIX_DEBUG` (#98) — all addressed in the v3.4.0 release.
|
|
611
681
|
|
|
612
682
|
## Contributors
|
|
613
683
|
|
|
614
684
|
- **[@VictorSun92](https://github.com/VictorSun92)** — Original monkey-patch fix for v2.1.88, identified partial scatter on v2.1.90, contributed forward-scan detection, correct block ordering, tighter block matchers, and the optional output-efficiency rewrite hook
|
|
615
|
-
- **[@bilby91](https://github.com/bilby91)** ([Crunchloop DAP](https://dap.crunchloop.ai)) — Agent SDK / DAP production environment validation, 1h cache TTL confirmation, tool ordering jitter discovery via debug trace (fixed in v1.5.1), fresh-session sort bug discovery via SKILLS SORT diagnostic (fixed in v1.6.2). First production team to roll the interceptor to trunk.
|
|
685
|
+
- **[@bilby91](https://github.com/bilby91)** ([Crunchloop DAP](https://dap.crunchloop.ai)) — Agent SDK / DAP production environment validation, 1h cache TTL confirmation, tool ordering jitter discovery via debug trace (fixed in v1.5.1), fresh-session sort bug discovery via SKILLS SORT diagnostic (fixed in v1.6.2). First production team to roll the interceptor to trunk. Designed and contributed the embeddable proxy factory (`startProxy()` / `createProxyServer()`) shipped in v3.6.0 (PR #123).
|
|
616
686
|
- **[@jmarianski](https://github.com/jmarianski)** — Root cause analysis via MITM proxy capture and Ghidra reverse engineering, multi-mode cache test script
|
|
617
687
|
- **[@cnighswonger](https://github.com/cnighswonger)** — Fingerprint stabilization, tool ordering fix, image stripping, monitoring features, overage TTL downgrade discovery, proxy architecture, package maintainer
|
|
618
688
|
- **[@ArkNill](https://github.com/ArkNill)** — Microcompact mechanism analysis, GrowthBook flag documentation, false rate limiter identification, fingerprint verification fix for CC v2.1.108+ (PR #21), Korean README (PR #22), [claude-code-hidden-problem-analysis](https://github.com/ArkNill/claude-code-hidden-problem-analysis) research
|
|
@@ -625,6 +695,7 @@ We monitor 30+ upstream Claude Code issues related to cache, quota, and context
|
|
|
625
695
|
- **[@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)
|
|
626
696
|
- **[@deafsquad](https://github.com/deafsquad)** — Universal smoosh_split un-smoosh fix (PR #26), source-level function attribution of resume scatter bug (anthropics/claude-code#43657), OTEL telemetry discovery, proposed and built proxy architecture for v3.0.0
|
|
627
697
|
- **[@vmfarms](https://github.com/vmfarms)** — Concurrent multi-runner production validation, surfaced proxy-mode resume-marker regex no-op (#96), TTL tier detection gap (#97), and image-strip stderr leak (#98)
|
|
698
|
+
- **[@ojura](https://github.com/ojura)** — Opus 4.7 thinking-summaries root-cause analysis: filed [anthropics/claude-code#59844](https://github.com/anthropics/claude-code/issues/59844) with the CLI-binary decode (`!getIsNonInteractiveSession()` gate at offset 230510599 in v2.1.142) and the two-stacked-special-cases framing, which made the `thinking-display` extension (v3.6.1) a clean proxy-side complement to the proposed upstream fix
|
|
628
699
|
|
|
629
700
|
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.
|
|
630
701
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-cache-fix",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
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":
|
|
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"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// thinking-display extension
|
|
2
|
+
//
|
|
3
|
+
// Restores Opus 4.7 thinking summaries in non-interactive CC surfaces
|
|
4
|
+
// (VS Code chat panel, SDK, `claude --print`, anything spawned with
|
|
5
|
+
// `--input-format stream-json`).
|
|
6
|
+
//
|
|
7
|
+
// Background: Anthropic flipped the `thinking.display` default to `"omitted"`
|
|
8
|
+
// on Opus 4.7. CC's CLI propagates `display: "summarized"` only when the
|
|
9
|
+
// session is interactive (the `!getIsNonInteractiveSession()` gate); every
|
|
10
|
+
// non-interactive subprocess gets the API default of omitted thinking, which
|
|
11
|
+
// renders as an empty stub in the IDE. Upstream root cause and patch proposed
|
|
12
|
+
// in anthropics/claude-code#59844 (credit: @ojura).
|
|
13
|
+
//
|
|
14
|
+
// This extension is the proxy-side workaround. When a request has thinking
|
|
15
|
+
// enabled but `display` unset, inject the configured mode at the API
|
|
16
|
+
// boundary. Works on any CC version routed through cache-fix-proxy without
|
|
17
|
+
// waiting for Anthropic to ship the CLI fix.
|
|
18
|
+
//
|
|
19
|
+
// Config (env var; built-in default is "summarized"):
|
|
20
|
+
// CACHE_FIX_THINKING_DISPLAY=summarized — inject display: "summarized"
|
|
21
|
+
// (main case; restores summaries in IDE/SDK/--print). DEFAULT.
|
|
22
|
+
// CACHE_FIX_THINKING_DISPLAY=omitted — inject display: "omitted"
|
|
23
|
+
// (force-suppress override; for agent runtimes that don't want thinking
|
|
24
|
+
// blocks at all, regardless of what their CLI sends)
|
|
25
|
+
// CACHE_FIX_THINKING_DISPLAY=disabled — no injection; extension is a no-op
|
|
26
|
+
//
|
|
27
|
+
// Default flipped to "summarized" in v3.6.1 after the cache-prefix test on
|
|
28
|
+
// Opus 4.7 measured 0% absolute drop in steady-state cache_read ratio with
|
|
29
|
+
// injection enabled (well inside the ≤5% "preserved" threshold). Users who
|
|
30
|
+
// want the older "no injection" behavior set CACHE_FIX_THINKING_DISPLAY=disabled.
|
|
31
|
+
|
|
32
|
+
const MODEL_REGEX = /^claude-opus-4-7/;
|
|
33
|
+
|
|
34
|
+
function resolveMode() {
|
|
35
|
+
const v = process.env.CACHE_FIX_THINKING_DISPLAY;
|
|
36
|
+
if (v === "summarized" || v === "omitted" || v === "disabled") return v;
|
|
37
|
+
return "summarized";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Thinking types that produce thinking blocks. CC v2.1.131+ ships
|
|
41
|
+
// `type: "adaptive"` (dynamic-budget mode) by default for the Bun binary's
|
|
42
|
+
// non-interactive paths; older versions and explicit-budget configs may
|
|
43
|
+
// still send `"enabled"`. Both produce the same empty-thinking symptom
|
|
44
|
+
// when `display` is unset on Opus 4.7, so both are in scope.
|
|
45
|
+
const ACTIVE_THINKING_TYPES = new Set(["enabled", "adaptive"]);
|
|
46
|
+
|
|
47
|
+
function shouldInject(body) {
|
|
48
|
+
if (!body || typeof body !== "object") return false;
|
|
49
|
+
if (typeof body.model !== "string") return false;
|
|
50
|
+
if (!MODEL_REGEX.test(body.model)) return false;
|
|
51
|
+
if (!body.thinking || typeof body.thinking !== "object") return false;
|
|
52
|
+
if (!ACTIVE_THINKING_TYPES.has(body.thinking.type)) return false;
|
|
53
|
+
// Only inject when display is unset. Preserve any explicit user choice
|
|
54
|
+
// (including explicit "omitted" for compliance opt-out).
|
|
55
|
+
return body.thinking.display === undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { MODEL_REGEX, ACTIVE_THINKING_TYPES, resolveMode, shouldInject };
|
|
59
|
+
|
|
60
|
+
export default {
|
|
61
|
+
name: "thinking-display",
|
|
62
|
+
description:
|
|
63
|
+
"Inject thinking.display on Opus 4.7 requests when unset, to restore " +
|
|
64
|
+
"thinking summaries lost to CC's non-interactive CLI gate (claude-code#59844)",
|
|
65
|
+
enabled: false,
|
|
66
|
+
order: 360,
|
|
67
|
+
|
|
68
|
+
async onRequest(ctx) {
|
|
69
|
+
const mode = resolveMode();
|
|
70
|
+
if (mode === "disabled") return;
|
|
71
|
+
if (!shouldInject(ctx.body)) return;
|
|
72
|
+
|
|
73
|
+
ctx.body.thinking.display = mode;
|
|
74
|
+
|
|
75
|
+
if (ctx.meta) {
|
|
76
|
+
ctx.meta.thinkingDisplayInjected = mode;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
package/proxy/extensions.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"content-strip": { "enabled": true, "order": 330 },
|
|
10
10
|
"tool-input-normalize": { "enabled": true, "order": 340 },
|
|
11
11
|
"microcompact-stability": { "enabled": true, "order": 350 },
|
|
12
|
+
"thinking-display": { "enabled": true, "order": 360 },
|
|
12
13
|
"cache-control-normalize": { "enabled": true, "order": 400 },
|
|
13
14
|
"messages-cache-breakpoint": { "enabled": true, "order": 410 },
|
|
14
15
|
"ttl-management": { "enabled": true, "order": 500 },
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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(
|
|
162
|
-
startWatcher(
|
|
186
|
+
await loadExtensions(extensionsDir, extensionsConfig);
|
|
187
|
+
if (watch) watcher = startWatcher(extensionsDir, extensionsConfig);
|
|
163
188
|
} catch {}
|
|
164
|
-
}
|
|
165
189
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
+
}
|