openzoo 0.50.78 → 0.50.79

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
@@ -25,6 +25,10 @@ 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
+ **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.
29
+
30
+ **Orca** ([stablyai/orca](https://github.com/stablyai/orca), the desktop orchestrator for parallel agents) — Orca launches any CLI agent in a worktree terminal, so type `openzoo claude` as the agent command (or pick **openzoo** from the agent picker once the upstream catalog entry lands). Orca treats it as Claude Code: same process, same hooks and status dots, same `--dangerously-skip-permissions` default. No account switching applies, because nothing is logged in; every turn pays x402.
31
+
28
32
  `GET /v1/models` is the live OpenRouter catalog **after** dropping `:batch`, `$0` / missing prices, and `openzoo-*` twins. Claude Code's `/model` picker (Anthropic-shaped GET) is a short list: current Opus/Sonnet/Haiku-class + a few real gateway models + `openzoo/auto`.
29
33
 
30
34
  Mac:
package/bin/openzoo.js CHANGED
@@ -142,6 +142,15 @@ usage:
142
142
  path hard-codes $0.00 and ignores /v1/models pricing)
143
143
  --all (whole catalog) --models a,b (exact ids)
144
144
  --default <id> (set the agents' primary model)
145
+ npx openzoo aoe [aoe args] Agent of Empires (tmux session manager for coding
146
+ agents): registers "openzoo" as an agent in aoe's
147
+ config.toml (= Claude Code on the zoo, detected as
148
+ claude so status/resume work), starts a background
149
+ proxy if none is up, then runs aoe.
150
+ aoe add . -l one session in this dir, launched
151
+ --no-launch (config only) --no-proxy --tunnel
152
+ --override-claude (aoe's built-in claude pays via
153
+ the zoo too) --no-receipts (skip the Alt+z tool)
145
154
  npx openzoo mcp stdio MCP server (tools: zoo_ask, zoo_bind, zoo_models, zoo_wallet, zoo_contexts)
146
155
  npx openzoo unblock restore the editor's own backend in the hosts file
147
156
  npx openzoo tunnel public-url-only mode (everything key-gated, no keyless localhost)
@@ -201,6 +210,9 @@ async function main() {
201
210
  case 'openclaw':
202
211
  await (await import('../lib/openclaw.js')).setupOpenClaw(process.argv.slice(3));
203
212
  break;
213
+ case 'aoe':
214
+ await (await import('../lib/aoe.js')).setupAoe(process.argv.slice(3));
215
+ break;
204
216
  case 'voice':
205
217
  await (await import('../lib/voice.js')).runVoice(process.argv.slice(3));
206
218
  break;
package/lib/aoe.js ADDED
@@ -0,0 +1,314 @@
1
+ /**
2
+ * `npx openzoo aoe [aoe args...]` — register openzoo as an agent in Agent of
3
+ * Empires (https://github.com/agent-of-empires/agent-of-empires), make sure a
4
+ * proxy is up for the sessions it will spawn, then hand off to `aoe`.
5
+ *
6
+ * AoE runs each coding agent in its own tmux session and picks the agent from
7
+ * a registry it ships (`aoe agents`). openzoo is not in that registry, but AoE
8
+ * has a first-class hook for exactly this shape: `[session.custom_agents]`
9
+ * names a command, and `[session.agent_detect_as]` tells AoE which built-in
10
+ * the command wraps so status hooks, resume flags and the structured-view
11
+ * adapter are inherited. `openzoo claude` IS Claude Code (it spawns the real
12
+ * CLI with ANTHROPIC_BASE_URL pointed at the local proxy and forwards every
13
+ * flag untouched), so `openzoo = "openzoo claude"` + `openzoo = "claude"` is
14
+ * the whole integration: `aoe add --tool openzoo` launches a Claude Code
15
+ * session that pays per turn over x402 with no API key and no account.
16
+ *
17
+ * Everything written is a surgical edit of the user's config.toml: keys are
18
+ * inserted into the tables they belong to (header form or inline form,
19
+ * whichever the file already uses), never a second `[session.custom_agents]`
20
+ * header, which TOML rejects as a duplicate table and would take the whole
21
+ * config down with it. Re-running is idempotent.
22
+ */
23
+ import { spawn, spawnSync } from 'node:child_process';
24
+ import fs from 'node:fs';
25
+ import os from 'node:os';
26
+ import path from 'node:path';
27
+ import { config } from './config.js';
28
+
29
+ export const AGENT_NAME = 'openzoo';
30
+ export const TOOL_NAME = 'openzoo';
31
+
32
+ /**
33
+ * Where AoE reads its global config.toml. Mirrors `get_app_dir_path` in
34
+ * agent-of-empires/src/session/mod.rs: Linux is always XDG; macOS prefers an
35
+ * existing XDG dir, then an existing legacy `~/.agent-of-empires`, then XDG
36
+ * only when `XDG_CONFIG_HOME` is set explicitly; everything else is legacy.
37
+ * OPENZOO_AOE_CONFIG overrides outright (tests, unusual layouts).
38
+ */
39
+ export function aoeConfigPath({
40
+ env = process.env, home = os.homedir(), platform = process.platform, exists = fs.existsSync,
41
+ } = {}) {
42
+ if (env.OPENZOO_AOE_CONFIG) return env.OPENZOO_AOE_CONFIG;
43
+ const xdg = path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'agent-of-empires');
44
+ const legacy = path.join(home, '.agent-of-empires');
45
+ let dir;
46
+ if (platform === 'linux') dir = xdg;
47
+ else if (platform === 'darwin') {
48
+ if (exists(xdg)) dir = xdg;
49
+ else if (exists(legacy)) dir = legacy;
50
+ else dir = env.XDG_CONFIG_HOME ? xdg : legacy;
51
+ } else dir = legacy;
52
+ return path.join(dir, 'config.toml');
53
+ }
54
+
55
+ /**
56
+ * The command AoE will put in a tmux pane. `openzoo` on PATH when it is
57
+ * (global install); otherwise the exact node + script that is running right
58
+ * now, so `npx openzoo aoe` users get a session that launches instead of a
59
+ * "command not found" pane. AoE verifies the first word is on PATH before it
60
+ * creates a session, which is also why an absolute path beats `npx openzoo`.
61
+ */
62
+ export function openzooLaunchCommand({ env = process.env, execPath = process.execPath, script = process.argv[1] } = {}) {
63
+ const name = 'openzoo' + (process.platform === 'win32' ? '.cmd' : '');
64
+ for (const dir of String(env.PATH || '').split(path.delimiter)) {
65
+ if (!dir) continue;
66
+ try { fs.accessSync(path.join(dir, name), fs.constants.X_OK); return 'openzoo'; } catch { /* next */ }
67
+ }
68
+ return `${shQuote(execPath)} ${shQuote(script)}`;
69
+ }
70
+
71
+ /** Quote one argv word for the shell AoE hands the command to. */
72
+ export function shQuote(s) {
73
+ return /^[A-Za-z0-9_/.:@%+=-]+$/.test(s) ? s : `'${String(s).replace(/'/g, "'\\''")}'`;
74
+ }
75
+
76
+ /** TOML basic string. JSON's escapes are a subset of TOML's for the characters
77
+ * a path or command can contain, so this is exact, not approximate. */
78
+ export function tomlString(s) {
79
+ return JSON.stringify(String(s));
80
+ }
81
+
82
+ function keyMatches(rawKey, key) {
83
+ const k = rawKey.trim();
84
+ return k === key || k === `"${key}"` || k === `'${key}'`;
85
+ }
86
+
87
+ /** Split an inline-table body on commas that are not inside a string. */
88
+ function splitInline(body) {
89
+ const parts = [];
90
+ let cur = '';
91
+ let quote = null;
92
+ for (let i = 0; i < body.length; i++) {
93
+ const ch = body[i];
94
+ if (quote) {
95
+ cur += ch;
96
+ if (ch === '\\' && quote === '"') { cur += body[++i] ?? ''; continue; }
97
+ if (ch === quote) quote = null;
98
+ continue;
99
+ }
100
+ if (ch === '"' || ch === "'") { quote = ch; cur += ch; continue; }
101
+ if (ch === ',') { parts.push(cur); cur = ''; continue; }
102
+ cur += ch;
103
+ }
104
+ if (cur.trim()) parts.push(cur);
105
+ return parts.map((p) => p.trim()).filter(Boolean);
106
+ }
107
+
108
+ /**
109
+ * Set `key = value` inside TOML table `table` (dotted, e.g.
110
+ * `session.custom_agents`), replacing any existing value for that key.
111
+ * Pure: string in, string out.
112
+ *
113
+ * Handles the two spellings a hand-edited config uses:
114
+ * [session.custom_agents] header form → key inserted under it
115
+ * [session]
116
+ * custom_agents = { a = "b" } inline form → entry injected into the braces
117
+ * and appends a fresh header only when neither exists. `value` must already be
118
+ * a TOML literal (see tomlString).
119
+ */
120
+ export function upsertTomlKey(toml, table, key, value) {
121
+ const lines = String(toml || '').split('\n');
122
+ const parent = table.includes('.') ? table.slice(0, table.lastIndexOf('.')) : '';
123
+ const child = table.slice(table.lastIndexOf('.') + 1);
124
+ const headerOf = (line) => {
125
+ const m = line.match(/^\s*\[([^[\]]+)\]\s*(#.*)?$/);
126
+ return m ? m[1].trim().replace(/\s+/g, '').replace(/"/g, '') : null;
127
+ };
128
+ let section = '';
129
+ let headerIdx = -1;
130
+ let inlineIdx = -1;
131
+ const out = [];
132
+ for (const line of lines) {
133
+ const h = headerOf(line);
134
+ if (h !== null) {
135
+ section = h;
136
+ if (h === table && headerIdx === -1) headerIdx = out.length;
137
+ out.push(line);
138
+ continue;
139
+ }
140
+ if (section === table) {
141
+ const m = line.match(/^\s*("[^"]*"|'[^']*'|[A-Za-z0-9_-]+)\s*=/);
142
+ if (m && keyMatches(m[1], key)) continue; // ours, rewritten below
143
+ }
144
+ if (section === parent && inlineIdx === -1) {
145
+ const m = line.match(/^\s*("[^"]*"|'[^']*'|[A-Za-z0-9_-]+)\s*=\s*\{/);
146
+ if (m && keyMatches(m[1], child)) inlineIdx = out.length;
147
+ }
148
+ out.push(line);
149
+ }
150
+ const entry = `${key} = ${value}`;
151
+ if (headerIdx !== -1) {
152
+ out.splice(headerIdx + 1, 0, entry);
153
+ } else if (inlineIdx !== -1) {
154
+ const line = out[inlineIdx];
155
+ const open = line.indexOf('{');
156
+ const close = line.lastIndexOf('}');
157
+ if (close > open) {
158
+ const body = splitInline(line.slice(open + 1, close))
159
+ .filter((p) => !keyMatches(p.split('=')[0], key));
160
+ out[inlineIdx] = `${line.slice(0, open + 1)} ${[entry, ...body].join(', ')} ${line.slice(close)}`;
161
+ } else {
162
+ // Multi-line inline table (TOML 1.1). Rare; a fresh header would clash
163
+ // with it, so refuse loudly rather than corrupt the file.
164
+ throw new Error(`cannot edit multi-line inline table \`${child}\` in [${parent}] — add ${entry} by hand`);
165
+ }
166
+ } else {
167
+ while (out.length && out[out.length - 1].trim() === '') out.pop();
168
+ if (out.length) out.push('');
169
+ out.push(`[${table}]`, entry);
170
+ }
171
+ return `${out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
172
+ }
173
+
174
+ /** Drop a whole `[table]` block (header through the next header). */
175
+ export function stripTomlTable(toml, table) {
176
+ const lines = String(toml || '').split('\n');
177
+ const out = [];
178
+ let skipping = false;
179
+ for (const line of lines) {
180
+ const m = line.match(/^\s*\[([^[\]]+)\]\s*(#.*)?$/);
181
+ if (m) skipping = m[1].trim().replace(/\s+/g, '').replace(/"/g, '') === table;
182
+ if (!skipping) out.push(line);
183
+ }
184
+ return out.join('\n').replace(/\n{3,}/g, '\n\n');
185
+ }
186
+
187
+ /**
188
+ * Merge openzoo into an AoE config. Pure.
189
+ *
190
+ * - session.custom_agents.openzoo = "<launch> claude" the agent
191
+ * - session.agent_detect_as.openzoo = "claude" inherit Claude's
192
+ * status hooks, resume/fork flags and structured-view adapter
193
+ * - tools.openzoo receipts tail on Alt+z (persistent tool
194
+ * session; the proxy's log is the spend ledger)
195
+ * - session.agent_command_override.claude (opt-in, `overrideClaude`): every
196
+ * built-in claude session in AoE launches through openzoo too
197
+ */
198
+ export function mergeAoeConfig(toml, {
199
+ launch = 'openzoo', logPath = '~/.openzoo/proxy.log', overrideClaude = false, receipts = true,
200
+ } = {}) {
201
+ let out = String(toml || '');
202
+ const changes = [];
203
+ const claudeCmd = tomlString(`${launch} claude`);
204
+ out = upsertTomlKey(out, 'session.custom_agents', AGENT_NAME, claudeCmd);
205
+ changes.push(`session.custom_agents.${AGENT_NAME} = ${claudeCmd}`);
206
+ out = upsertTomlKey(out, 'session.agent_detect_as', AGENT_NAME, '"claude"');
207
+ changes.push(`session.agent_detect_as.${AGENT_NAME} = "claude"`);
208
+ if (overrideClaude) {
209
+ out = upsertTomlKey(out, 'session.agent_command_override', 'claude', claudeCmd);
210
+ changes.push(`session.agent_command_override.claude = ${claudeCmd}`);
211
+ }
212
+ out = stripTomlTable(out, `tools.${TOOL_NAME}`);
213
+ if (receipts) {
214
+ const cmd = tomlString(`${launch} balance; tail -n 40 -f ${logPath}`);
215
+ // Comment INSIDE the block: stripTomlTable drops header-to-next-header, so
216
+ // a comment above the header would survive every re-run and pile up.
217
+ out = `${out.trimEnd()}\n\n[tools.${TOOL_NAME}]\n# openzoo receipts: wallet balance, then the proxy's live payment log.\ncommand = ${cmd}\nhotkey = "Alt+z"\n`;
218
+ changes.push(`tools.${TOOL_NAME} (Alt+z) = ${cmd}`);
219
+ }
220
+ return { toml: out, changes };
221
+ }
222
+
223
+ function aoeInstalled() {
224
+ const r = spawnSync('aoe', ['--version'], { encoding: 'utf8' });
225
+ return r.status === 0 ? (r.stdout || '').trim() : null;
226
+ }
227
+
228
+ async function proxyUp(base) {
229
+ try { return (await fetch(`${base}/info`, { signal: AbortSignal.timeout(2500) })).ok; } catch { return false; }
230
+ }
231
+
232
+ /**
233
+ * Start a proxy that outlives this command. AoE sessions are tmux sessions:
234
+ * they keep running after the TUI closes, so a proxy tied to this process
235
+ * would vanish under them. `openzoo claude` inside a pane heals a missing
236
+ * proxy at launch, but a proxy that dies mid-conversation is a wall of API
237
+ * errors in every other session, so the long-lived one is started here.
238
+ * No public tunnel unless asked: a detached proxy nobody is watching must not
239
+ * open a URL + key that spends the wallet with no session cap.
240
+ */
241
+ function startDetachedProxy({ tunnel }) {
242
+ const logPath = path.join(os.homedir(), '.openzoo', 'proxy.log');
243
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
244
+ const fd = fs.openSync(logPath, 'a');
245
+ const env = { ...process.env };
246
+ if (!tunnel && !env.OPENZOO_NO_TUNNEL) env.OPENZOO_NO_TUNNEL = '1';
247
+ const child = spawn(process.execPath, [process.argv[1], 'proxy'], {
248
+ detached: true, stdio: ['ignore', fd, fd], env,
249
+ });
250
+ child.unref();
251
+ fs.closeSync(fd);
252
+ return { pid: child.pid, logPath };
253
+ }
254
+
255
+ export async function setupAoe(argv = []) {
256
+ const flags = new Set(argv.filter((a) => a.startsWith('--')));
257
+ const rest = argv.filter((a) => !['--no-launch', '--no-proxy', '--tunnel', '--override-claude', '--no-receipts'].includes(a));
258
+ const cfgPath = aoeConfigPath();
259
+ const launch = openzooLaunchCommand();
260
+
261
+ let existing = '';
262
+ try { existing = fs.readFileSync(cfgPath, 'utf8'); } catch { existing = ''; }
263
+ const { toml, changes } = mergeAoeConfig(existing, {
264
+ launch,
265
+ logPath: path.join(os.homedir(), '.openzoo', 'proxy.log'),
266
+ overrideClaude: flags.has('--override-claude'),
267
+ receipts: !flags.has('--no-receipts'),
268
+ });
269
+ if (toml !== existing) {
270
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
271
+ fs.writeFileSync(cfgPath, toml);
272
+ }
273
+ console.error(`openzoo: ${toml === existing ? 'already in' : 'written to'} ${cfgPath}`);
274
+ for (const c of changes) console.error(` ${c}`);
275
+ console.error(' agent "openzoo" = Claude Code on the zoo: pays x402 per turn, no API key, no account.');
276
+ console.error(' terminal view only — AoE\'s structured view runs claude-agent-acp itself, which bills Anthropic.');
277
+
278
+ const version = aoeInstalled();
279
+ if (!version) {
280
+ console.error('');
281
+ console.error('openzoo: `aoe` is not installed (Agent of Empires — tmux session manager for coding agents).');
282
+ console.error(' brew install aoe');
283
+ console.error(' curl -fsSL https://raw.githubusercontent.com/agent-of-empires/agent-of-empires/main/scripts/install.sh | bash');
284
+ console.error(' then: openzoo aoe (TUI) openzoo aoe add . -l (one session, launched)');
285
+ process.exit(flags.has('--no-launch') ? 0 : 1);
286
+ }
287
+
288
+ const base = `http://localhost:${config.port}/v1`;
289
+ if (!flags.has('--no-proxy')) {
290
+ if (await proxyUp(base)) {
291
+ console.error(`openzoo: proxy already up on ${base}`);
292
+ } else {
293
+ const { pid, logPath } = startDetachedProxy({ tunnel: flags.has('--tunnel') });
294
+ let up = false;
295
+ for (let i = 0; i < 40 && !up; i++) {
296
+ await new Promise((r) => setTimeout(r, 300));
297
+ up = await proxyUp(base);
298
+ }
299
+ if (up) console.error(`openzoo: proxy started in the background (pid ${pid}, receipts in ${logPath}; stop: kill ${pid})`);
300
+ else console.error(`openzoo: proxy not answering yet on ${base} — sessions will start one themselves; see ${logPath}`);
301
+ }
302
+ }
303
+ if (flags.has('--no-launch')) return;
304
+
305
+ // `openzoo aoe add …` → `aoe add --tool openzoo …` unless the caller chose.
306
+ const args = [...rest];
307
+ if (args[0] === 'add' && !args.some((a) => a === '--tool' || a === '--cmd' || a === '-c')) {
308
+ args.splice(1, 0, '--tool', AGENT_NAME);
309
+ }
310
+ console.error(`openzoo: aoe ${version} — running: aoe ${args.join(' ')}`.trimEnd());
311
+ const child = spawn('aoe', args, { stdio: 'inherit' });
312
+ child.on('exit', (code) => process.exit(code ?? 0));
313
+ child.on('error', (e) => { console.error(`openzoo: could not launch aoe: ${e.message}`); process.exit(1); });
314
+ }
@@ -1626,7 +1626,38 @@ function entryPlainText(v) {
1626
1626
  /** Prior turns for zooComplete. sendPrompt used to POST only the latest user
1627
1627
  * line, so the model said each thread starts blank while the UI still showed
1628
1628
  * the canvas. Skip the last user echo of `currentPrompt` (already appended). */
1629
- function historyMessages(agentId, currentPrompt) {
1629
+ /**
1630
+ * Strict user/assistant alternation. The abliteration lane 400s
1631
+ * "Invalid model provider request" on `auauauuuau` (wakeup + "continue" +
1632
+ * nudge = three user turns; error text + reply = two assistant turns).
1633
+ * Lossless fold: texts joined with a blank line; tool turns never crossed.
1634
+ */
1635
+ export function foldSameRole(messages) {
1636
+ const out = [];
1637
+ const foldable = (m) => m && (m.role === 'user' || m.role === 'assistant') && !m.tool_calls && !m.tool_call_id;
1638
+ for (const m of messages || []) {
1639
+ const last = out[out.length - 1];
1640
+ if (last && foldable(last) && foldable(m) && last.role === m.role) {
1641
+ const a = last.content; const b = m.content;
1642
+ let content;
1643
+ if (Array.isArray(a) || Array.isArray(b)) {
1644
+ const arr = (v) => (Array.isArray(v) ? v : (v == null || v === '' ? [] : [{ type: 'text', text: String(v) }]));
1645
+ content = [...arr(a), ...arr(b)];
1646
+ } else {
1647
+ const sa = a == null ? '' : String(a); const sb = b == null ? '' : String(b);
1648
+ content = sa && sb ? `${sa}\n\n${sb}` : (sa || sb);
1649
+ }
1650
+ out[out.length - 1] = { ...last, content };
1651
+ continue;
1652
+ }
1653
+ out.push(m);
1654
+ }
1655
+ return out;
1656
+ }
1657
+ function historyMessages(...args) {
1658
+ return foldSameRole(historyMessagesRaw(...args));
1659
+ }
1660
+ function historyMessagesRaw(agentId, currentPrompt) {
1630
1661
  const entries = agentTranscript(agentId).entries || [];
1631
1662
  const cur = String(currentPrompt || '').trim();
1632
1663
  const out = [];
@@ -2997,6 +3028,14 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
2997
3028
  }
2998
3029
  }
2999
3030
  const data = await r.json().catch(() => ({}));
3031
+ // EVIDENCE FOR THE NEXT DEBUG: the exact body behind a non-200, on disk.
3032
+ // "Invalid model provider request." reproduced with no probe until the
3033
+ // real transcript could be replayed.
3034
+ if (r.status !== 200) {
3035
+ try {
3036
+ fs.writeFileSync(path.join(os.homedir(), '.openzoo', 'zoo-last-error-body.json'), JSON.stringify({ at: new Date().toISOString(), status: r.status, agentId, response: data, payload }, null, 1));
3037
+ } catch { /* */ }
3038
+ }
3000
3039
  if (r.status === 402) {
3001
3040
  const raw = zooTextFromMessage(data?.choices?.[0]?.message, data)
3002
3041
  || data?.error?.message
package/lib/openclaw.js CHANGED
@@ -18,6 +18,14 @@
18
18
  * charges at most this, less with trailing volume and leCore context reuse.
19
19
  * A client-side estimate can only be honest-or-high; receipts on the proxy
20
20
  * console stay the ground truth.
21
+ *
22
+ * RE-MEASURED on OpenClaw 2026.8.2 (2026-09-02): the block this writes still
23
+ * validates (`openclaw config validate` accepts baseUrl/apiKey/api/models with
24
+ * cost/contextWindow/maxTokens), and a hand-added provider still gets no live
25
+ * discovery — 2026.8's "live model discovery" is provider-plugin-owned. The
26
+ * bundled `extensions/openzoo` provider plugin (upstream PR) is the proper
27
+ * 2026.8+ path: discovery + pricing from /v1/models, no config surgery. This
28
+ * command stays for older builds and for pinning an exact model list.
21
29
  */
22
30
  import fs from 'node:fs';
23
31
  import os from 'node:os';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.78",
3
+ "version": "0.50.79",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",