@openhoo/hoopilot 2.0.0 → 2.1.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 CHANGED
@@ -131,32 +131,45 @@ The standalone installer also installs a `codexx` wrapper next to `hoopilot`. Re
131
131
 
132
132
  ### Docker
133
133
 
134
- 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:
135
139
 
136
140
  ```sh
137
- # 1. Sign in once; the OAuth credential is written to the persisted /data volume.
141
+ # 1. Sign in once; the OAuth credential persists in the named volume.
138
142
  docker run --rm -it -v hoopilot-data:/data ghcr.io/openhoo/hoopilot login
139
143
 
140
- # 2. Run the proxy on localhost with a strong, unique API key.
141
- export HOOPILOT_API_KEY=$(openssl rand -hex 24)
142
- docker run -d --name hoopilot --restart unless-stopped \
143
- -p 127.0.0.1:4141:4141 \
144
- -e HOOPILOT_API_KEY \
145
- -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
149
+ ```
150
+
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`).
152
+
153
+ Or, from a clone of this repo, use the bundled `docker-compose.yml` — it is keyless on loopback by default:
154
+
155
+ ```sh
156
+ docker compose run --rm hoopilot login # one-time
157
+ docker compose up -d # keyless on loopback
146
158
  ```
147
159
 
148
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`.
149
161
 
150
- 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`.
162
+ #### Exposing the proxy beyond loopback
151
163
 
152
- A `docker-compose.yml` is provided. Set `HOOPILOT_API_KEY` first; compose passes it through to the container:
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>`:
153
165
 
154
166
  ```sh
155
- docker compose run --rm hoopilot login
156
167
  export HOOPILOT_API_KEY=$(openssl rand -hex 24)
157
- docker compose up -d
168
+ docker run -d --name hoopilot --restart unless-stopped -p 4141:4141 -e HOOPILOT_API_KEY -v hoopilot-data:/data ghcr.io/openhoo/hoopilot
158
169
  ```
159
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
+
160
173
  ## Update
161
174
 
162
175
  Standalone binaries update themselves in place from the latest GitHub release:
@@ -233,9 +246,11 @@ Without a global install:
233
246
  npx --package @openhoo/hoopilot codexx
234
247
  ```
235
248
 
236
- If the server requires an API key, set `HOOPILOT_API_KEY` (or `CODEXX_API_KEY`) in the `codexx` environment to match.
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.
250
+
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, disables Codex's managed network proxy (`-c permissions.workspace.network.enabled=false`), and removes standard proxy variables from the spawned Codex process.
237
252
 
238
- `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.
253
+ > **Why the network proxy is disabled.** Codex ships a managed network proxy that routes the agent's traffic through a local proxy (port `3128`) and enforces a domain allowlist. Because the local Hoopilot server isn't on that allowlist, leaving the proxy on makes Codex's request to Hoopilot fail with an instant `403` Squid error. The proxy has two independent gates the `network_proxy` feature flag and the `permissions.workspace.network.enabled` config — and disabling only the feature is not always enough, so `codexx` turns off both (`--disable network_proxy` and `-c permissions.workspace.network.enabled=false`) and requests reach Hoopilot directly.
239
254
 
240
255
  `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.
241
256
 
@@ -141,9 +141,20 @@ function buildCodexxInvocation(argv, env = process.env) {
141
141
  ].join(", ");
142
142
  return {
143
143
  args: [
144
+ // Codex ships a managed network proxy (codex-rs/network-proxy) that routes the
145
+ // agent's traffic through a local proxy on :3128 and enforces a domain allowlist.
146
+ // A host that is not allowlisted — like the local Hoopilot server — gets an instant
147
+ // 403 (the Squid error page) and never reaches Hoopilot. It has two independent
148
+ // gates: the `network_proxy` feature flag and the `permissions.workspace.network`
149
+ // config. Disabling only the feature (`--disable network_proxy`) does not reliably
150
+ // turn it off when the proxy is enabled through the permissions config, so set both
151
+ // off: the feature flag via `--disable`, and the proxy itself via the config key
152
+ // (when `enabled` is false the proxy no-ops and binds no listeners).
144
153
  "--disable",
145
154
  "network_proxy",
146
155
  "-c",
156
+ "permissions.workspace.network.enabled=false",
157
+ "-c",
147
158
  'model_provider="hoopilot"',
148
159
  "-c",
149
160
  `model_providers.hoopilot=${providerConfig}`,
@@ -253,7 +264,7 @@ Environment:
253
264
  CODEXX_SKIP_MODEL_PREFLIGHT
254
265
  Set to 1 to skip checking /v1/models before starting Codex.
255
266
 
256
- codexx 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.`;
267
+ codexx 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 managed network proxy (permissions.workspace.network.enabled=false) so requests reach the local server instead of being blocked by its allowlist, and removes proxy variables only from the spawned Codex process.`;
257
268
  }
258
269
  function signalNumber(signal) {
259
270
  return osConstants.signals[signal] ?? 1;
@@ -284,4 +295,4 @@ export {
284
295
  main,
285
296
  verifyCodexxModel
286
297
  };
287
- //# sourceMappingURL=chunk-6ALEIJJM.js.map
298
+ //# sourceMappingURL=chunk-CYR6I4C3.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 // Codex ships a managed network proxy (codex-rs/network-proxy) that routes the\n // agent's traffic through a local proxy on :3128 and enforces a domain allowlist.\n // A host that is not allowlisted — like the local Hoopilot server — gets an instant\n // 403 (the Squid error page) and never reaches Hoopilot. It has two independent\n // gates: the `network_proxy` feature flag and the `permissions.workspace.network`\n // config. Disabling only the feature (`--disable network_proxy`) does not reliably\n // turn it off when the proxy is enabled through the permissions config, so set both\n // off: the feature flag via `--disable`, and the proxy itself via the config key\n // (when `enabled` is false the proxy no-ops and binds no listeners).\n \"--disable\",\n \"network_proxy\",\n \"-c\",\n \"permissions.workspace.network.enabled=false\",\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 managed network proxy (permissions.workspace.network.enabled=false) so requests reach the local server instead of being blocked by its allowlist, 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":[]}
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  safeJsonParse,
16
16
  trimTrailingSlash,
17
17
  truncatedResponseText
18
- } from "./chunk-6ALEIJJM.js";
18
+ } from "./chunk-CYR6I4C3.js";
19
19
 
20
20
  // src/cli.ts
21
21
  import { spawn } from "child_process";
package/dist/codexx.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  buildCodexxInvocation,
4
4
  main,
5
5
  verifyCodexxModel
6
- } from "./chunk-6ALEIJJM.js";
6
+ } from "./chunk-CYR6I4C3.js";
7
7
  export {
8
8
  buildCodexxInvocation,
9
9
  main,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openhoo/hoopilot",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "packageManager": "bun@1.3.14",
5
5
  "description": "OpenAI-compatible local proxy for GitHub Copilot accounts.",
6
6
  "repository": {
@@ -1 +0,0 @@
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":[]}