openzoo 0.51.4 → 0.51.6

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.
Files changed (55) hide show
  1. package/README.md +12 -1
  2. package/bin/openzoo.js +21 -46
  3. package/lib/chatgpt-install.js +106 -0
  4. package/lib/chatgpt.js +108 -90
  5. package/lib/config.js +34 -0
  6. package/lib/modelcommand.js +443 -0
  7. package/lib/pay.js +60 -4
  8. package/lib/proxy.js +39 -0
  9. package/package.json +4 -7
  10. package/transmute/README.md +0 -385
  11. package/transmute/bin/openzoo-transmute.js +0 -7
  12. package/transmute/docs/OPENZOO_CLI_PATCH.md +0 -139
  13. package/transmute/docs/VERCEL_TO_SOLANA.md +0 -381
  14. package/transmute/lib/build.js +0 -381
  15. package/transmute/lib/cli.js +0 -293
  16. package/transmute/lib/compile/bytecode.js +0 -591
  17. package/transmute/lib/compile/hostids.js +0 -11
  18. package/transmute/lib/compile/index.js +0 -159
  19. package/transmute/lib/compile/ir.js +0 -1634
  20. package/transmute/lib/compile/parse.js +0 -753
  21. package/transmute/lib/compile/rust.js +0 -781
  22. package/transmute/lib/deploy.js +0 -357
  23. package/transmute/lib/eligibility.js +0 -211
  24. package/transmute/lib/gateway.js +0 -710
  25. package/transmute/lib/hub.js +0 -290
  26. package/transmute/lib/solana.js +0 -387
  27. package/transmute/lib/vercel.js +0 -296
  28. package/transmute/lib/wallet.js +0 -41
  29. package/transmute/lib/wire.js +0 -208
  30. package/transmute/prebuilt/zoo_vm.v0.so +0 -0
  31. package/transmute/runtime/zoo-host/Cargo.lock +0 -588
  32. package/transmute/runtime/zoo-host/Cargo.toml +0 -30
  33. package/transmute/runtime/zoo-host/examples/vmrun.rs +0 -43
  34. package/transmute/runtime/zoo-host/src/assets.rs +0 -214
  35. package/transmute/runtime/zoo-host/src/ctx.rs +0 -350
  36. package/transmute/runtime/zoo-host/src/fmt.rs +0 -188
  37. package/transmute/runtime/zoo-host/src/helpers.rs +0 -245
  38. package/transmute/runtime/zoo-host/src/json.rs +0 -263
  39. package/transmute/runtime/zoo-host/src/kv.rs +0 -206
  40. package/transmute/runtime/zoo-host/src/lib.rs +0 -137
  41. package/transmute/runtime/zoo-host/src/site.rs +0 -94
  42. package/transmute/runtime/zoo-host/src/val.rs +0 -1050
  43. package/transmute/runtime/zoo-host/src/vm.rs +0 -902
  44. package/transmute/runtime/zoo-host/src/wire.rs +0 -239
  45. package/transmute/runtime/zoo-vm/Cargo.lock +0 -596
  46. package/transmute/runtime/zoo-vm/Cargo.toml +0 -23
  47. package/transmute/runtime/zoo-vm/src/lib.rs +0 -92
  48. package/transmute/site/.env.production +0 -2
  49. package/transmute/site/api/echo.js +0 -10
  50. package/transmute/site/api/hello.js +0 -12
  51. package/transmute/site/api/hits.js +0 -13
  52. package/transmute/site/api/time.js +0 -5
  53. package/transmute/site/package.json +0 -6
  54. package/transmute/site/public/app.js +0 -16
  55. package/transmute/site/public/index.html +0 -96
package/README.md CHANGED
@@ -25,7 +25,18 @@ paid $0.002137 (9.5× cheaper than direct) · rail solana · tx 5Kd…
25
25
 
26
26
  **Claude Code / grokui Auto** — `openzoo claude` sets the Anthropic API key + base URL to the local OpenZoo proxy (x402 pay-per-call). You do **not** need to authenticate Claude first. grokui orange Auto is this harness, not a `RUN:` text parser. PATH `~/.local/bin` is required on Mac so `claude` is found.
27
27
 
28
- **ChatGPT desktop (official Linux/macOS/Windows app)** — `openzoo chatgpt` puts the zoo in as the Codex provider in `~/.codex/config.toml`, starts a proxy if none is up, and launches the app; choose **sign in with API key** (any key) and the Codex / agent side pays x402 per turn. The plain ChatGPT chat tab still talks to chatgpt.com — it is not API-shaped. On Arch or anything without dpkg, `openzoo chatgpt --deb ~/Downloads/chatgpt_amd64.deb` unpacks the official .deb into `~/.openzoo/apps/chatgpt` and runs it from there (a `chatgpt*.deb` in `~/Downloads` is found automatically). `--no-launch` writes config only; `--print` shows the block.
28
+ **ChatGPT desktop (official macOS/Windows/Linux app)** — `openzoo chatgpt` configures the zoo, reuses an installed Codex-enabled ChatGPT app or downloads and installs the current official build for your platform/architecture, starts a background proxy, and launches the app. No TryOmarchy dependency. Choose **sign in with API key** (`sk-openzoo`) if prompted. The Codex side pays x402 per turn from your OpenZoo wallet; ordinary ChatGPT chats still use chatgpt.com.
29
+
30
+ - **macOS (Apple Silicon / Intel):** installs the official DMG into `~/Applications/ChatGPT.app`, verifies its signature, then launches through macOS LaunchServices. Requires a supported macOS version (currently 14+).
31
+ - **Windows (ARM64 / x64):** installs the current official Store-signed MSIX using PowerShell `Add-AppxPackage`, then discovers its executable from the installed package manifest. Windows app-installation policies and package prerequisites still apply.
32
+ - **Linux (ARM64 / x64):** downloads the official `.deb` and unpacks it under `~/.openzoo/apps/chatgpt`, without sudo or dpkg. Requires `bsdtar` or `ar`, plus `tar` and the desktop runtime libraries required by ChatGPT. Ubuntu/Debian/Fedora are upstream-supported; other distributions depend on compatible libraries. `--deb <file>` uses a local package when no app is installed.
33
+
34
+ Configuration defaults to `~/.openzoo/chatgpt/config.toml`, with a separate desktop profile. `CODEX_HOME` overrides that location; `--config /path/to/config.toml` also sets the home used at launch. Unrelated config tables are retained, and the first existing config is saved as `config.toml.openzoo-backup`. Installed apps are reused, not upgraded by this command. Use the app's updater for later updates.
35
+
36
+ `--model <id>` overrides `deepseek-v4-flash`; `--bearer` stores the non-secret placeholder token instead of requiring an environment variable. `--no-launch` writes config only (no download, install, proxy, or launch); `--no-proxy` skips proxy startup; `--print` previews without side effects. Pass app flags after `--`, for example `openzoo chatgpt -- --ozone-platform=wayland`. `OPENZOO_CHATGPT_BIN` selects an explicit executable. Failed downloads, installation, or launch commands return an error.
37
+
38
+ Installer references: [ChatGPT downloads](https://chatgpt.com/download/), [Windows MSIX deployment](https://learn.chatgpt.com/docs/enterprise/windows-deployment), [Linux packages](https://learn.chatgpt.com/docs/linux/linux-app).
39
+
29
40
 
30
41
  **Agent of Empires** (tmux session manager for coding agents, [agent-of-empires](https://github.com/agent-of-empires/agent-of-empires)) — `openzoo aoe` registers an `openzoo` agent in aoe's `config.toml` (`[session.custom_agents] openzoo = "openzoo claude"`, detected as `claude` so status hooks, resume and fork all work), starts a background proxy if none is listening (no public tunnel unless `--tunnel`), then runs `aoe`. `openzoo aoe add . -l` creates and launches one session in the current directory; in the TUI pick **openzoo** from the agent picker. `Alt+z` opens a tool session with the wallet balance and the live receipts log. Terminal view only: aoe's structured view runs `claude-agent-acp` itself, which bills Anthropic, not the zoo. `--override-claude` additionally makes aoe's built-in `claude` agent launch through openzoo; `--no-launch` writes config only.
31
42
 
package/bin/openzoo.js CHANGED
@@ -146,16 +146,17 @@ usage:
146
146
  voice login / voice watch PREHOOK your own outgoing Telegram
147
147
  messages: type raw, the watcher revises in place (userbot;
148
148
  "." prefix sends raw). X has no edit API — no equivalent.
149
- npx openzoo chatgpt OpenAI's ChatGPT desktop app on the zoo. Writes the zoo as
150
- the Codex provider in ~/.codex/config.toml (other tables kept,
151
- old file backed up), starts a background proxy if none is up,
152
- and launches the app — the Codex / agent side pays x402 per
153
- turn; pick "sign in with API key" (any key). The plain chat
154
- tab still talks to chatgpt.com.
155
- Arch / no dpkg: --deb ~/Downloads/chatgpt_amd64.deb unpacks it
156
- (a chatgpt*.deb in ~/Downloads is picked up automatically)
157
- --model <id> (claude-opus-5) --wire responses|chat --bearer
149
+ npx openzoo chatgpt Configure, install if missing, and launch official ChatGPT
150
+ on macOS / Windows / Linux (x64 / ARM64), no TryOmarchy.
151
+ Downloads current official DMG / MSIX / .deb as needed.
152
+ Uses ~/.openzoo/chatgpt/config.toml (or CODEX_HOME), keeps
153
+ other tables and the original backup, starts a local proxy.
154
+ Codex / agent mode pays x402; plain chat uses chatgpt.com.
155
+ Choose "sign in with API key" (sk-openzoo) if prompted.
156
+ --deb <file> (Linux) --config /path/to/config.toml
157
+ --model <id> (deepseek-v4-flash) --wire responses|chat --bearer
158
158
  --no-launch (config only) --no-proxy --tunnel --print
159
+ -- <app flags>
159
160
  npx openzoo openclaw write the zoo into ~/.openclaw/openclaw.json as a model
160
161
  provider WITH REAL PRICES (OpenClaw's own custom-provider
161
162
  path hard-codes $0.00 and ignores /v1/models pricing)
@@ -179,23 +180,12 @@ usage:
179
180
  npx openzoo contexts --forget <hash|all> drop manifest entries
180
181
  npx openzoo balance wallet balance on every rail — Solana (USDC/TOKEN/SOL),
181
182
  Base (USDC/ETH), Robinhood Chain (USDG/memecoins/ETH)
182
- npx openzoo build [dir] transmute a Vercel-shaped app (Next.js pages/api,
183
- app/**/route, Vite + api/*) for Solana. Default
184
- --target shared: bytecode for the shared zoo-vm
185
- runtime, no Rust toolchain, a site is data accounts
186
- (~0.03-0.1 SOL). --target program: own Pinocchio
187
- program per site (--arch v0|v3, ~1 SOL+)
188
- npx openzoo deploy [dir] deploy the site to Solana accounts (--cluster mainnet,
189
- --runtime <zoo-vm id> or OPENZOO_VM_PROGRAM, --yes to
190
- accept the rent estimate; burner wallet)
191
- npx openzoo serve [site] local gateway/explorer for a deployed site (--runtime,
192
- --cluster, --port 4402); /api/* reads are free
193
- simulations, writes are signed by the burner
194
- npx openzoo runtime deploy deploy the shared zoo-vm runtime once per cluster
195
- npx openzoo inspect [dir] print the app in Vercel terms + eligibility report
196
- npx openzoo hub hosted explorer for every transmuted site on the
197
- cluster (what sites.openzoo.fun runs)
198
183
  npx openzoo address print both funding addresses (Solana + EVM)
184
+ npx openzoo model [id] pin the model every chat uses, whatever the app sends
185
+ (no id = show it · reset|off = clear). Same override the
186
+ chat command \`/model <id>\` sets from inside ChatGPT,
187
+ Cursor or Claude Code — a running proxy picks it up
188
+ without a restart.
199
189
  npx openzoo help this text
200
190
 
201
191
  point any OpenAI-compatible harness at:
@@ -433,30 +423,15 @@ async function main() {
433
423
  if (bal <= 0) console.log('buy some with: npx openzoo topup 10');
434
424
  break;
435
425
  }
436
- case 'build':
437
- case 'deploy':
438
- case 'serve':
439
- case 'inspect':
440
- case 'hub':
441
- case 'runtime':
442
- case 'transmute': {
443
- // VERCEL-SHAPED APP -> SOLANA MAINNET. `openzoo build|deploy|serve` hand
444
- // the argv to openzoo-transmute (a sibling npm package): it reads the
445
- // app the way `vercel build` would (pages/api, app/**/route, api/*,
446
- // .vercel/output), transmutes each Lambda into a Pinocchio Rust program
447
- // route, stores the static build in asset accounts, deploys with the
448
- // burner wallet at ~/.openzoo/wallet.json, and `serve` runs the local
449
- // gateway/explorer that maps http://localhost:4402/... onto the program.
450
- const argv = process.argv.slice(2).map((a, i) => (i === 0 && a === 'transmute' ? 'help' : a));
451
- // The transmuter ships inside this package (transmute/): lib/cli.js, the
452
- // compiler, the gateway + hub and the on-chain runtime crate
453
- // (transmute/runtime/zoo-host) all travel with npm — no second install.
454
- await (await import('../transmute/lib/cli.js')).run(argv);
455
- break;
456
- }
457
426
  case 'address':
458
427
  (await import('../lib/info.js')).printAddress();
459
428
  break;
429
+ // Same override file the in-chat `/model` command writes. Two doors on one
430
+ // switch: a harness with no model picker (ChatGPT/Codex) uses the chat
431
+ // command, everyone else can just type it here.
432
+ case 'model':
433
+ await (await import('../lib/modelcommand.js')).cliModel(process.argv[3]);
434
+ break;
460
435
  case 'help':
461
436
  case '--help':
462
437
  case '-h':
@@ -0,0 +1,106 @@
1
+ /** Official current desktop installers; no third-party distribution service. */
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { Readable } from 'node:stream';
7
+ import { pipeline } from 'node:stream/promises';
8
+
9
+ const CDN = 'https://persistent.oaistatic.com/codex-app-prod';
10
+ export function installerAsset(platform = process.platform, arch = process.arch) {
11
+ if (!['x64', 'arm64'].includes(arch)) throw new Error(`ChatGPT has no installer for ${platform}/${arch}; see https://chatgpt.com/download/`);
12
+ if (platform === 'darwin') return { url: `${CDN}/${arch === 'x64' ? 'ChatGPT-latest-x64.dmg' : 'ChatGPT.dmg'}`, name: 'ChatGPT.dmg' };
13
+ if (platform === 'win32') return { url: `${CDN}/ChatGPT-${arch}.msix`, name: 'ChatGPT.msix' };
14
+ if (platform === 'linux') {
15
+ const name = `chatgpt_${arch === 'x64' ? 'amd64' : 'arm64'}.deb`;
16
+ return { url: `${CDN}/linux/deb/latest/${name}`, name };
17
+ }
18
+ throw new Error(`ChatGPT desktop is not available for ${platform}; see https://chatgpt.com/download/`);
19
+ }
20
+
21
+ export function runCommand(command, args, options = {}) {
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], ...options });
24
+ let stdout = '', stderr = '';
25
+ child.stdout?.on('data', b => { stdout = (stdout + b).slice(-1024 * 1024); });
26
+ child.stderr?.on('data', b => { stderr = (stderr + b).slice(-8192); });
27
+ child.once('error', reject);
28
+ child.once('close', code => code === 0 ? resolve(stdout.trim()) : reject(new Error(`${command} exited ${code}: ${stderr.trim()}`)));
29
+ });
30
+ }
31
+
32
+ export async function downloadInstaller(url, file, fetchImpl = fetch) {
33
+ const partial = `${file}.part`;
34
+ try {
35
+ const response = await fetchImpl(url, { signal: AbortSignal.timeout(15 * 60_000) });
36
+ if (!response.ok || !response.body) throw new Error(`download failed (${response.status}): ${url}`);
37
+ if (/text\/html/.test(response.headers.get('content-type') || '')) throw new Error(`installer URL returned HTML: ${url}`);
38
+ await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(partial, { flags: 'wx' }));
39
+ if (!fs.statSync(partial).size) throw new Error('downloaded installer is empty');
40
+ fs.renameSync(partial, file);
41
+ } finally {
42
+ fs.rmSync(partial, { force: true });
43
+ }
44
+ }
45
+
46
+ // Query the installed package manifest instead of guessing a versioned WindowsApps path.
47
+ // The bundled Codex engine distinguishes the combined app from ChatGPT Classic.
48
+ export async function windowsChatGptApp(run = runCommand) {
49
+ const script = `$ErrorActionPreference = 'Stop'
50
+ Get-AppxPackage | Where-Object { $_.Name -match 'OpenAI|ChatGPT|Codex' } | ForEach-Object {
51
+ $pkg = $_
52
+ $manifest = Get-AppxPackageManifest -Package $pkg.PackageFullName
53
+ foreach ($entry in $manifest.Package.Applications.Application) {
54
+ if ($entry.Executable) {
55
+ $exe = Join-Path $pkg.InstallLocation $entry.Executable
56
+ if ((Test-Path -LiteralPath $exe) -and (Test-Path -LiteralPath (Join-Path (Split-Path $exe) 'resources/codex.exe'))) { Write-Output $exe }
57
+ }
58
+ }
59
+ }`;
60
+ const output = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]);
61
+ return output.split(/\r?\n/).map(s => s.trim()).find(Boolean) || null;
62
+ }
63
+
64
+ export async function installChatGpt({ platform = process.platform, arch = process.arch, home = os.homedir(),
65
+ run = runCommand, download = downloadInstaller, unpack, findWindows = windowsChatGptApp, log = console.log } = {}) {
66
+ const asset = installerAsset(platform, arch);
67
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'openzoo-chatgpt-'));
68
+ const file = path.join(scratch, asset.name);
69
+ try {
70
+ log(`downloading current ChatGPT for ${platform}/${arch} from ${asset.url}`);
71
+ await download(asset.url, file);
72
+ if (platform === 'linux') return unpack(file, path.join(home, '.openzoo', 'apps', 'chatgpt'));
73
+ if (platform === 'win32') {
74
+ // The path is passed through the environment, never interpolated into PowerShell code.
75
+ await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
76
+ "$ErrorActionPreference = 'Stop'; Add-AppxPackage -Path $env:OPENZOO_CHATGPT_INSTALLER"],
77
+ { env: { ...process.env, OPENZOO_CHATGPT_INSTALLER: file } });
78
+ const app = await findWindows(run);
79
+ if (!app) throw new Error('ChatGPT MSIX installed but its desktop executable was not found');
80
+ return app;
81
+ }
82
+ const mount = path.join(scratch, 'mount');
83
+ fs.mkdirSync(mount);
84
+ let mounted = false;
85
+ try {
86
+ await run('hdiutil', ['attach', '-nobrowse', '-readonly', '-mountpoint', mount, file]);
87
+ mounted = true;
88
+ const source = path.join(mount, 'ChatGPT.app');
89
+ // Let macOS validate the vendor signature before copying executable code.
90
+ await run('codesign', ['--verify', '--deep', '--strict', source]);
91
+ if (!fs.existsSync(path.join(source, 'Contents', 'Resources', 'codex'))) throw new Error('installer contains ChatGPT Classic, not the Codex-enabled desktop app');
92
+ const applications = path.join(home, 'Applications');
93
+ fs.mkdirSync(applications, { recursive: true });
94
+ const destination = path.join(applications, 'ChatGPT.app');
95
+ if (fs.existsSync(destination)) throw new Error(`${destination} already exists but is not usable; move it aside or set OPENZOO_CHATGPT_BIN`);
96
+ const staged = path.join(applications, `.openzoo-chatgpt-${process.pid}-${Date.now()}.app`);
97
+ try {
98
+ await run('ditto', [source, staged]);
99
+ fs.renameSync(staged, destination);
100
+ } finally { fs.rmSync(staged, { recursive: true, force: true }); }
101
+ return path.join(destination, 'Contents', 'MacOS', 'ChatGPT');
102
+ } finally {
103
+ if (mounted) await run('hdiutil', ['detach', mount]);
104
+ }
105
+ } finally { fs.rmSync(scratch, { recursive: true, force: true }); }
106
+ }
package/lib/chatgpt.js CHANGED
@@ -1,38 +1,17 @@
1
1
  /**
2
2
  * `npx openzoo chatgpt` — OpenAI's ChatGPT desktop app on the zoo.
3
3
  *
4
- * The Linux preview of the official app (chatgpt.com/download, August 2026)
5
- * ships the Codex engine inside it: `usr/lib/chatgpt/resources/codex`, and
6
- * `usr/bin/chatgpt` is a symlink named `codex-launcher`. That engine reads a
7
- * standard Codex config — `$CODEX_HOME/config.toml`, default `~/.codex` — and
8
- * its `[model_providers.<id>]` table is the supported way to point it at any
9
- * OpenAI-shaped server. MEASURED 2026-09-09 on the .deb's `codex` binary:
10
- * the strings `model_providers`, `env_key`, `requires_openai_auth`,
11
- * `experimental_bearer_token`, `responses` and `https://api.openai.com/v1`
12
- * (as a default, next to `ollama` and `amazon-bedrock`) are all there, and
13
- * a provider block pointed at :8402 answered in the app.
14
- *
15
- * What this does and does not redirect, so nobody is surprised:
16
- * - the Codex / agent side of the app ("sign in with API key") → the zoo,
17
- * paid per turn by x402 through the local proxy;
18
- * - the plain ChatGPT conversation tab → still chatgpt.com. It talks to
19
- * `chatgpt_base_url = "https://chatgpt.com/backend-api/"`, a private
20
- * backend that is not API-shaped, so there is nothing to point at us.
21
- * - the two auth modes are exclusive: the binary logs "ChatGPT login is
22
- * required, but an API key is currently being used. Logging out." Pick
23
- * API-key mode for the session that should pay x402.
24
- *
25
- * One command, end to end: write the provider into config.toml (backing up
26
- * the old file, keeping every other table), make sure a proxy is listening,
27
- * find the app — or unpack the downloaded .deb on a distro that cannot
28
- * install it, which is how this was first done on Arch — and launch it with
29
- * the env var the provider's `env_key` names.
4
+ * Configures an isolated Codex home, installs the current official app when
5
+ * missing, starts the local proxy and launches the app with that configuration.
6
+ * Custom provider routing applies to Codex; ordinary ChatGPT chats still use
7
+ * chatgpt.com. No TryOmarchy or third-party app distribution is involved.
30
8
  */
31
9
  import { spawn, spawnSync } from 'node:child_process';
32
10
  import fs from 'node:fs';
33
11
  import os from 'node:os';
34
12
  import path from 'node:path';
35
13
  import { config } from './config.js';
14
+ import { installChatGpt, windowsChatGptApp, runCommand } from './chatgpt-install.js';
36
15
 
37
16
  export const PROVIDER_ID = 'openzoo';
38
17
  export const API_KEY_ENV = 'OPENZOO_API_KEY';
@@ -48,8 +27,15 @@ export function codexConfigPath(env = process.env, home = os.homedir()) {
48
27
  }
49
28
 
50
29
  export function defaultModel(env = process.env) {
30
+ // DEEPSEEK BY DEFAULT. The person running `openzoo chatgpt` has not chosen
31
+ // a model and mostly will not: the default is what they pay for on every
32
+ // turn until they type /model. It used to be claude-opus-5, the most
33
+ // expensive door in the catalog, so the "much less spend" pitch shipped
34
+ // with the priciest setting turned on. deepseek-v4-flash is a fraction of
35
+ // a cent per million tokens in, carries a 1M-token window, and is a bare
36
+ // id the doors serve directly. Anyone who wants more says /model.
51
37
  // Bare ids are what the doors serve; the gateway resolves the rest.
52
- return env.OPENZOO_DEFAULT_MODEL || 'claude-opus-5';
38
+ return env.OPENZOO_DEFAULT_MODEL || 'deepseek-v4-flash';
53
39
  }
54
40
 
55
41
  /**
@@ -113,7 +99,7 @@ export function mergeCodexConfig(existing, { model, port, wireApi, bearer } = {}
113
99
  });
114
100
  const head = [
115
101
  OUR_MARK,
116
- `model = "${model || defaultModel()}"`,
102
+ `model = ${JSON.stringify(model || defaultModel())}`,
117
103
  `model_provider = "${PROVIDER_ID}"`,
118
104
  ];
119
105
  // trim blank padding so reruns do not accumulate empty lines
@@ -134,14 +120,15 @@ export function mergeCodexConfig(existing, { model, port, wireApi, bearer } = {}
134
120
 
135
121
  /** Candidate app binaries, most specific first. Exported for tests. */
136
122
  export function chatGptCandidates({ env = process.env, home = os.homedir(), platform = process.platform } = {}) {
123
+ const paths = platform === 'win32' ? path.win32 : path;
137
124
  const out = [];
138
125
  if (env.OPENZOO_CHATGPT_BIN) out.push(env.OPENZOO_CHATGPT_BIN);
139
126
  if (platform === 'darwin') {
140
127
  out.push('/Applications/ChatGPT.app/Contents/MacOS/ChatGPT');
141
128
  out.push(path.join(home, 'Applications', 'ChatGPT.app', 'Contents', 'MacOS', 'ChatGPT'));
142
129
  } else if (platform === 'win32') {
143
- const local = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
144
- out.push(path.join(local, 'Programs', 'ChatGPT', 'ChatGPT.exe'));
130
+ const local = env.LOCALAPPDATA || paths.join(home, 'AppData', 'Local');
131
+ out.push(paths.join(local, 'Programs', 'ChatGPT', 'ChatGPT.exe'));
145
132
  } else {
146
133
  // the .deb's layout (apt install), then where THIS command unpacks it,
147
134
  // then the by-hand unpack path the first Arch run used
@@ -150,15 +137,21 @@ export function chatGptCandidates({ env = process.env, home = os.homedir(), plat
150
137
  out.push(path.join(unpackDir(home), 'usr', 'lib', 'chatgpt', 'ChatGPT'));
151
138
  out.push(path.join(home, 'apps', 'chatgpt', 'usr', 'lib', 'chatgpt', 'ChatGPT'));
152
139
  }
153
- for (const dir of String(env.PATH || '').split(path.delimiter).filter(Boolean)) {
154
- out.push(path.join(dir, platform === 'win32' ? 'chatgpt.exe' : 'chatgpt'));
140
+ for (const dir of String(env.PATH || '').split(platform === 'win32' ? ';' : ':').filter(Boolean)) {
141
+ out.push(paths.join(dir, platform === 'win32' ? 'chatgpt.exe' : 'chatgpt'));
155
142
  }
156
143
  return out;
157
144
  }
158
145
 
159
146
  export function resolveChatGptApp(opts = {}) {
160
147
  const exists = opts.exists || ((p) => { try { fs.accessSync(p, fs.constants.X_OK); return true; } catch { return false; } });
161
- return chatGptCandidates(opts).find(exists) || null;
148
+ return chatGptCandidates(opts).find(p => {
149
+ if (!exists(p)) return false;
150
+ if ((opts.platform || process.platform) === 'darwin' && p.endsWith('/Contents/MacOS/ChatGPT')) {
151
+ return exists(path.resolve(path.dirname(p), '../Resources/codex'));
152
+ }
153
+ return true;
154
+ }) || null;
162
155
  }
163
156
 
164
157
  /** Where `--deb` (or an auto-found download) is unpacked. */
@@ -190,27 +183,35 @@ function have(bin) {
190
183
  */
191
184
  export function unpackDeb(deb, dest = unpackDir()) {
192
185
  if (!fs.existsSync(deb)) throw new Error(`no such file: ${deb}`);
193
- fs.rmSync(dest, { recursive: true, force: true });
194
- fs.mkdirSync(dest, { recursive: true });
186
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
195
187
  const outer = fs.mkdtempSync(path.join(os.tmpdir(), 'openzoo-deb-'));
188
+ const staged = fs.mkdtempSync(path.join(path.dirname(dest), '.chatgpt-unpack-'));
196
189
  const run = (cmd, args, cwd) => {
197
190
  const r = spawnSync(cmd, args, { cwd, stdio: ['ignore', 'ignore', 'pipe'] });
198
- if (r.status !== 0) throw new Error(`${cmd} ${args.join(' ')} failed: ${String(r.stderr || '').trim().slice(0, 200)}`);
191
+ if (r.status !== 0) throw new Error(`${cmd} failed: ${r.error?.message || String(r.stderr || '').trim().slice(0, 200)}`);
199
192
  };
200
- if (have('bsdtar')) run('bsdtar', ['-xf', deb], outer);
201
- else if (have('ar')) run('ar', ['x', deb], outer);
202
- else throw new Error('need bsdtar (libarchive) or ar (binutils) to unpack a .deb');
203
- const data = fs.readdirSync(outer).find((n) => /^data\.tar/.test(n));
204
- if (!data) throw new Error('not a .deb: no data.tar inside');
205
- run('tar', ['-xf', path.join(outer, data), '-C', dest]);
206
- fs.rmSync(outer, { recursive: true, force: true });
207
- const app = path.join(dest, 'usr', 'lib', 'chatgpt', 'ChatGPT');
208
- if (!fs.existsSync(app)) throw new Error(`unpacked, but ${app} is not there — not the ChatGPT .deb?`);
209
- return app;
193
+ try {
194
+ if (have('bsdtar')) run('bsdtar', ['-xf', deb], outer);
195
+ else if (have('ar')) run('ar', ['x', deb], outer);
196
+ else throw new Error('need bsdtar (libarchive) or ar (binutils) to unpack a .deb');
197
+ const data = fs.readdirSync(outer).find((n) => /^data\.tar/.test(n));
198
+ if (!data) throw new Error('not a .deb: no data.tar inside');
199
+ run('tar', ['-xf', path.join(outer, data), '-C', staged]);
200
+ const relative = path.join('usr', 'lib', 'chatgpt', 'ChatGPT');
201
+ if (!fs.existsSync(path.join(staged, relative))) throw new Error('package does not contain the ChatGPT desktop executable');
202
+ fs.accessSync(path.join(staged, relative), fs.constants.X_OK);
203
+ // Do not erase a previous unpack until the replacement has been validated.
204
+ fs.rmSync(dest, { recursive: true, force: true });
205
+ fs.renameSync(staged, dest);
206
+ return path.join(dest, relative);
207
+ } finally {
208
+ fs.rmSync(outer, { recursive: true, force: true });
209
+ fs.rmSync(staged, { recursive: true, force: true });
210
+ }
210
211
  }
211
212
 
212
213
  async function ensureProxy({ tunnel }) {
213
- const { startProxy, oursOn, packageVersion } = await import('./proxy.js');
214
+ const { oursOn, packageVersion } = await import('./proxy.js');
214
215
  if (await oursOn(config.port)) return { started: false };
215
216
  // DETACHED, like `openzoo aoe`: the app outlives this command, so the
216
217
  // proxy must too. No public tunnel unless asked — a detached proxy nobody
@@ -237,26 +238,27 @@ async function ensureProxy({ tunnel }) {
237
238
  throw new Error(`proxy did not answer on :${config.port} within 60s — see ${logPath}`);
238
239
  }
239
240
 
240
- const INSTALL_HELP = `openzoo: ChatGPT desktop app not found.
241
- download it from https://chatgpt.com/download (Linux: the .deb; macOS/Windows: the installer), then:
242
- Debian/Ubuntu: sudo apt install ./chatgpt_amd64.deb
243
- Fedora: sudo dnf install ./chatgpt*.rpm
244
- Arch / other: npx openzoo chatgpt --deb ~/Downloads/chatgpt_amd64.deb (unpacked, no dpkg needed)
245
- or point OPENZOO_CHATGPT_BIN at the binary.`;
246
-
247
241
  /**
248
242
  * `npx openzoo chatgpt [--model <id>] [--deb <file>] [--wire responses|chat]
249
243
  * [--bearer] [--no-launch] [--no-proxy] [--tunnel] [--print]`
250
244
  */
251
- export async function setupChatGpt(argv = []) {
245
+ export async function setupChatGpt(argv = [], dependencies = {}) {
246
+ const { platform = process.platform, home = os.homedir(), env: sourceEnv = process.env,
247
+ resolve = resolveChatGptApp, install = installChatGpt, proxy = ensureProxy,
248
+ launch = launchChatGpt, findWindows = windowsChatGptApp } = dependencies;
252
249
  const args = [...argv];
253
250
  const opt = { model: null, deb: null, wireApi: 'responses', bearer: false, launch: true, proxy: true, tunnel: false, print: false, configPath: null, rest: [] };
254
251
  while (args.length) {
255
252
  const a = args.shift();
256
- if (a === '--model') opt.model = String(args.shift() || '').trim() || null;
257
- else if (a === '--deb') opt.deb = String(args.shift() || '').trim() || null;
258
- else if (a === '--wire') opt.wireApi = String(args.shift() || '').trim();
259
- else if (a === '--config') opt.configPath = String(args.shift() || '').trim() || null;
253
+ if (a === '--') { opt.rest.push(...args); break; }
254
+ const value = () => {
255
+ if (!args.length || args[0].startsWith('--') || !args[0].trim()) throw new Error(`${a} requires a value`);
256
+ return args.shift().trim();
257
+ };
258
+ if (a === '--model') opt.model = value();
259
+ else if (a === '--deb') opt.deb = value();
260
+ else if (a === '--wire') opt.wireApi = value();
261
+ else if (a === '--config') opt.configPath = value();
260
262
  else if (a === '--bearer') opt.bearer = true;
261
263
  else if (a === '--no-launch') opt.launch = false;
262
264
  else if (a === '--no-proxy') opt.proxy = false;
@@ -266,7 +268,8 @@ export async function setupChatGpt(argv = []) {
266
268
  else opt.rest.push(a);
267
269
  }
268
270
  if (!['responses', 'chat'].includes(opt.wireApi)) throw new Error('--wire must be responses or chat');
269
- const model = opt.model || defaultModel();
271
+ if (opt.deb && platform !== 'linux') throw new Error('--deb is only supported on Linux');
272
+ const model = opt.model || defaultModel(sourceEnv);
270
273
  const merge = { model, port: config.port, wireApi: opt.wireApi, bearer: opt.bearer };
271
274
 
272
275
  if (opt.print) {
@@ -275,54 +278,69 @@ export async function setupChatGpt(argv = []) {
275
278
  }
276
279
 
277
280
  // 1. config.toml
278
- const file = opt.configPath || codexConfigPath();
281
+ const appHome = sourceEnv.CODEX_HOME || path.join(home, '.openzoo', 'chatgpt');
282
+ const file = path.resolve(opt.configPath || path.join(appHome, 'config.toml'));
283
+ if (path.basename(file) !== 'config.toml') throw new Error('--config must name config.toml (the filename Codex reads)');
279
284
  let existing = '';
280
285
  if (fs.existsSync(file)) {
281
286
  existing = fs.readFileSync(file, 'utf8');
282
- fs.copyFileSync(file, `${file}.openzoo-backup`);
283
287
  } else {
284
288
  fs.mkdirSync(path.dirname(file), { recursive: true });
285
289
  }
286
290
  const next = mergeCodexConfig(existing, merge);
287
- if (next !== existing) fs.writeFileSync(file, next);
288
- console.log(`${next !== existing ? 'wrote' : 'kept'} ${file}: model_provider = "${PROVIDER_ID}" (http://localhost:${config.port}/v1, ${opt.wireApi}), model = "${model}"`);
289
-
290
- // 2. proxy
291
- if (opt.proxy) {
292
- const p = await ensureProxy({ tunnel: opt.tunnel });
293
- console.log(p.started ? `proxy up on :${config.port} (background)` : `proxy already on :${config.port}`);
291
+ if (next !== existing) {
292
+ if (existing && !fs.existsSync(`${file}.openzoo-backup`)) fs.copyFileSync(file, `${file}.openzoo-backup`);
293
+ fs.writeFileSync(file, next);
294
294
  }
295
+ console.log(`${next !== existing ? 'wrote' : 'kept'} ${file}: model_provider = "${PROVIDER_ID}" (http://localhost:${config.port}/v1, ${opt.wireApi}), model = "${model}"`);
295
296
 
296
297
  if (!opt.launch) {
297
- console.log(`config only. launch the app with ${API_KEY_ENV}=${API_KEY_VALUE} in its environment and pick "sign in with API key".`);
298
+ console.log('config only; no download, installation, proxy or app launch.');
298
299
  return { file, model, launched: false };
299
300
  }
300
301
 
301
- // 3. the app
302
- let app = resolveChatGptApp();
303
- if (!app && process.platform === 'linux') {
304
- const deb = opt.deb || findDownloadedDeb();
305
- if (deb) {
306
- console.log(`unpacking ${deb} into ${unpackDir()} (no dpkg needed)`);
307
- app = unpackDeb(deb);
308
- }
309
- } else if (opt.deb) {
310
- app = unpackDeb(opt.deb);
311
- }
312
- if (!app) {
313
- console.error(INSTALL_HELP);
314
- process.exitCode = 1;
315
- return { file, model, launched: false };
302
+ // Reuse an installation before attempting any download.
303
+ let app = resolve({ env: sourceEnv, home, platform });
304
+ if (!app && sourceEnv.OPENZOO_CHATGPT_BIN) throw new Error(`OPENZOO_CHATGPT_BIN is not an executable ChatGPT app: ${sourceEnv.OPENZOO_CHATGPT_BIN}`);
305
+ if (!app && platform === 'win32') app = await findWindows();
306
+ if (!app && platform === 'linux' && opt.deb) app = unpackDeb(path.resolve(opt.deb), unpackDir(home));
307
+ if (!app) app = await install({ platform, home, unpack: unpackDeb });
308
+
309
+ if (opt.proxy) {
310
+ const p = await proxy({ tunnel: opt.tunnel });
311
+ console.log(p.started ? `proxy up on :${config.port} (background)` : `proxy already on :${config.port}`);
316
312
  }
317
313
 
318
314
  // 4. launch, with the env var the provider's env_key names
319
- const env = { ...process.env, [API_KEY_ENV]: process.env[API_KEY_ENV] || API_KEY_VALUE };
320
- if (!env.CODEX_HOME) env.CODEX_HOME = codexHome();
315
+ const env = { ...sourceEnv, [API_KEY_ENV]: sourceEnv[API_KEY_ENV] || API_KEY_VALUE,
316
+ CODEX_HOME: path.dirname(file),
317
+ CODEX_ELECTRON_USER_DATA_PATH: path.join(path.dirname(file), 'desktop'),
318
+ };
321
319
  console.log(`launching ${app}`);
322
320
  console.log(` Codex / agent mode → the zoo, paid per turn by x402. Choose "sign in with API key" (any key).`);
323
321
  console.log(' the plain ChatGPT chat tab still talks to chatgpt.com — that half is not API-shaped.');
324
- const child = spawn(app, opt.rest, { stdio: 'ignore', env, detached: true });
325
- child.on('error', (e) => console.error(`openzoo: could not launch ${app}: ${e.message}`));
326
- child.unref();
322
+ await launch(app, opt.rest, { env, platform });
327
323
  return { file, model, app, launched: true };
328
324
  }
325
+
326
+ /** LaunchServices handles app bundles on macOS; detect early failures elsewhere. */
327
+ export async function launchChatGpt(app, args, { env, platform = process.platform, run = runCommand, spawnImpl = spawn } = {}) {
328
+ if (platform === 'darwin' && app.endsWith('/Contents/MacOS/ChatGPT')) {
329
+ await run('open', ['-n', '--env', `CODEX_HOME=${env.CODEX_HOME}`,
330
+ '--env', `CODEX_ELECTRON_USER_DATA_PATH=${env.CODEX_ELECTRON_USER_DATA_PATH}`,
331
+ '--env', `${API_KEY_ENV}=${env[API_KEY_ENV]}`,
332
+ app.slice(0, -'/Contents/MacOS/ChatGPT'.length), '--args',
333
+ `--user-data-dir=${env.CODEX_ELECTRON_USER_DATA_PATH}`, ...args], { env });
334
+ return;
335
+ }
336
+ await new Promise((resolve, reject) => {
337
+ const child = spawnImpl(app, [`--user-data-dir=${env.CODEX_ELECTRON_USER_DATA_PATH}`, ...args], { stdio: 'ignore', env, detached: true });
338
+ const timer = setTimeout(() => { child.unref(); resolve(); }, 1500);
339
+ child.once('error', e => { clearTimeout(timer); reject(e); });
340
+ child.once('exit', (code, signal) => {
341
+ clearTimeout(timer);
342
+ if (code === 0) resolve(); // an existing instance accepted the launch
343
+ else reject(new Error(`ChatGPT exited during launch (${signal || code})`));
344
+ });
345
+ });
346
+ }
package/lib/config.js CHANGED
@@ -80,6 +80,40 @@ export const EVM_FUNDING_ASSETS = {
80
80
  ],
81
81
  };
82
82
 
83
+ /**
84
+ * RPC endpoint BY CHAIN ID — the only lookup a 402 row can be resolved with.
85
+ *
86
+ * A row names its chain as `eip155:<id>` and nothing else. The rail NAMES
87
+ * above are a coarser bucket: railOf() collapses every chain that is not Base
88
+ * or Robinhood into the single name "evm", so a lookup keyed by rail name
89
+ * cannot tell Polygon from Arbitrum and answers with whatever the fallback
90
+ * branch happens to be.
91
+ *
92
+ * MEASURED 2026-09-09: the payment preflight did exactly that — for any
93
+ * non-Base EVM row it read the balance over the ROBINHOOD RPC, asked a Polygon
94
+ * USDC address that is not a contract there, threw, was swallowed by the
95
+ * preflight's catch, and signed an authorization for a chain the wallet held
96
+ * nothing on. The facilitator then reverted it and the caller was told
97
+ * "execution reverted" while 76 USDC sat on Base. Keyed by chain id, that
98
+ * cannot happen: either we can read the chain, or we say we cannot.
99
+ */
100
+ export const EVM_RPC_BY_CHAIN = {
101
+ 8453: () => config.baseRpcUrl,
102
+ 4663: () => config.rhRpcUrl,
103
+ 137: () => process.env.OPENZOO_POLYGON_RPC || 'https://polygon-rpc.com',
104
+ 42161: () => process.env.OPENZOO_ARBITRUM_RPC || 'https://arb1.arbitrum.io/rpc',
105
+ 10: () => process.env.OPENZOO_OPTIMISM_RPC || 'https://mainnet.optimism.io',
106
+ 480: () => process.env.OPENZOO_WORLD_RPC || 'https://worldchain-mainnet.gateway.tenderly.co',
107
+ };
108
+
109
+ /** RPC for a row's `eip155:<id>` network, or null when we cannot read that chain. */
110
+ export function evmRpcForNetwork(network) {
111
+ const m = /^eip155:(\d+)$/.exec(String(network || ''));
112
+ if (!m) return null;
113
+ const f = EVM_RPC_BY_CHAIN[Number(m[1])];
114
+ return f ? f() : null;
115
+ }
116
+
83
117
  /** RPC endpoint for an EVM rail name. */
84
118
  export function evmRpcFor(rail) {
85
119
  // A rail with no RPC here reports NO balance at all — silently. Every entry