@openhoo/hoopilot 1.3.0 → 2.1.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 +67 -23
- package/dist/{chunk-JU6F5L34.js → chunk-6ALEIJJM.js} +82 -20
- package/dist/chunk-6ALEIJJM.js.map +1 -0
- package/dist/cli.js +394 -403
- package/dist/cli.js.map +1 -1
- package/dist/codexx.js +1 -1
- package/dist/index.d.ts +20 -6
- package/dist/index.js +410 -354
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-JU6F5L34.js.map +0 -1
- package/dist/index.cjs +0 -4653
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -388
package/README.md
CHANGED
|
@@ -7,9 +7,29 @@ Hoopilot is a local OpenAI- and Anthropic-compatible proxy for GitHub Copilot ac
|
|
|
7
7
|
|
|
8
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
9
|
|
|
10
|
+
## Contents
|
|
11
|
+
|
|
12
|
+
- [Highlights](#highlights)
|
|
13
|
+
- [Requirements](#requirements)
|
|
14
|
+
- [Quick start](#quick-start)
|
|
15
|
+
- [Install](#install)
|
|
16
|
+
- [Update](#update)
|
|
17
|
+
- [Running the proxy](#running-the-proxy)
|
|
18
|
+
- [Client setup](#client-setup)
|
|
19
|
+
- [Authentication](#authentication)
|
|
20
|
+
- [Logging](#logging)
|
|
21
|
+
- [Metrics and usage](#metrics-and-usage)
|
|
22
|
+
- [Dashboard](#dashboard)
|
|
23
|
+
- [Troubleshooting](#troubleshooting)
|
|
24
|
+
- [Configuration](#configuration)
|
|
25
|
+
- [CLI reference](#cli-reference)
|
|
26
|
+
- [Endpoints](#endpoints)
|
|
27
|
+
- [Development](#development)
|
|
28
|
+
- [Release](#release)
|
|
29
|
+
|
|
10
30
|
## Highlights
|
|
11
31
|
|
|
12
|
-
-
|
|
32
|
+
- GitHub Copilot OAuth login via GitHub's device flow — prints a one-time code and opens your browser when possible — with a local credential store.
|
|
13
33
|
- OpenAI-compatible Chat Completions, Responses, legacy Completions, and model-list routes.
|
|
14
34
|
- Anthropic Messages compatibility for Claude Code and other Anthropic-style clients.
|
|
15
35
|
- Bundled `codexx` launcher that runs Codex against a local Hoopilot server with the right Responses API provider settings.
|
|
@@ -17,6 +37,12 @@ This project uses GitHub Copilot service endpoints and is not an official GitHub
|
|
|
17
37
|
- Self-contained live dashboard at `/dashboard` showing usage and status metrics in real time.
|
|
18
38
|
- npm package, standalone binaries, Docker image, and self-update support for release binaries.
|
|
19
39
|
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- A GitHub account with an active GitHub Copilot subscription.
|
|
43
|
+
- [Bun](https://bun.sh) 1.3 or newer to run from npm or source — the CLI runs on the Bun runtime, so `bun` must be on your `PATH` (this applies to `npx @openhoo/hoopilot` too).
|
|
44
|
+
- Nothing extra for the standalone binary or Docker image: both bundle the runtime, so neither needs Bun or Node.js installed.
|
|
45
|
+
|
|
20
46
|
## Quick start
|
|
21
47
|
|
|
22
48
|
Sign in once, then start the proxy on localhost:
|
|
@@ -55,6 +81,8 @@ npx --package @openhoo/hoopilot codexx
|
|
|
55
81
|
|
|
56
82
|
## Install
|
|
57
83
|
|
|
84
|
+
Choose the method that fits your environment: **npm** if you already have Bun, a **standalone binary** for a dependency-free install, or **Docker** to run Hoopilot as a long-lived service.
|
|
85
|
+
|
|
58
86
|
### npm
|
|
59
87
|
|
|
60
88
|
Run without installing:
|
|
@@ -63,13 +91,16 @@ Run without installing:
|
|
|
63
91
|
npx @openhoo/hoopilot
|
|
64
92
|
```
|
|
65
93
|
|
|
66
|
-
Or install the package globally:
|
|
94
|
+
Or install the package globally with either npm or Bun:
|
|
67
95
|
|
|
68
96
|
```sh
|
|
69
97
|
npm install -g @openhoo/hoopilot
|
|
98
|
+
# or
|
|
70
99
|
bun add -g @openhoo/hoopilot
|
|
71
100
|
```
|
|
72
101
|
|
|
102
|
+
The package is published **ESM-only**: the CLIs (`hoopilot`, `codexx`) and the library entry (`import { startHoopilotServer } from "@openhoo/hoopilot"`) both require an ESM loader. There is no CommonJS (`require()`) entry — it was dropped because Hoopilot is a Bun/ESM-native tool, not because any dependency forced it.
|
|
103
|
+
|
|
73
104
|
### Standalone binary
|
|
74
105
|
|
|
75
106
|
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.
|
|
@@ -100,32 +131,45 @@ The standalone installer also installs a `codexx` wrapper next to `hoopilot`. Re
|
|
|
100
131
|
|
|
101
132
|
### Docker
|
|
102
133
|
|
|
103
|
-
Run Hoopilot as a long-lived service from the published multi-arch image on the GitHub Container Registry (`linux/amd64` and `linux/arm64`)
|
|
134
|
+
Run Hoopilot as a long-lived service from the published multi-arch image on the GitHub Container Registry (`linux/amd64` and `linux/arm64`). The commands below are the same on Windows (PowerShell or `cmd`), macOS, and Linux — only the shell prompt differs.
|
|
135
|
+
|
|
136
|
+
#### Keyless local quick start
|
|
137
|
+
|
|
138
|
+
Published on loopback (`127.0.0.1`) only, the proxy is unreachable from other hosts, so no client API key is needed. Three commands — nothing to export, no keys to generate or keep in sync:
|
|
104
139
|
|
|
105
140
|
```sh
|
|
106
|
-
# 1. Sign in once; the OAuth credential
|
|
141
|
+
# 1. Sign in once; the OAuth credential persists in the named volume.
|
|
107
142
|
docker run --rm -it -v hoopilot-data:/data ghcr.io/openhoo/hoopilot login
|
|
108
143
|
|
|
109
|
-
# 2.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
-v hoopilot-data:/data ghcr.io/openhoo/hoopilot
|
|
144
|
+
# 2. Start the proxy: loopback-only, no key.
|
|
145
|
+
docker run -d --name hoopilot --restart unless-stopped -p 127.0.0.1:4141:4141 -e HOOPILOT_ALLOW_UNAUTHENTICATED=1 -v hoopilot-data:/data ghcr.io/openhoo/hoopilot
|
|
146
|
+
|
|
147
|
+
# 3. Run Codex through it from any directory — no key, no setup.
|
|
148
|
+
npx --package @openhoo/hoopilot codexx --yolo
|
|
115
149
|
```
|
|
116
150
|
|
|
117
|
-
|
|
151
|
+
Every line is a single command that pastes as-is into PowerShell, `cmd`, bash, or zsh. Step 3 needs the `codex` CLI on your `PATH`; `codexx` defaults to `gpt-5.5` and sends a throwaway key that the keyless proxy accepts (Bun users can swap `npx --package @openhoo/hoopilot` for `bunx`).
|
|
118
152
|
|
|
119
|
-
|
|
153
|
+
Or, from a clone of this repo, use the bundled `docker-compose.yml` — it is keyless on loopback by default:
|
|
120
154
|
|
|
121
|
-
|
|
155
|
+
```sh
|
|
156
|
+
docker compose run --rm hoopilot login # one-time
|
|
157
|
+
docker compose up -d # keyless on loopback
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Tags follow the release version, for example `ghcr.io/openhoo/hoopilot:1.3`, `:1.3.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`.
|
|
161
|
+
|
|
162
|
+
#### Exposing the proxy beyond loopback
|
|
163
|
+
|
|
164
|
+
The image binds `0.0.0.0` and cannot tell whether the published port is loopback-only, so it fails closed: drop the `-e HOOPILOT_ALLOW_UNAUTHENTICATED=1` opt-in (or map the port to a non-loopback interface) and it refuses to start without a strong, unique `HOOPILOT_API_KEY` (well-known demo keys are rejected). Clients then send that key as `Authorization: Bearer <key>` or `x-api-key: <key>`:
|
|
122
165
|
|
|
123
166
|
```sh
|
|
124
|
-
docker compose run --rm hoopilot login
|
|
125
167
|
export HOOPILOT_API_KEY=$(openssl rand -hex 24)
|
|
126
|
-
docker
|
|
168
|
+
docker run -d --name hoopilot --restart unless-stopped -p 4141:4141 -e HOOPILOT_API_KEY -v hoopilot-data:/data ghcr.io/openhoo/hoopilot
|
|
127
169
|
```
|
|
128
170
|
|
|
171
|
+
With compose, a set `HOOPILOT_API_KEY` takes precedence over the keyless default: `export HOOPILOT_API_KEY=$(openssl rand -hex 24)` then `docker compose up -d`. To run unauthenticated on a non-loopback bind anyway — for example behind your own authenticating proxy — keep `HOOPILOT_ALLOW_UNAUTHENTICATED=1`. Point `codexx` at a keyed server by exporting the same `HOOPILOT_API_KEY` (or `CODEXX_API_KEY`) in its environment.
|
|
172
|
+
|
|
129
173
|
## Update
|
|
130
174
|
|
|
131
175
|
Standalone binaries update themselves in place from the latest GitHub release:
|
|
@@ -172,9 +216,7 @@ export OPENAI_BASE_URL=http://127.0.0.1:4141/v1
|
|
|
172
216
|
export OPENAI_API_KEY=hoopilot
|
|
173
217
|
```
|
|
174
218
|
|
|
175
|
-
The client key
|
|
176
|
-
|
|
177
|
-
Use any model returned by:
|
|
219
|
+
The client key is arbitrary unless you set `HOOPILOT_API_KEY` (see [Running the proxy](#running-the-proxy)). Available models depend on your Copilot plan; list the ones your account can use with:
|
|
178
220
|
|
|
179
221
|
```sh
|
|
180
222
|
hoopilot models
|
|
@@ -204,7 +246,7 @@ Without a global install:
|
|
|
204
246
|
npx --package @openhoo/hoopilot codexx
|
|
205
247
|
```
|
|
206
248
|
|
|
207
|
-
If the server
|
|
249
|
+
With the [keyless Docker quick start](#keyless-local-quick-start), no key is involved: `codexx --yolo` works from any directory because `codexx` sends a throwaway key that the loopback-only proxy accepts. If the server *does* require an API key, set `HOOPILOT_API_KEY` (or `CODEXX_API_KEY`) in the `codexx` environment to match.
|
|
208
250
|
|
|
209
251
|
`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.
|
|
210
252
|
|
|
@@ -225,7 +267,7 @@ Direct bearer tokens, GitHub CLI token fallback, classic GitHub PATs, and fine-g
|
|
|
225
267
|
|
|
226
268
|
Re-run `hoopilot login` after upgrading Hoopilot if Copilot reports a supported model as unavailable. Older stored tokens can have a reduced model set.
|
|
227
269
|
|
|
228
|
-
To print the verified OAuth token for another local tool, use `--print-key
|
|
270
|
+
To print the verified OAuth token for another local tool, use `--print-key` (the alias `--print-token` also works). Login status goes to stderr, so stdout contains only the token.
|
|
229
271
|
|
|
230
272
|
```sh
|
|
231
273
|
hoopilot login --print-key | sed 's/^/COPILOT_OAUTH_TOKEN=/' >> .env
|
|
@@ -270,7 +312,7 @@ Incoming `x-request-id` headers are preserved on responses. If a request has no
|
|
|
270
312
|
|
|
271
313
|
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, plus your GitHub REST API rate-limit budget.
|
|
272
314
|
|
|
273
|
-
- `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, Copilot quota gauges, and GitHub REST API rate-limit gauges (`hoopilot_github_ratelimit_limit`, `_remaining`, `_used`, `_reset_timestamp_seconds`, `_retry_after_seconds`, labelled by `resource`) — the quota and rate-limit series appear after `/v1/usage` has been fetched at least once. Counters reset to zero on restart, which Prometheus handles natively.
|
|
315
|
+
- `GET /metrics` returns Prometheus text (`text/plain; version=0.0.4; charset=utf-8`). It exposes request counters, upstream call counters, token counters by model and type, a request-duration histogram, an in-flight gauge, process start-time and uptime gauges (`hoopilot_process_start_time_seconds`, `hoopilot_uptime_seconds`), Copilot quota gauges, and GitHub REST API rate-limit gauges (`hoopilot_github_ratelimit_limit`, `_remaining`, `_used`, `_reset_timestamp_seconds`, `_retry_after_seconds`, labelled by `resource`) — the quota and rate-limit series appear after `/v1/usage` has been fetched at least once. Counters reset to zero on restart, which Prometheus handles natively.
|
|
274
316
|
- `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. The snapshot's `proxy.githubRateLimit` field reports the most recent GitHub REST rate-limit budget per resource (`limit`, `remaining`, `used`, `resetAt`, `retryAfterSeconds`, `observedAt`).
|
|
275
317
|
- `hoopilot usage` prints your Copilot plan and quota — and, when GitHub returns them, your GitHub API rate-limit budget — from the command line.
|
|
276
318
|
|
|
@@ -312,6 +354,8 @@ If `/v1/models` returns `401 copilot_auth_error`, rerun `hoopilot login` and con
|
|
|
312
354
|
|
|
313
355
|
## Configuration
|
|
314
356
|
|
|
357
|
+
Settings written as `ENV / --flag` accept either the environment variable or the command-line flag.
|
|
358
|
+
|
|
315
359
|
Server and local-client settings:
|
|
316
360
|
|
|
317
361
|
| Setting | Description |
|
|
@@ -322,14 +366,14 @@ Server and local-client settings:
|
|
|
322
366
|
| `--api-key-file` | Read the local API key from a file instead of argv. |
|
|
323
367
|
| `HOOPILOT_ALLOWED_ORIGINS` | Comma-separated browser origins allowed to make cross-origin requests. Loopback origins are always allowed; every other origin is blocked. |
|
|
324
368
|
| `HOOPILOT_ALLOW_UNAUTHENTICATED` / `--allow-unauthenticated` | Allow non-loopback binds without a local API key. |
|
|
325
|
-
| `HOOPILOT_STREAM_MODE` / `--stream-mode` | `auto`, `live`, or `buffer`. `auto` buffers streams for Windows standalone binaries. |
|
|
369
|
+
| `HOOPILOT_STREAM_MODE` / `--stream-mode` | `auto`, `live`, or `buffer`. `auto` buffers streams for Windows standalone binaries. `HOOPILOT_STREAMING_PROXY_MODE` is accepted as an alias. |
|
|
326
370
|
|
|
327
371
|
Copilot and GitHub settings:
|
|
328
372
|
|
|
329
373
|
| Setting | Description |
|
|
330
374
|
| --- | --- |
|
|
331
375
|
| `HOOPILOT_AUTH_FILE` / `--auth-file` | OAuth credential store path. |
|
|
332
|
-
| `HOOPILOT_GITHUB_CLIENT_ID` | GitHub OAuth app client ID override. |
|
|
376
|
+
| `HOOPILOT_GITHUB_CLIENT_ID` | GitHub OAuth app client ID override. `COPILOT_GITHUB_CLIENT_ID` is accepted as an alias. |
|
|
333
377
|
| `HOOPILOT_GITHUB_DOMAIN` | GitHub domain override. Default: `github.com`. |
|
|
334
378
|
| `COPILOT_API_BASE_URL` / `--copilot-api-base-url` | Upstream Copilot API base URL. Default: `https://api.githubcopilot.com`. |
|
|
335
379
|
| `HOOPILOT_GITHUB_API_BASE_URL` | GitHub REST API base URL used for quota lookup. Default: `https://api.github.com`. |
|
|
@@ -39,8 +39,12 @@ function parseUrl(rawUrl) {
|
|
|
39
39
|
}
|
|
40
40
|
return url;
|
|
41
41
|
}
|
|
42
|
+
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
43
|
+
function isLoopbackHostname(host) {
|
|
44
|
+
return LOOPBACK_HOSTNAMES.has(host);
|
|
45
|
+
}
|
|
42
46
|
function isLoopbackHttpUrl(url) {
|
|
43
|
-
return url.protocol === "http:" && (url.hostname
|
|
47
|
+
return url.protocol === "http:" && isLoopbackHostname(url.hostname);
|
|
44
48
|
}
|
|
45
49
|
async function truncatedResponseText(response, max = 500) {
|
|
46
50
|
const text = await response.text();
|
|
@@ -49,6 +53,63 @@ async function truncatedResponseText(response, max = 500) {
|
|
|
49
53
|
function asRecord(value) {
|
|
50
54
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
51
55
|
}
|
|
56
|
+
function errorMessage(error) {
|
|
57
|
+
return error instanceof Error ? error.message : String(error);
|
|
58
|
+
}
|
|
59
|
+
function firstNumber(...values) {
|
|
60
|
+
for (const value of values) {
|
|
61
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
function randomId() {
|
|
68
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
69
|
+
}
|
|
70
|
+
function removeUndefined(value) {
|
|
71
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
72
|
+
}
|
|
73
|
+
function safeJsonParse(text) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(text);
|
|
76
|
+
} catch {
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function parseJsonObject(text) {
|
|
81
|
+
try {
|
|
82
|
+
return asRecord(JSON.parse(text));
|
|
83
|
+
} catch {
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function modelIdsFromResponse(body) {
|
|
88
|
+
const record = asRecord(body);
|
|
89
|
+
const data = Array.isArray(record.data) ? record.data : Array.isArray(body) ? body : [];
|
|
90
|
+
const seen = /* @__PURE__ */ new Set();
|
|
91
|
+
const ids = [];
|
|
92
|
+
for (const model of data) {
|
|
93
|
+
const id = asRecord(model).id;
|
|
94
|
+
if (typeof id !== "string" || id.length === 0 || seen.has(id)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
seen.add(id);
|
|
98
|
+
ids.push(id);
|
|
99
|
+
}
|
|
100
|
+
return ids;
|
|
101
|
+
}
|
|
102
|
+
var STREAMING_PROXY_MODES = [
|
|
103
|
+
"auto",
|
|
104
|
+
"buffer",
|
|
105
|
+
"live"
|
|
106
|
+
];
|
|
107
|
+
function parseStreamingProxyMode(value) {
|
|
108
|
+
if (STREAMING_PROXY_MODES.includes(value)) {
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Invalid stream mode: ${value}. Expected ${STREAMING_PROXY_MODES.join(", ")}.`);
|
|
112
|
+
}
|
|
52
113
|
|
|
53
114
|
// src/codexx.ts
|
|
54
115
|
var DEFAULT_BASE_URL = "http://127.0.0.1:4141/v1";
|
|
@@ -138,13 +199,19 @@ async function main(argv = Bun.argv.slice(2), env = process.env) {
|
|
|
138
199
|
process.exitCode = exitCode;
|
|
139
200
|
}
|
|
140
201
|
async function verifyCodexxModel(invocation, fetcher = fetch) {
|
|
141
|
-
const modelsUrl = `${invocation.baseUrl
|
|
202
|
+
const modelsUrl = `${trimTrailingSlash(invocation.baseUrl)}/models`;
|
|
203
|
+
const apiKey = invocation.env.OPENAI_API_KEY;
|
|
204
|
+
if (apiKey === void 0) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"verifyCodexxModel requires invocation.env.OPENAI_API_KEY; build the invocation with buildCodexxInvocation."
|
|
207
|
+
);
|
|
208
|
+
}
|
|
142
209
|
let response;
|
|
143
210
|
try {
|
|
144
211
|
response = await fetcher(modelsUrl, {
|
|
145
212
|
headers: {
|
|
146
213
|
accept: "application/json",
|
|
147
|
-
authorization: `Bearer ${
|
|
214
|
+
authorization: `Bearer ${apiKey}`
|
|
148
215
|
},
|
|
149
216
|
method: "GET"
|
|
150
217
|
});
|
|
@@ -155,10 +222,10 @@ async function verifyCodexxModel(invocation, fetcher = fetch) {
|
|
|
155
222
|
}
|
|
156
223
|
if (!response.ok) {
|
|
157
224
|
throw new Error(
|
|
158
|
-
`Could not verify model ${JSON.stringify(invocation.model)} because ${modelsUrl} returned ${response.status}: ${await
|
|
225
|
+
`Could not verify model ${JSON.stringify(invocation.model)} because ${modelsUrl} returned ${response.status}: ${await truncatedResponseText(response)}`
|
|
159
226
|
);
|
|
160
227
|
}
|
|
161
|
-
const models =
|
|
228
|
+
const models = modelIdsFromResponse(await response.json().catch(() => void 0));
|
|
162
229
|
if (models.length > 0 && !models.includes(invocation.model)) {
|
|
163
230
|
throw new Error(
|
|
164
231
|
`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.`
|
|
@@ -191,20 +258,6 @@ codexx does not start Hoopilot and does not change your shell environment. It se
|
|
|
191
258
|
function signalNumber(signal) {
|
|
192
259
|
return osConstants.signals[signal] ?? 1;
|
|
193
260
|
}
|
|
194
|
-
function modelIds(value) {
|
|
195
|
-
const record = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
196
|
-
const data = "data" in record && Array.isArray(record.data) ? record.data : [];
|
|
197
|
-
return data.map(
|
|
198
|
-
(entry) => entry && typeof entry === "object" && "id" in entry && typeof entry.id === "string" ? entry.id : void 0
|
|
199
|
-
).filter((id) => typeof id === "string" && id.length > 0);
|
|
200
|
-
}
|
|
201
|
-
async function shortResponseText(response) {
|
|
202
|
-
const text = await response.text();
|
|
203
|
-
return text.slice(0, 500);
|
|
204
|
-
}
|
|
205
|
-
function errorMessage(error) {
|
|
206
|
-
return error instanceof Error ? error.message : String(error);
|
|
207
|
-
}
|
|
208
261
|
if (import.meta.main) {
|
|
209
262
|
main().catch((error) => {
|
|
210
263
|
console.error(errorMessage(error));
|
|
@@ -216,10 +269,19 @@ export {
|
|
|
216
269
|
trimTrailingSlash,
|
|
217
270
|
envValue,
|
|
218
271
|
isTrustedTokenBaseUrl,
|
|
272
|
+
isLoopbackHostname,
|
|
219
273
|
truncatedResponseText,
|
|
220
274
|
asRecord,
|
|
275
|
+
errorMessage,
|
|
276
|
+
firstNumber,
|
|
277
|
+
randomId,
|
|
278
|
+
removeUndefined,
|
|
279
|
+
safeJsonParse,
|
|
280
|
+
parseJsonObject,
|
|
281
|
+
modelIdsFromResponse,
|
|
282
|
+
parseStreamingProxyMode,
|
|
221
283
|
buildCodexxInvocation,
|
|
222
284
|
main,
|
|
223
285
|
verifyCodexxModel
|
|
224
286
|
};
|
|
225
|
-
//# sourceMappingURL=chunk-
|
|
287
|
+
//# sourceMappingURL=chunk-6ALEIJJM.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 {\n envValue,\n errorMessage,\n modelIdsFromResponse,\n trimTrailingSlash,\n truncatedResponseText,\n} 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 = `${trimTrailingSlash(invocation.baseUrl)}/models`;\n const apiKey = invocation.env.OPENAI_API_KEY;\n if (apiKey === undefined) {\n throw new Error(\n \"verifyCodexxModel requires invocation.env.OPENAI_API_KEY; build the invocation with buildCodexxInvocation.\",\n );\n }\n let response: Response;\n try {\n response = await fetcher(modelsUrl, {\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${apiKey}`,\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 truncatedResponseText(response)}`,\n );\n }\n\n const models = modelIdsFromResponse(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\nif (import.meta.main) {\n main().catch((error: unknown) => {\n console.error(errorMessage(error));\n process.exit(1);\n });\n}\n","import type { JsonObject, StreamingProxyMode } 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\nconst LOOPBACK_HOSTNAMES = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\"]);\n\n/** True for hostnames that always resolve to the local machine. */\nexport function isLoopbackHostname(host: string): boolean {\n return LOOPBACK_HOSTNAMES.has(host);\n}\n\nfunction isLoopbackHttpUrl(url: URL): boolean {\n return url.protocol === \"http:\" && isLoopbackHostname(url.hostname);\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\n/** Extract a human-readable message from an unknown thrown value. */\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Return the first finite number among the candidates, else undefined. */\nexport function firstNumber(...values: unknown[]): number | undefined {\n for (const value of values) {\n if (typeof value === \"number\" && Number.isFinite(value)) {\n return value;\n }\n }\n return undefined;\n}\n\n/** Generate a dash-free random identifier for synthesized response/message ids. */\nexport function randomId(): string {\n return crypto.randomUUID().replaceAll(\"-\", \"\");\n}\n\n/** Drop keys whose value is undefined so they are omitted from JSON output. */\nexport function removeUndefined<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\n/** Parse JSON, returning undefined instead of throwing on malformed input. */\nexport function safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\n/** Parse JSON into a plain object, returning undefined on malformed or non-object input. */\nexport function parseJsonObject(text: string): JsonObject | undefined {\n try {\n return asRecord(JSON.parse(text));\n } catch {\n return undefined;\n }\n}\n\n/**\n * Extract de-duplicated model IDs from an OpenAI-style `/models` response (an\n * object carrying a `data` array, or a bare array of model objects).\n */\nexport function modelIdsFromResponse(body: unknown): string[] {\n const record = asRecord(body);\n const data = Array.isArray(record.data) ? record.data : Array.isArray(body) ? body : [];\n const seen = new Set<string>();\n const ids: string[] = [];\n for (const model of data) {\n const id = asRecord(model).id;\n if (typeof id !== \"string\" || id.length === 0 || seen.has(id)) {\n continue;\n }\n seen.add(id);\n ids.push(id);\n }\n return ids;\n}\n\n/** Canonical set of accepted streaming-proxy modes, kept in sync with {@link StreamingProxyMode}. */\nexport const STREAMING_PROXY_MODES = [\n \"auto\",\n \"buffer\",\n \"live\",\n] as const satisfies readonly StreamingProxyMode[];\n\n/** Validate a stream-mode string against the allowed {@link StreamingProxyMode} values. */\nexport function parseStreamingProxyMode(value: string): StreamingProxyMode {\n if ((STREAMING_PROXY_MODES as readonly string[]).includes(value)) {\n return value as StreamingProxyMode;\n }\n throw new Error(`Invalid stream mode: ${value}. Expected ${STREAMING_PROXY_MODES.join(\", \")}.`);\n}\n"],"mappings":"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","names":[]}
|