openzoo 0.50.78 → 0.50.80
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 +4 -0
- package/bin/openzoo.js +12 -0
- package/lib/aoe.js +314 -0
- package/lib/cursorbackend.js +44 -1
- package/lib/openclaw.js +8 -0
- package/package.json +1 -1
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
|
+
}
|
package/lib/cursorbackend.js
CHANGED
|
@@ -1626,7 +1626,42 @@ 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
|
-
|
|
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
|
+
// An assistant entry with no content and no tool_calls (a failed/empty turn
|
|
1638
|
+
// recorded on the canvas) is invalid for every OpenAI-compatible provider:
|
|
1639
|
+
// measured 2026-09-02 as `400 Invalid model provider request` param=messages.
|
|
1640
|
+
messages = (messages || []).filter((m) => !(m && m.role === 'assistant' && (m.content == null || m.content === '') && !m.tool_calls));
|
|
1641
|
+
const foldable = (m) => m && (m.role === 'user' || m.role === 'assistant') && !m.tool_calls && !m.tool_call_id;
|
|
1642
|
+
for (const m of messages) {
|
|
1643
|
+
const last = out[out.length - 1];
|
|
1644
|
+
if (last && foldable(last) && foldable(m) && last.role === m.role) {
|
|
1645
|
+
const a = last.content; const b = m.content;
|
|
1646
|
+
let content;
|
|
1647
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
1648
|
+
const arr = (v) => (Array.isArray(v) ? v : (v == null || v === '' ? [] : [{ type: 'text', text: String(v) }]));
|
|
1649
|
+
content = [...arr(a), ...arr(b)];
|
|
1650
|
+
} else {
|
|
1651
|
+
const sa = a == null ? '' : String(a); const sb = b == null ? '' : String(b);
|
|
1652
|
+
content = sa && sb ? `${sa}\n\n${sb}` : (sa || sb);
|
|
1653
|
+
}
|
|
1654
|
+
out[out.length - 1] = { ...last, content };
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
out.push(m);
|
|
1658
|
+
}
|
|
1659
|
+
return out;
|
|
1660
|
+
}
|
|
1661
|
+
function historyMessages(...args) {
|
|
1662
|
+
return foldSameRole(historyMessagesRaw(...args));
|
|
1663
|
+
}
|
|
1664
|
+
function historyMessagesRaw(agentId, currentPrompt) {
|
|
1630
1665
|
const entries = agentTranscript(agentId).entries || [];
|
|
1631
1666
|
const cur = String(currentPrompt || '').trim();
|
|
1632
1667
|
const out = [];
|
|
@@ -2997,6 +3032,14 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
2997
3032
|
}
|
|
2998
3033
|
}
|
|
2999
3034
|
const data = await r.json().catch(() => ({}));
|
|
3035
|
+
// EVIDENCE FOR THE NEXT DEBUG: the exact body behind a non-200, on disk.
|
|
3036
|
+
// "Invalid model provider request." reproduced with no probe until the
|
|
3037
|
+
// real transcript could be replayed.
|
|
3038
|
+
if (r.status !== 200) {
|
|
3039
|
+
try {
|
|
3040
|
+
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));
|
|
3041
|
+
} catch { /* */ }
|
|
3042
|
+
}
|
|
3000
3043
|
if (r.status === 402) {
|
|
3001
3044
|
const raw = zooTextFromMessage(data?.choices?.[0]?.message, data)
|
|
3002
3045
|
|| 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.
|
|
3
|
+
"version": "0.50.80",
|
|
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",
|