@openhoo/hoopilot 0.9.3 → 1.0.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 +233 -128
- package/dist/{chunk-7GSQVYYT.js → chunk-JU6F5L34.js} +9 -5
- package/dist/chunk-JU6F5L34.js.map +1 -0
- package/dist/cli.js +93 -26
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/index.cjs +57 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +57 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-7GSQVYYT.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,34 +1,81 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hoopilot
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@openhoo/hoopilot)
|
|
4
4
|
[](https://github.com/openhoo/hoopilot/actions/workflows/ci.yml)
|
|
5
5
|
|
|
6
|
-
OpenAI- and Anthropic-compatible
|
|
6
|
+
Hoopilot is a local OpenAI- and Anthropic-compatible proxy for GitHub Copilot accounts. It runs on Bun and exposes OpenAI-style `/v1/chat/completions`, `/v1/responses`, `/v1/completions`, and `/v1/models` routes, plus Claude Code-compatible `/v1/messages` and `/v1/messages/count_tokens` routes.
|
|
7
7
|
|
|
8
|
-
This project uses GitHub Copilot
|
|
8
|
+
This project uses GitHub Copilot service endpoints and is not an official GitHub product. Upstream behavior can change without notice. Use Hoopilot only with accounts and usage patterns you are allowed to use.
|
|
9
|
+
|
|
10
|
+
## Highlights
|
|
11
|
+
|
|
12
|
+
- Browser-based GitHub Copilot OAuth login with a local credential store.
|
|
13
|
+
- OpenAI-compatible Chat Completions, Responses, legacy Completions, and model-list routes.
|
|
14
|
+
- Anthropic Messages compatibility for Claude Code and other Anthropic-style clients.
|
|
15
|
+
- Bundled `codexx` launcher that runs Codex against a local Hoopilot server with the right Responses API provider settings.
|
|
16
|
+
- Local API-key gate, loopback-safe defaults, structured logs, Prometheus metrics, and Copilot quota reporting.
|
|
17
|
+
- npm package, standalone binaries, Docker image, and self-update support for release binaries.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
Sign in once, then start the proxy on localhost:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx @openhoo/hoopilot login
|
|
25
|
+
npx @openhoo/hoopilot
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
By default the server listens on `127.0.0.1:4141` and accepts local requests without authentication, so any placeholder works as the client key:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
export OPENAI_BASE_URL=http://127.0.0.1:4141/v1
|
|
32
|
+
export OPENAI_API_KEY=hoopilot
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
PowerShell:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
$env:OPENAI_BASE_URL = "http://127.0.0.1:4141/v1"
|
|
39
|
+
$env:OPENAI_API_KEY = "hoopilot"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
To require clients to authenticate — recommended whenever you expose the proxy beyond localhost — set `HOOPILOT_API_KEY` to a strong, unique secret and send that value as the client key:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
export HOOPILOT_API_KEY=$(openssl rand -hex 24)
|
|
46
|
+
npx @openhoo/hoopilot
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Run Codex through Hoopilot after the server is running:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npx --package @openhoo/hoopilot codexx
|
|
53
|
+
```
|
|
9
54
|
|
|
10
55
|
## Install
|
|
11
56
|
|
|
12
57
|
### npm
|
|
13
58
|
|
|
14
|
-
|
|
59
|
+
Run without installing:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
15
62
|
npx @openhoo/hoopilot
|
|
16
63
|
```
|
|
17
64
|
|
|
18
|
-
Or install
|
|
65
|
+
Or install the package globally:
|
|
19
66
|
|
|
20
|
-
```
|
|
67
|
+
```sh
|
|
21
68
|
npm install -g @openhoo/hoopilot
|
|
22
69
|
bun add -g @openhoo/hoopilot
|
|
23
70
|
```
|
|
24
71
|
|
|
25
|
-
### Standalone
|
|
72
|
+
### Standalone binary
|
|
26
73
|
|
|
27
|
-
When
|
|
74
|
+
When npm is unavailable but GitHub releases are reachable, install a prebuilt self-contained binary. Node.js and Bun are not required to run the binary.
|
|
28
75
|
|
|
29
|
-
Linux
|
|
76
|
+
Linux/macOS:
|
|
30
77
|
|
|
31
|
-
```
|
|
78
|
+
```sh
|
|
32
79
|
curl -fsSL https://raw.githubusercontent.com/openhoo/hoopilot/main/scripts/install.sh | sh
|
|
33
80
|
```
|
|
34
81
|
|
|
@@ -38,9 +85,9 @@ Windows PowerShell:
|
|
|
38
85
|
irm https://raw.githubusercontent.com/openhoo/hoopilot/main/scripts/install.ps1 | iex
|
|
39
86
|
```
|
|
40
87
|
|
|
41
|
-
The installer detects your OS, CPU architecture, and libc, downloads the matching binary, verifies its SHA-256 checksum, and installs it to `~/.local/bin` on Linux/macOS or `%LOCALAPPDATA%\Programs\hoopilot` on Windows. Override the
|
|
88
|
+
The installer detects your OS, CPU architecture, and libc, downloads the matching binary, verifies its SHA-256 checksum, and installs it to `~/.local/bin` on Linux/macOS or `%LOCALAPPDATA%\Programs\hoopilot` on Windows. Override the install directory with `HOOPILOT_INSTALL_DIR`, or pin a version:
|
|
42
89
|
|
|
43
|
-
```
|
|
90
|
+
```sh
|
|
44
91
|
curl -fsSL https://raw.githubusercontent.com/openhoo/hoopilot/main/scripts/install.sh | sh -s -- --version <version> --dir ~/bin
|
|
45
92
|
```
|
|
46
93
|
|
|
@@ -58,16 +105,23 @@ Run Hoopilot as a long-lived service from the published multi-arch image on the
|
|
|
58
105
|
# 1. Sign in once; the OAuth credential is written to the persisted /data volume.
|
|
59
106
|
docker run --rm -it -v hoopilot-data:/data ghcr.io/openhoo/hoopilot login
|
|
60
107
|
|
|
61
|
-
# 2. Run the proxy on localhost.
|
|
108
|
+
# 2. Run the proxy on localhost with a strong, unique API key.
|
|
109
|
+
export HOOPILOT_API_KEY=$(openssl rand -hex 24)
|
|
62
110
|
docker run -d --name hoopilot --restart unless-stopped \
|
|
63
111
|
-p 127.0.0.1:4141:4141 \
|
|
112
|
+
-e HOOPILOT_API_KEY \
|
|
64
113
|
-v hoopilot-data:/data ghcr.io/openhoo/hoopilot
|
|
65
114
|
```
|
|
66
115
|
|
|
67
|
-
Tags follow the release version
|
|
116
|
+
Tags follow the release version, for example `ghcr.io/openhoo/hoopilot:0.10`, `:0.10.0`, and `:latest`. The image listens on `0.0.0.0:4141` (required so Docker port publishing can reach it), runs as a non-root user, and stores its OAuth credential at `/data/auth.json` by default. Override that path with `HOOPILOT_AUTH_FILE`.
|
|
117
|
+
|
|
118
|
+
Because it binds a non-loopback interface, the image fails closed: it refuses to start unless you set `HOOPILOT_API_KEY` to a strong, unique secret (well-known demo keys are rejected). Clients then send that key as `Authorization: Bearer <key>` or `x-api-key: <key>`. To intentionally run without authentication — for example behind your own authenticating proxy — set `HOOPILOT_ALLOW_UNAUTHENTICATED=1`.
|
|
119
|
+
|
|
120
|
+
A `docker-compose.yml` is provided. Set `HOOPILOT_API_KEY` first; compose passes it through to the container:
|
|
68
121
|
|
|
69
122
|
```sh
|
|
70
|
-
docker compose run --rm hoopilot login
|
|
123
|
+
docker compose run --rm hoopilot login
|
|
124
|
+
export HOOPILOT_API_KEY=$(openssl rand -hex 24)
|
|
71
125
|
docker compose up -d
|
|
72
126
|
```
|
|
73
127
|
|
|
@@ -75,189 +129,231 @@ docker compose up -d
|
|
|
75
129
|
|
|
76
130
|
Standalone binaries update themselves in place from the latest GitHub release:
|
|
77
131
|
|
|
78
|
-
```
|
|
132
|
+
```sh
|
|
79
133
|
hoopilot update
|
|
80
134
|
```
|
|
81
135
|
|
|
82
136
|
npm installs report when a newer version is available and print the right command. Hoopilot checks GitHub at most once a day in the background. Disable the check with `--no-update-check`, `HOOPILOT_NO_UPDATE_CHECK`, or `NO_UPDATE_NOTIFIER`.
|
|
83
137
|
|
|
84
|
-
##
|
|
138
|
+
## Running the proxy
|
|
85
139
|
|
|
86
|
-
|
|
140
|
+
Login uses GitHub's browser/device flow, verifies that the returned OAuth token can reach the Copilot API, and stores it locally:
|
|
87
141
|
|
|
88
|
-
```
|
|
89
|
-
|
|
142
|
+
```sh
|
|
143
|
+
hoopilot login
|
|
90
144
|
```
|
|
91
145
|
|
|
92
|
-
|
|
146
|
+
Default credential paths:
|
|
93
147
|
|
|
94
|
-
|
|
148
|
+
- Linux/macOS: `$HOME/.config/hoopilot/auth.json`
|
|
149
|
+
- Windows: `%APPDATA%\hoopilot\auth.json`
|
|
95
150
|
|
|
96
|
-
|
|
97
|
-
|
|
151
|
+
Override the path with `HOOPILOT_AUTH_FILE` or `--auth-file`.
|
|
152
|
+
|
|
153
|
+
Start the server:
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
hoopilot --port 4141
|
|
98
157
|
```
|
|
99
158
|
|
|
100
|
-
By default Hoopilot listens on `127.0.0.1:4141`
|
|
159
|
+
By default Hoopilot listens on `127.0.0.1:4141`. If `HOOPILOT_API_KEY` is unset, local requests are accepted without client authentication. Binding to a non-loopback host requires either a strong, unique `HOOPILOT_API_KEY` or the explicit `--allow-unauthenticated` / `HOOPILOT_ALLOW_UNAUTHENTICATED=1` opt-in. Well-known demo keys are always rejected on a non-loopback host, even with the unauthenticated opt-in.
|
|
101
160
|
|
|
102
|
-
|
|
103
|
-
- Windows: `$env:APPDATA\hoopilot\auth.json`
|
|
161
|
+
When an API key is configured, clients may send it as either `Authorization: Bearer <key>` or `x-api-key: <key>`.
|
|
104
162
|
|
|
105
|
-
|
|
163
|
+
Cross-origin browser requests are always blocked, even when an API key is set, so a malicious web page cannot drive the local proxy. Requests from loopback origins are allowed; to permit specific web origins, list them in `HOOPILOT_ALLOWED_ORIGINS` (comma-separated).
|
|
106
164
|
|
|
107
|
-
|
|
165
|
+
## Client setup
|
|
108
166
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
167
|
+
### OpenAI-compatible clients
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
export OPENAI_BASE_URL=http://127.0.0.1:4141/v1
|
|
171
|
+
export OPENAI_API_KEY=hoopilot
|
|
112
172
|
```
|
|
113
173
|
|
|
114
|
-
|
|
174
|
+
The client key value is arbitrary when the server runs without `HOOPILOT_API_KEY`; if you set one, use that value here instead.
|
|
115
175
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
176
|
+
Use any model returned by:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
hoopilot models
|
|
119
180
|
```
|
|
120
181
|
|
|
121
|
-
|
|
182
|
+
### Claude Code and Anthropic-style clients
|
|
122
183
|
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
|
|
184
|
+
```sh
|
|
185
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:4141
|
|
186
|
+
export ANTHROPIC_AUTH_TOKEN=hoopilot
|
|
126
187
|
claude
|
|
127
188
|
```
|
|
128
189
|
|
|
129
|
-
Hoopilot accepts the local key as
|
|
130
|
-
`x-api-key: <key>`, so `ANTHROPIC_API_KEY` also works for clients that send
|
|
131
|
-
Anthropic's `x-api-key` header.
|
|
190
|
+
Hoopilot accepts the local key as `x-api-key` too, so `ANTHROPIC_API_KEY` also works for clients that send Anthropic's standard API-key header.
|
|
132
191
|
|
|
133
|
-
|
|
192
|
+
### Codex
|
|
134
193
|
|
|
135
|
-
|
|
136
|
-
|
|
194
|
+
Use the bundled `codexx` command after Hoopilot is running:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
137
197
|
codexx
|
|
138
198
|
```
|
|
139
199
|
|
|
140
|
-
Without a global install
|
|
200
|
+
Without a global install:
|
|
141
201
|
|
|
142
|
-
```
|
|
143
|
-
$env:HOOPILOT_API_KEY = "local-key"
|
|
202
|
+
```sh
|
|
144
203
|
npx --package @openhoo/hoopilot codexx
|
|
145
204
|
```
|
|
146
205
|
|
|
147
|
-
|
|
148
|
-
`codex` with a temporary `hoopilot` model provider pointed at
|
|
149
|
-
`http://127.0.0.1:4141/v1`, disables Codex Responses WebSockets for that provider,
|
|
150
|
-
maps `HOOPILOT_API_KEY` to `OPENAI_API_KEY` for that child process, passes
|
|
151
|
-
`--disable network_proxy` to Codex, and removes standard proxy variables from the
|
|
152
|
-
spawned Codex process so Codex talks directly to the local server. Override the local
|
|
153
|
-
URL with `CODEXX_BASE_URL`, the local key with `CODEXX_API_KEY`, or the Codex
|
|
154
|
-
executable with `CODEXX_CODEX_BIN`, the model with `CODEXX_MODEL`, or the reasoning
|
|
155
|
-
effort with `CODEXX_MODEL_REASONING_EFFORT`.
|
|
156
|
-
|
|
157
|
-
`codexx` defaults to `gpt-5.5` with `model_reasoning_effort="xhigh"`. Codex sends
|
|
158
|
-
those requests through its Responses API provider, and Hoopilot forwards them to
|
|
159
|
-
Copilot's Responses endpoint because `gpt-5.5` is not available through Copilot's
|
|
160
|
-
chat-completions endpoint. Before starting Codex, `codexx` checks
|
|
161
|
-
`http://127.0.0.1:4141/v1/models` and reports if the logged-in Copilot account does
|
|
162
|
-
not advertise the requested model. Set `CODEXX_MODEL` to one of the listed models,
|
|
163
|
-
or log in with a Copilot account that has `gpt-5.5`.
|
|
164
|
-
|
|
165
|
-
When Codex compacts a long session it POSTs to `/v1/responses/compact` — a server-side
|
|
166
|
-
endpoint it expects from `OpenAI`- and Azure-named providers and for which it has no
|
|
167
|
-
local fallback, so an unhandled route would abort compaction. Hoopilot handles it by
|
|
168
|
-
running the supplied conversation through Copilot's Responses endpoint as a unary
|
|
169
|
-
request and returning the resulting `{ "output": [...] }` summary, so compaction works
|
|
170
|
-
whether Codex points at Hoopilot through `codexx` or through a plain `OPENAI_BASE_URL`
|
|
171
|
-
override of the built-in `openai` provider.
|
|
172
|
-
|
|
173
|
-
If no `HOOPILOT_API_KEY` is configured, Hoopilot accepts local requests without client authentication. Binding to a non-loopback host requires `HOOPILOT_API_KEY` unless `--allow-unauthenticated` is set.
|
|
206
|
+
If the server requires an API key, set `HOOPILOT_API_KEY` (or `CODEXX_API_KEY`) in the `codexx` environment to match.
|
|
174
207
|
|
|
175
|
-
|
|
208
|
+
`codexx` does not start Hoopilot and does not alter your shell environment. It starts `codex` with a temporary `hoopilot` model provider pointed at `http://127.0.0.1:4141/v1`, uses the Responses API wire format, disables Responses WebSockets for that provider, maps `HOOPILOT_API_KEY` (or a random throwaway key when none is set) to `OPENAI_API_KEY` for the child process, passes `--disable network_proxy`, and removes standard proxy variables from the spawned Codex process.
|
|
176
209
|
|
|
177
|
-
|
|
210
|
+
`codexx` defaults to `gpt-5.5` with `model_reasoning_effort="xhigh"`. Before starting Codex, it checks `/v1/models` and reports if the logged-in Copilot account does not advertise the requested model. Set `CODEXX_MODEL` to one of the listed models, or log in with a Copilot account that has access to the default model.
|
|
178
211
|
|
|
179
|
-
|
|
212
|
+
Codex compaction posts to `/v1/responses/compact` for OpenAI- and Azure-named providers. Hoopilot handles that route with a unary Copilot Responses request and returns the `{ "output": [...] }` summary Codex expects, so compaction works through either `codexx` or a direct OpenAI-compatible base URL override.
|
|
180
213
|
|
|
181
|
-
|
|
182
|
-
|
|
214
|
+
## Authentication
|
|
215
|
+
|
|
216
|
+
Hoopilot supports one upstream credential flow: GitHub Copilot OAuth browser login.
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
hoopilot login
|
|
220
|
+
hoopilot
|
|
183
221
|
```
|
|
184
222
|
|
|
185
|
-
|
|
223
|
+
Direct bearer tokens, GitHub CLI token fallback, classic GitHub PATs, and fine-grained GitHub PATs are not supported.
|
|
224
|
+
|
|
225
|
+
Re-run `hoopilot login` after upgrading Hoopilot if Copilot reports a supported model as unavailable. Older stored tokens can have a reduced model set.
|
|
226
|
+
|
|
227
|
+
To print the verified OAuth token for another local tool, use `--print-key`. Login status goes to stderr, so stdout contains only the token.
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
hoopilot login --print-key | sed 's/^/COPILOT_OAUTH_TOKEN=/' >> .env
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
PowerShell:
|
|
186
234
|
|
|
187
235
|
```powershell
|
|
188
|
-
|
|
236
|
+
hoopilot login --print-key |
|
|
237
|
+
ForEach-Object { "COPILOT_OAUTH_TOKEN=$_" } |
|
|
238
|
+
Add-Content -Encoding utf8 .env
|
|
189
239
|
```
|
|
190
240
|
|
|
191
|
-
|
|
241
|
+
Docker:
|
|
192
242
|
|
|
193
|
-
|
|
194
|
-
-
|
|
243
|
+
```sh
|
|
244
|
+
docker run --rm -v hoopilot-data:/data ghcr.io/openhoo/hoopilot login --print-key \
|
|
245
|
+
| sed 's/^/COPILOT_OAUTH_TOKEN=/' >> .env
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Logging
|
|
249
|
+
|
|
250
|
+
Hoopilot uses Pino for structured logs. Server startup, request completion, upstream Copilot failures, model-list fallback, auth failures, and update-check diagnostics are logged with stable event names and request IDs.
|
|
251
|
+
|
|
252
|
+
Logs never include request bodies, prompt text, completions, stream chunks, OAuth tokens, API keys, authorization headers, cookies, or auth-file contents.
|
|
253
|
+
|
|
254
|
+
Console logs default to pretty output at `info` level:
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
hoopilot --log-level info --log-format pretty
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
For newline-delimited JSON:
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
hoopilot --log-level info --log-format json
|
|
264
|
+
```
|
|
195
265
|
|
|
196
266
|
Incoming `x-request-id` headers are preserved on responses. If a request has no ID, Hoopilot generates one and returns it as `x-request-id`.
|
|
197
267
|
|
|
198
268
|
## Metrics and usage
|
|
199
269
|
|
|
200
|
-
Hoopilot tracks token usage, request counts, and latency in memory while the server runs
|
|
270
|
+
Hoopilot tracks token usage, request counts, and latency in memory while the server runs. It can also report your GitHub Copilot account quota and premium-request usage.
|
|
201
271
|
|
|
202
|
-
- `GET /metrics` returns Prometheus text (`text/plain; version=0.0.4`). It exposes request counters
|
|
203
|
-
- `GET /v1/usage` returns JSON combining the proxy metrics snapshot with live Copilot quota fetched from GitHub
|
|
272
|
+
- `GET /metrics` returns Prometheus text (`text/plain; version=0.0.4`). It exposes request counters, upstream call counters, token counters by model and type, a request-duration histogram, an in-flight gauge, and Copilot quota gauges after `/v1/usage` has been fetched at least once. Counters reset to zero on restart, which Prometheus handles natively.
|
|
273
|
+
- `GET /v1/usage` returns JSON combining the proxy metrics snapshot with live Copilot quota fetched from GitHub and cached for 60 seconds. If quota cannot be read, `copilot` is `null` and `copilot_error` explains why.
|
|
204
274
|
- `hoopilot usage` prints your Copilot plan and quota from the command line.
|
|
205
275
|
|
|
206
|
-
Token usage is read from the upstream `usage` object. For streaming chat completions, usage is only available when the client sends `stream_options: {"include_usage": true}`; Hoopilot
|
|
276
|
+
Token usage is read from the upstream `usage` object. For streaming chat completions, usage is only available when the client sends `stream_options: {"include_usage": true}`; Hoopilot does not inject that flag. Responses API streaming always reports usage, so streamed Responses requests are fully accounted.
|
|
207
277
|
|
|
208
278
|
`/metrics` and `/v1/usage` are subject to the same `HOOPILOT_API_KEY` gate as the other routes.
|
|
209
279
|
|
|
210
|
-
##
|
|
280
|
+
## Troubleshooting
|
|
211
281
|
|
|
212
|
-
|
|
282
|
+
### Codex auth errors
|
|
213
283
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
284
|
+
Hoopilot does not return raw `403` responses to Codex for authentication or Copilot-entitlement failures. Local Hoopilot API-key problems return `401 invalid_api_key`; OAuth credential and upstream Copilot auth failures return `401 copilot_auth_error`.
|
|
285
|
+
|
|
286
|
+
Verify browser login and the local proxy before retrying Codex:
|
|
287
|
+
|
|
288
|
+
```sh
|
|
289
|
+
hoopilot login
|
|
290
|
+
hoopilot --port 4141
|
|
217
291
|
```
|
|
218
292
|
|
|
219
|
-
|
|
293
|
+
Then, in another shell:
|
|
220
294
|
|
|
221
|
-
|
|
295
|
+
```sh
|
|
296
|
+
curl http://127.0.0.1:4141/v1/models
|
|
297
|
+
codexx
|
|
298
|
+
```
|
|
222
299
|
|
|
223
|
-
|
|
224
|
-
- `HOOPILOT_GITHUB_CLIENT_ID`: GitHub OAuth app client ID override. The default uses the same GitHub Copilot OAuth app as opencode's Copilot provider.
|
|
225
|
-
- `HOOPILOT_GITHUB_DOMAIN`: GitHub domain override. Default: `github.com`.
|
|
226
|
-
- `COPILOT_API_BASE_URL`: upstream Copilot API base URL override. Default: `https://api.githubcopilot.com`.
|
|
227
|
-
- `HOOPILOT_GITHUB_API_BASE_URL`: GitHub REST API base URL used for the Copilot quota lookup. Default: `https://api.github.com`.
|
|
228
|
-
- `HOOPILOT_ALLOW_UNSAFE_UPSTREAM=1`: allow sending the stored OAuth token to nonstandard HTTPS Copilot/GitHub API hosts. Use only for trusted test or enterprise endpoints.
|
|
300
|
+
If you started the server with `HOOPILOT_API_KEY`, add `-H "Authorization: Bearer $HOOPILOT_API_KEY"` to the curl command and set the same `HOOPILOT_API_KEY` for `codexx`.
|
|
229
301
|
|
|
230
|
-
|
|
302
|
+
If `/v1/models` returns `401 copilot_auth_error`, rerun `hoopilot login` and confirm that the GitHub account has active Copilot access.
|
|
231
303
|
|
|
232
|
-
|
|
304
|
+
## Configuration
|
|
233
305
|
|
|
234
|
-
|
|
306
|
+
Server and local-client settings:
|
|
235
307
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
308
|
+
| Setting | Description |
|
|
309
|
+
| --- | --- |
|
|
310
|
+
| `HOST` / `--host` | Host to listen on. Default: `127.0.0.1` for local runs; Docker sets `0.0.0.0`. |
|
|
311
|
+
| `PORT` / `--port` | Port to listen on. Default: `4141`. |
|
|
312
|
+
| `HOOPILOT_API_KEY` / `--api-key` | Require clients to send `Authorization: Bearer <key>` or `x-api-key: <key>`. Must be a strong, unique secret on non-loopback binds; well-known demo keys are rejected. |
|
|
313
|
+
| `--api-key-file` | Read the local API key from a file instead of argv. |
|
|
314
|
+
| `HOOPILOT_ALLOWED_ORIGINS` | Comma-separated browser origins allowed to make cross-origin requests. Loopback origins are always allowed; every other origin is blocked. |
|
|
315
|
+
| `HOOPILOT_ALLOW_UNAUTHENTICATED` / `--allow-unauthenticated` | Allow non-loopback binds without a local API key. |
|
|
316
|
+
| `HOOPILOT_STREAM_MODE` / `--stream-mode` | `auto`, `live`, or `buffer`. `auto` buffers streams for Windows standalone binaries. |
|
|
241
317
|
|
|
242
|
-
|
|
318
|
+
Copilot and GitHub settings:
|
|
243
319
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
320
|
+
| Setting | Description |
|
|
321
|
+
| --- | --- |
|
|
322
|
+
| `HOOPILOT_AUTH_FILE` / `--auth-file` | OAuth credential store path. |
|
|
323
|
+
| `HOOPILOT_GITHUB_CLIENT_ID` | GitHub OAuth app client ID override. |
|
|
324
|
+
| `HOOPILOT_GITHUB_DOMAIN` | GitHub domain override. Default: `github.com`. |
|
|
325
|
+
| `COPILOT_API_BASE_URL` / `--copilot-api-base-url` | Upstream Copilot API base URL. Default: `https://api.githubcopilot.com`. |
|
|
326
|
+
| `HOOPILOT_GITHUB_API_BASE_URL` | GitHub REST API base URL used for quota lookup. Default: `https://api.github.com`. |
|
|
327
|
+
| `HOOPILOT_ALLOW_UNSAFE_UPSTREAM=1` | Allow sending the stored OAuth token to nonstandard HTTPS Copilot/GitHub API hosts. Use only for trusted test or enterprise endpoints. |
|
|
250
328
|
|
|
251
|
-
|
|
329
|
+
Logging and update settings:
|
|
252
330
|
|
|
253
|
-
|
|
331
|
+
| Setting | Description |
|
|
332
|
+
| --- | --- |
|
|
333
|
+
| `HOOPILOT_LOG_LEVEL` / `--log-level` | `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or `silent`. Default: `info`. |
|
|
334
|
+
| `HOOPILOT_LOG_FORMAT` / `--log-format` | `pretty` or `json`. Default: `pretty`. |
|
|
335
|
+
| `HOOPILOT_NO_UPDATE_CHECK` / `--no-update-check` | Disable background update checks. `NO_UPDATE_NOTIFIER` is also honored. |
|
|
254
336
|
|
|
255
|
-
|
|
337
|
+
`codexx` settings:
|
|
338
|
+
|
|
339
|
+
| Setting | Description |
|
|
340
|
+
| --- | --- |
|
|
341
|
+
| `CODEXX_BASE_URL` | OpenAI-compatible Hoopilot base URL. Default: `http://127.0.0.1:4141/v1`. |
|
|
342
|
+
| `CODEXX_API_KEY` | API key sent to Hoopilot. Falls back to `HOOPILOT_API_KEY`, then a random per-run key for an unauthenticated local server. |
|
|
343
|
+
| `CODEXX_CODEX_BIN` | Codex executable to run. Default: `codex`. |
|
|
344
|
+
| `CODEXX_MODEL` | Codex model to use. Default: `gpt-5.5`. |
|
|
345
|
+
| `CODEXX_MODEL_REASONING_EFFORT` | Codex reasoning effort. Default: `xhigh`. |
|
|
346
|
+
| `CODEXX_SKIP_MODEL_PREFLIGHT=1` | Skip the `/v1/models` availability check before starting Codex. |
|
|
347
|
+
|
|
348
|
+
## CLI reference
|
|
349
|
+
|
|
350
|
+
```txt
|
|
256
351
|
hoopilot [serve] [options]
|
|
257
352
|
hoopilot codexx [codex options] [prompt]
|
|
258
353
|
hoopilot login [options]
|
|
259
354
|
hoopilot models [options]
|
|
260
355
|
hoopilot usage [options]
|
|
356
|
+
hoopilot update
|
|
261
357
|
```
|
|
262
358
|
|
|
263
359
|
Commands:
|
|
@@ -280,36 +376,41 @@ Options:
|
|
|
280
376
|
--api-key-file <path> Read the local API key from a file instead of argv
|
|
281
377
|
--auth-file <path> OAuth credential store path
|
|
282
378
|
--copilot-api-base-url <url> Copilot API base URL override
|
|
379
|
+
--print-key Login: print the received OAuth token to stdout
|
|
283
380
|
--log-level <level> trace, debug, info, warn, error, fatal, or silent
|
|
284
381
|
--log-format <format> json or pretty. Default: pretty
|
|
382
|
+
--stream-mode <mode> auto, live, or buffer. Auto buffers Windows standalone streams.
|
|
285
383
|
--no-update-check Do not check GitHub for a newer release
|
|
286
384
|
--allow-unauthenticated Allow non-loopback bind without --api-key
|
|
385
|
+
-h, --help Show help
|
|
386
|
+
-v, --version Show version
|
|
287
387
|
```
|
|
288
388
|
|
|
289
389
|
## Endpoints
|
|
290
390
|
|
|
291
|
-
- `GET /healthz`
|
|
391
|
+
- `GET /` and `GET /healthz`
|
|
292
392
|
- `GET /metrics`
|
|
293
393
|
- `GET /v1/models`
|
|
294
394
|
- `GET /v1/usage`
|
|
295
|
-
- `POST /v1/messages`
|
|
296
|
-
- `POST /v1/messages/count_tokens`
|
|
297
395
|
- `POST /v1/chat/completions`
|
|
298
396
|
- `POST /v1/responses`
|
|
397
|
+
- `POST /v1/responses/compact`
|
|
299
398
|
- `POST /v1/completions`
|
|
399
|
+
- `POST /v1/messages`
|
|
400
|
+
- `POST /v1/messages/count_tokens`
|
|
300
401
|
|
|
301
|
-
`/v1/chat/completions` and `/v1/responses` are proxied to the matching Copilot endpoints as directly as possible. `/v1/messages` translates Anthropic Messages requests and responses to Copilot's Responses endpoint
|
|
402
|
+
`/v1/chat/completions` and `/v1/responses` are proxied to the matching Copilot endpoints as directly as possible. `/v1/messages` translates Anthropic Messages requests and responses to Copilot's Responses endpoint. `/v1/messages/count_tokens` returns a local token estimate for Claude Code preflights because Copilot does not expose Anthropic's count-tokens route. `/v1/completions` translates legacy completion requests and responses to the closest chat-completions equivalent. `GET /v1/responses` returns an explicit unsupported-WebSocket response; `codexx` configures Codex to use HTTP Responses instead.
|
|
302
403
|
|
|
303
404
|
## Development
|
|
304
405
|
|
|
305
|
-
```
|
|
406
|
+
```sh
|
|
306
407
|
bun install
|
|
307
408
|
bun run check
|
|
308
409
|
```
|
|
309
410
|
|
|
310
411
|
Useful scripts:
|
|
311
412
|
|
|
312
|
-
```
|
|
413
|
+
```sh
|
|
313
414
|
bun run test
|
|
314
415
|
bun run test:coverage
|
|
315
416
|
bun run typecheck
|
|
@@ -319,7 +420,11 @@ bun run biome:fix
|
|
|
319
420
|
|
|
320
421
|
## Release
|
|
321
422
|
|
|
322
|
-
Commits merged to `main` are evaluated by hooversion after CI passes. When a release is produced, the release workflow creates the release commit, tag, and GitHub release automatically, publishes the package through npm trusted publishing, then cross-compiles standalone binaries for every supported platform
|
|
423
|
+
Commits merged to `main` are evaluated by hooversion after CI passes. When a release is produced, the release workflow creates the release commit, tag, and GitHub release automatically, publishes the package through npm trusted publishing, then cross-compiles standalone binaries for every supported platform with `scripts/build-binaries.sh` and attaches them plus a `SHA256SUMS` manifest to the GitHub release. Build all binaries locally with:
|
|
424
|
+
|
|
425
|
+
```sh
|
|
426
|
+
bun run build:binaries
|
|
427
|
+
```
|
|
323
428
|
|
|
324
429
|
Configure npm trusted publishing for `@openhoo/hoopilot` on npmjs.com before relying on automatic publication. The workflow uses GitHub Actions OIDC with `npm publish --access public --provenance`.
|
|
325
430
|
|
|
@@ -52,7 +52,6 @@ function asRecord(value) {
|
|
|
52
52
|
|
|
53
53
|
// src/codexx.ts
|
|
54
54
|
var DEFAULT_BASE_URL = "http://127.0.0.1:4141/v1";
|
|
55
|
-
var DEFAULT_API_KEY = "local-key";
|
|
56
55
|
var DEFAULT_CODEX_BIN = "codex";
|
|
57
56
|
var DEFAULT_MODEL = "gpt-5.5";
|
|
58
57
|
var DEFAULT_REASONING_EFFORT = "xhigh";
|
|
@@ -68,7 +67,7 @@ var PROXY_ENV_KEYS = [
|
|
|
68
67
|
];
|
|
69
68
|
function buildCodexxInvocation(argv, env = process.env) {
|
|
70
69
|
const baseUrl = envValue(env.CODEXX_BASE_URL) ?? DEFAULT_BASE_URL;
|
|
71
|
-
const apiKey = envValue(env.CODEXX_API_KEY) ?? envValue(env.HOOPILOT_API_KEY) ??
|
|
70
|
+
const apiKey = envValue(env.CODEXX_API_KEY) ?? envValue(env.HOOPILOT_API_KEY) ?? generateEphemeralApiKey();
|
|
72
71
|
const command = envValue(env.CODEXX_CODEX_BIN) ?? DEFAULT_CODEX_BIN;
|
|
73
72
|
const model = envValue(env.CODEXX_MODEL) ?? DEFAULT_MODEL;
|
|
74
73
|
const reasoningEffort = envValue(env.CODEXX_MODEL_REASONING_EFFORT) ?? DEFAULT_REASONING_EFFORT;
|
|
@@ -102,6 +101,9 @@ function buildCodexxInvocation(argv, env = process.env) {
|
|
|
102
101
|
model
|
|
103
102
|
};
|
|
104
103
|
}
|
|
104
|
+
function generateEphemeralApiKey() {
|
|
105
|
+
return `codexx-${crypto.randomUUID()}`;
|
|
106
|
+
}
|
|
105
107
|
function withoutProxyEnv(env) {
|
|
106
108
|
const next = { ...env };
|
|
107
109
|
for (const key of PROXY_ENV_KEYS) {
|
|
@@ -142,7 +144,7 @@ async function verifyCodexxModel(invocation, fetcher = fetch) {
|
|
|
142
144
|
response = await fetcher(modelsUrl, {
|
|
143
145
|
headers: {
|
|
144
146
|
accept: "application/json",
|
|
145
|
-
authorization: `Bearer ${invocation.env.OPENAI_API_KEY ??
|
|
147
|
+
authorization: `Bearer ${invocation.env.OPENAI_API_KEY ?? generateEphemeralApiKey()}`
|
|
146
148
|
},
|
|
147
149
|
method: "GET"
|
|
148
150
|
});
|
|
@@ -174,7 +176,9 @@ Usage:
|
|
|
174
176
|
Environment:
|
|
175
177
|
CODEXX_BASE_URL OpenAI-compatible base URL. Default: ${DEFAULT_BASE_URL}
|
|
176
178
|
CODEXX_API_KEY API key sent to the local Hoopilot server.
|
|
177
|
-
HOOPILOT_API_KEY Used as the API key when CODEXX_API_KEY is unset.
|
|
179
|
+
HOOPILOT_API_KEY Used as the API key when CODEXX_API_KEY is unset. When
|
|
180
|
+
neither is set, a random throwaway key is generated for
|
|
181
|
+
an unauthenticated local server.
|
|
178
182
|
CODEXX_CODEX_BIN Codex executable to run. Default: ${DEFAULT_CODEX_BIN}
|
|
179
183
|
CODEXX_MODEL Codex model to use. Default: ${DEFAULT_MODEL}
|
|
180
184
|
CODEXX_MODEL_REASONING_EFFORT
|
|
@@ -218,4 +222,4 @@ export {
|
|
|
218
222
|
main,
|
|
219
223
|
verifyCodexxModel
|
|
220
224
|
};
|
|
221
|
-
//# sourceMappingURL=chunk-
|
|
225
|
+
//# sourceMappingURL=chunk-JU6F5L34.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/codexx.ts","../src/util.ts"],"sourcesContent":["#!/usr/bin/env bun\n\nimport { spawn } from \"node:child_process\";\nimport { constants as osConstants } from \"node:os\";\nimport type { FetchLike } from \"./types\";\nimport { envValue } from \"./util\";\n\nconst DEFAULT_BASE_URL = \"http://127.0.0.1:4141/v1\";\nconst DEFAULT_CODEX_BIN = \"codex\";\nconst DEFAULT_MODEL = \"gpt-5.5\";\nconst DEFAULT_REASONING_EFFORT = \"xhigh\";\nconst PROXY_ENV_KEYS = [\n \"ALL_PROXY\",\n \"HTTPS_PROXY\",\n \"HTTP_PROXY\",\n \"NO_PROXY\",\n \"all_proxy\",\n \"https_proxy\",\n \"http_proxy\",\n \"no_proxy\",\n];\n\nexport interface CodexxInvocation {\n args: string[];\n baseUrl: string;\n command: string;\n env: NodeJS.ProcessEnv;\n model: string;\n}\n\nexport function buildCodexxInvocation(\n argv: string[],\n env: NodeJS.ProcessEnv = process.env,\n): CodexxInvocation {\n const baseUrl = envValue(env.CODEXX_BASE_URL) ?? DEFAULT_BASE_URL;\n // Never fall back to a public, predictable key: a shared constant like the old\n // \"local-key\" default is also a credential a malicious local/browser client\n // could guess. When no key is configured the local server is expected to run\n // unauthenticated, which accepts any value, so a random throwaway key is safe.\n const apiKey =\n envValue(env.CODEXX_API_KEY) ?? envValue(env.HOOPILOT_API_KEY) ?? generateEphemeralApiKey();\n const command = envValue(env.CODEXX_CODEX_BIN) ?? DEFAULT_CODEX_BIN;\n const model = envValue(env.CODEXX_MODEL) ?? DEFAULT_MODEL;\n const reasoningEffort = envValue(env.CODEXX_MODEL_REASONING_EFFORT) ?? DEFAULT_REASONING_EFFORT;\n const providerConfig = [\n '{ name = \"Hoopilot\"',\n `base_url = ${JSON.stringify(baseUrl)}`,\n 'env_key = \"OPENAI_API_KEY\"',\n 'wire_api = \"responses\"',\n \"supports_websockets = false }\",\n ].join(\", \");\n\n return {\n args: [\n \"--disable\",\n \"network_proxy\",\n \"-c\",\n 'model_provider=\"hoopilot\"',\n \"-c\",\n `model_providers.hoopilot=${providerConfig}`,\n \"-m\",\n model,\n \"-c\",\n `model_reasoning_effort=${JSON.stringify(reasoningEffort)}`,\n ...argv,\n ],\n baseUrl,\n command,\n env: withoutProxyEnv({\n ...env,\n OPENAI_API_KEY: apiKey,\n }),\n model,\n };\n}\n\n// A random, non-guessable placeholder key for when neither CODEXX_API_KEY nor\n// HOOPILOT_API_KEY is set. An unauthenticated local Hoopilot accepts any value;\n// a keyed server rejects it with a 401, which the model preflight surfaces.\nfunction generateEphemeralApiKey(): string {\n return `codexx-${crypto.randomUUID()}`;\n}\n\nfunction withoutProxyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {\n const next = { ...env };\n for (const key of PROXY_ENV_KEYS) {\n delete next[key];\n }\n return next;\n}\n\nexport async function main(argv = Bun.argv.slice(2), env = process.env): Promise<void> {\n if (argv.length === 1 && (argv[0] === \"--help\" || argv[0] === \"-h\")) {\n console.log(helpText());\n return;\n }\n\n const invocation = buildCodexxInvocation(argv, env);\n if (env.CODEXX_SKIP_MODEL_PREFLIGHT !== \"1\") {\n await verifyCodexxModel(invocation);\n }\n const child = spawn(invocation.command, invocation.args, {\n env: invocation.env,\n shell: process.platform === \"win32\",\n stdio: \"inherit\",\n });\n\n const exitCode = await new Promise<number>((resolve, reject) => {\n child.once(\"error\", reject);\n child.once(\"exit\", (code, signal) => {\n if (typeof code === \"number\") {\n resolve(code);\n return;\n }\n resolve(signal ? 128 + signalNumber(signal) : 1);\n });\n });\n\n process.exitCode = exitCode;\n}\n\nexport async function verifyCodexxModel(\n invocation: Pick<CodexxInvocation, \"baseUrl\" | \"env\" | \"model\">,\n fetcher: FetchLike = fetch,\n): Promise<void> {\n const modelsUrl = `${invocation.baseUrl.replace(/\\/+$/, \"\")}/models`;\n let response: Response;\n try {\n response = await fetcher(modelsUrl, {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${invocation.env.OPENAI_API_KEY ?? generateEphemeralApiKey()}`,\n },\n method: \"GET\",\n });\n } catch (error) {\n throw new Error(\n `Could not reach Hoopilot at ${modelsUrl}. Start Hoopilot first, or set CODEXX_SKIP_MODEL_PREFLIGHT=1 to skip this check. ${errorMessage(error)}`,\n );\n }\n\n if (!response.ok) {\n throw new Error(\n `Could not verify model ${JSON.stringify(invocation.model)} because ${modelsUrl} returned ${response.status}: ${await shortResponseText(response)}`,\n );\n }\n\n const models = modelIds(await response.json().catch(() => undefined));\n if (models.length > 0 && !models.includes(invocation.model)) {\n throw new Error(\n `The logged-in Copilot account does not advertise model ${JSON.stringify(invocation.model)} at ${modelsUrl}. Available models: ${models.join(\", \")}. After upgrading Hoopilot, rerun \"hoopilot login\" to refresh the Copilot OAuth token, or set CODEXX_MODEL to one of the advertised model IDs.`,\n );\n }\n}\n\nfunction helpText(): string {\n return `codexx\n\nRun Codex against an already-running local Hoopilot server.\n\nUsage:\n codexx [codex options] [prompt]\n\nEnvironment:\n CODEXX_BASE_URL OpenAI-compatible base URL. Default: ${DEFAULT_BASE_URL}\n CODEXX_API_KEY API key sent to the local Hoopilot server.\n HOOPILOT_API_KEY Used as the API key when CODEXX_API_KEY is unset. When\n neither is set, a random throwaway key is generated for\n an unauthenticated local server.\n CODEXX_CODEX_BIN Codex executable to run. Default: ${DEFAULT_CODEX_BIN}\n CODEXX_MODEL Codex model to use. Default: ${DEFAULT_MODEL}\n CODEXX_MODEL_REASONING_EFFORT\n Codex reasoning effort. Default: ${DEFAULT_REASONING_EFFORT}\n CODEXX_SKIP_MODEL_PREFLIGHT\n Set to 1 to skip checking /v1/models before starting Codex.\n\ncodexx does not start Hoopilot and does not change your shell environment. It selects a temporary Hoopilot model provider with Responses WebSockets disabled, uses ${DEFAULT_MODEL} with ${DEFAULT_REASONING_EFFORT} reasoning by default, disables Codex's network_proxy feature, and removes proxy variables only from the spawned Codex process.`;\n}\n\nfunction signalNumber(signal: NodeJS.Signals): number {\n return osConstants.signals[signal] ?? 1;\n}\n\nfunction modelIds(value: unknown): string[] {\n const record = value && typeof value === \"object\" && !Array.isArray(value) ? value : {};\n const data = \"data\" in record && Array.isArray(record.data) ? record.data : [];\n return data\n .map((entry) =>\n entry && typeof entry === \"object\" && \"id\" in entry && typeof entry.id === \"string\"\n ? entry.id\n : undefined,\n )\n .filter((id): id is string => typeof id === \"string\" && id.length > 0);\n}\n\nasync function shortResponseText(response: Response): Promise<string> {\n const text = await response.text();\n return text.slice(0, 500);\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nif (import.meta.main) {\n main().catch((error: unknown) => {\n console.error(errorMessage(error));\n process.exit(1);\n });\n}\n","import type { JsonObject } from \"./types\";\n\n/** Remove any trailing slashes from a URL or path string. */\nexport function trimTrailingSlash(value: string): string {\n return value.replace(/\\/+$/, \"\");\n}\n\n/** Treat blank environment variables as unset while preserving nonblank values. */\nexport function envValue(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\n/** True for HTTPS URLs, or HTTP only on loopback hosts used by local tests/dev. */\nexport function isHttpsOrLoopbackUrl(rawUrl: string): boolean {\n const url = parseUrl(rawUrl);\n if (!url) {\n return false;\n }\n return url.protocol === \"https:\" || isLoopbackHttpUrl(url);\n}\n\n/** Validate a base URL before sending a bearer/OAuth token to it. */\nexport function isTrustedTokenBaseUrl(\n rawUrl: string,\n allowedHttpsHosts: readonly string[],\n allowUnsafeHttps = false,\n): boolean {\n const url = parseUrl(rawUrl);\n if (!url) {\n return false;\n }\n if (url.username || url.password || url.search || url.hash) {\n return false;\n }\n if (url.pathname !== \"\" && url.pathname !== \"/\") {\n return false;\n }\n if (isLoopbackHttpUrl(url)) {\n return true;\n }\n if (url.protocol !== \"https:\") {\n return false;\n }\n const host = url.hostname.toLowerCase();\n return allowedHttpsHosts.includes(host) || allowUnsafeHttps;\n}\n\nfunction parseUrl(rawUrl: string): URL | undefined {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n return undefined;\n }\n return url;\n}\n\nfunction isLoopbackHttpUrl(url: URL): boolean {\n return (\n url.protocol === \"http:\" &&\n (url.hostname === \"127.0.0.1\" ||\n url.hostname === \"localhost\" ||\n url.hostname === \"::1\" ||\n url.hostname === \"[::1]\")\n );\n}\n\n/** Read a response body as text, truncated to keep error messages bounded. */\nexport async function truncatedResponseText(response: Response, max = 500): Promise<string> {\n const text = await response.text();\n return text.slice(0, max);\n}\n\n/** Narrow an unknown value to a plain object, returning {} for arrays/primitives/null. */\nexport function asRecord(value: unknown): JsonObject {\n return value && typeof value === \"object\" && !Array.isArray(value) ? (value as JsonObject) : {};\n}\n"],"mappings":"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","names":[]}
|