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.
- package/README.md +12 -1
- package/bin/openzoo.js +21 -46
- package/lib/chatgpt-install.js +106 -0
- package/lib/chatgpt.js +108 -90
- package/lib/config.js +34 -0
- package/lib/modelcommand.js +443 -0
- package/lib/pay.js +60 -4
- package/lib/proxy.js +39 -0
- package/package.json +4 -7
- package/transmute/README.md +0 -385
- package/transmute/bin/openzoo-transmute.js +0 -7
- package/transmute/docs/OPENZOO_CLI_PATCH.md +0 -139
- package/transmute/docs/VERCEL_TO_SOLANA.md +0 -381
- package/transmute/lib/build.js +0 -381
- package/transmute/lib/cli.js +0 -293
- package/transmute/lib/compile/bytecode.js +0 -591
- package/transmute/lib/compile/hostids.js +0 -11
- package/transmute/lib/compile/index.js +0 -159
- package/transmute/lib/compile/ir.js +0 -1634
- package/transmute/lib/compile/parse.js +0 -753
- package/transmute/lib/compile/rust.js +0 -781
- package/transmute/lib/deploy.js +0 -357
- package/transmute/lib/eligibility.js +0 -211
- package/transmute/lib/gateway.js +0 -710
- package/transmute/lib/hub.js +0 -290
- package/transmute/lib/solana.js +0 -387
- package/transmute/lib/vercel.js +0 -296
- package/transmute/lib/wallet.js +0 -41
- package/transmute/lib/wire.js +0 -208
- package/transmute/prebuilt/zoo_vm.v0.so +0 -0
- package/transmute/runtime/zoo-host/Cargo.lock +0 -588
- package/transmute/runtime/zoo-host/Cargo.toml +0 -30
- package/transmute/runtime/zoo-host/examples/vmrun.rs +0 -43
- package/transmute/runtime/zoo-host/src/assets.rs +0 -214
- package/transmute/runtime/zoo-host/src/ctx.rs +0 -350
- package/transmute/runtime/zoo-host/src/fmt.rs +0 -188
- package/transmute/runtime/zoo-host/src/helpers.rs +0 -245
- package/transmute/runtime/zoo-host/src/json.rs +0 -263
- package/transmute/runtime/zoo-host/src/kv.rs +0 -206
- package/transmute/runtime/zoo-host/src/lib.rs +0 -137
- package/transmute/runtime/zoo-host/src/site.rs +0 -94
- package/transmute/runtime/zoo-host/src/val.rs +0 -1050
- package/transmute/runtime/zoo-host/src/vm.rs +0 -902
- package/transmute/runtime/zoo-host/src/wire.rs +0 -239
- package/transmute/runtime/zoo-vm/Cargo.lock +0 -596
- package/transmute/runtime/zoo-vm/Cargo.toml +0 -23
- package/transmute/runtime/zoo-vm/src/lib.rs +0 -92
- package/transmute/site/.env.production +0 -2
- package/transmute/site/api/echo.js +0 -10
- package/transmute/site/api/hello.js +0 -12
- package/transmute/site/api/hits.js +0 -13
- package/transmute/site/api/time.js +0 -5
- package/transmute/site/package.json +0 -6
- package/transmute/site/public/app.js +0 -16
- 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
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
--model <id> (
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 || '
|
|
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 =
|
|
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 ||
|
|
144
|
-
out.push(
|
|
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(
|
|
154
|
-
out.push(
|
|
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(
|
|
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.
|
|
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} ${
|
|
191
|
+
if (r.status !== 0) throw new Error(`${cmd} failed: ${r.error?.message || String(r.stderr || '').trim().slice(0, 200)}`);
|
|
199
192
|
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 {
|
|
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 === '--
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
288
|
-
|
|
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(
|
|
298
|
+
console.log('config only; no download, installation, proxy or app launch.');
|
|
298
299
|
return { file, model, launched: false };
|
|
299
300
|
}
|
|
300
301
|
|
|
301
|
-
//
|
|
302
|
-
let app =
|
|
303
|
-
if (!app &&
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 = { ...
|
|
320
|
-
|
|
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
|
-
|
|
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
|