openzoo 0.49.8 → 0.49.10

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
@@ -23,23 +23,56 @@ paid $0.002137 (9.5× cheaper than direct) · rail solana · tx 5Kd…
23
23
 
24
24
  **Cursor** (Settings → Models → OpenAI API): set *Override OpenAI Base URL* to `http://localhost:8402/v1`, API key `sk-openzoo`. (Cursor Hobby can't BYOK; Pro can.)
25
25
 
26
- **Claude Code (zoo catalog, not opus-5-only):**
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
- Prefer this. `ANTHROPIC_API_KEY` stays unset (it would bill api.anthropic.com). Base URL is `http://localhost:8402/v1`. `/model` should list zoo animals from the live OpenRouter catalog (plus `openzoo-*` twins), not a single opus-5.
28
+ `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
29
 
30
- ```bash
31
- npx openzoo claude
30
+ Mac:
31
+
32
+ ```
33
+ curl -fsSL https://claude.ai/install.sh | bash
34
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.7/install.sh | bash
35
+ . "$HOME/.nvm/nvm.sh"
36
+ nvm install 24
37
+ npm i -g openzoo
38
+ echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc
39
+ openzoo claude
40
+ ```
41
+
42
+ Windows — official Claude install, then nvm-windows (https://github.com/coreybutler/nvm-windows — `nvm-setup.exe`). Do not use the unix nvm curl on Windows. Do not source `~/.zshrc`.
43
+
44
+ PowerShell:
45
+
46
+ ```
47
+ irm https://claude.ai/install.ps1 | iex
48
+ ```
49
+
50
+ CMD:
51
+
52
+ ```
53
+ curl -fsSL https://downloads.claude.ai/install.cmd -o install.cmd && install.cmd && del install.cmd
54
+ ```
55
+
56
+ Then nvm-windows:
57
+
58
+ ```
59
+ nvm install 24
60
+ nvm use 24
61
+ npm i -g openzoo
62
+ openzoo claude
32
63
  ```
33
64
 
34
- Leave a healthy `:8402` sidecar alone. Manual equivalent:
65
+ Leave a healthy `:8402` sidecar alone. Manual equivalent / check that the list is quoteable:
35
66
 
36
67
  ```bash
37
68
  unset ANTHROPIC_API_KEY
38
69
  export ANTHROPIC_BASE_URL=http://localhost:8402/v1
39
70
  export ANTHROPIC_AUTH_TOKEN=sk-openzoo
40
71
  export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1
41
- curl -s "$ANTHROPIC_BASE_URL/models" # many OpenRouter ids, not 1
42
- claude # /model lists those zoo animals
72
+ curl -s "$ANTHROPIC_BASE_URL/models" | jq '[.data[].id] | map(select(test(":batch") or startswith("openzoo-")))'
73
+ # -> [] (no :batch, no openzoo-* clones)
74
+ curl -s -H 'anthropic-version: 2023-06-01' "$ANTHROPIC_BASE_URL/models" | jq '[.data[].id]'
75
+ # -> short picker (opus/sonnet/haiku + a few gateway ids + openzoo/auto)
43
76
  ```
44
77
 
45
78
  **Any OpenAI-env tool:**
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Drive Claude Code (`claude --print --output-format stream-json`) as grokui
3
+ * Auto. This is the same harness `openzoo claude` launches: env from
4
+ * claudeZooEnv (ANTHROPIC_BASE_URL + AUTH_TOKEN at :8402, no Anthropic
5
+ * API key). Tools are Claude Code's own (Bash, Read, Write, Edit, Glob, Grep).
6
+ *
7
+ * Do not re-parse RUN:/WRITE:/DONE: here. Stream-json is Claude Code's
8
+ * official print protocol; we only fold those events onto the canvas.
9
+ */
10
+ import { spawn } from 'node:child_process';
11
+ import { claudeZooEnv, resolveClaudeCli } from './launch.js';
12
+
13
+ export const AUTO_CLAUDE_SYSTEM = 'You are grokui Auto, paid per call through the local OpenZoo proxy (x402 on :8402). '
14
+ + 'Use your native tools (Bash, Read, Write, Edit, Glob, Grep) to do the work in this working directory. '
15
+ + 'Do not curl localhost:8402/v1/chat/completions and do not emit RUN:/WRITE:/DONE: text directives — you already have real tools. '
16
+ + 'Do not ask the user to type continue.';
17
+
18
+ export const CLAUDE_MISSING = 'claude CLI not found. Auto is the Claude Code harness via OpenZoo — install it with: '
19
+ + 'curl -fsSL https://claude.ai/install.sh | bash (then ensure ~/.local/bin is on PATH). '
20
+ + 'No Anthropic login: `openzoo claude` already points ANTHROPIC_BASE_URL at the local proxy.';
21
+
22
+ let runnerOverride = null;
23
+ export function setClaudeRunnerForTest(fn) {
24
+ runnerOverride = typeof fn === 'function' ? fn : null;
25
+ }
26
+
27
+ export function claudePrintArgs({ prompt, sessionId, model, system } = {}) {
28
+ const args = [
29
+ '--print',
30
+ '--verbose',
31
+ '--output-format', 'stream-json',
32
+ '--permission-mode', 'bypassPermissions',
33
+ ];
34
+ const sys = system === undefined ? AUTO_CLAUDE_SYSTEM : system;
35
+ if (sys) args.push('--append-system-prompt', sys);
36
+ if (sessionId) args.push('--resume', String(sessionId));
37
+ if (model) args.push('--model', String(model));
38
+ if (prompt != null && prompt !== '') args.push(String(prompt));
39
+ return args;
40
+ }
41
+
42
+ export function toolStatusLine(name, input) {
43
+ const n = String(name || 'tool');
44
+ const i = input && typeof input === 'object' ? input : {};
45
+ if (n === 'Write' || n === 'Edit' || n === 'Read' || n === 'NotebookEdit') {
46
+ return `${n} ${i.file_path || i.path || ''}`.trim();
47
+ }
48
+ if (n === 'Bash') return `Bash ${String(i.command || i.cmd || '').slice(0, 80)}`.trim();
49
+ if (n === 'Glob') return `Glob ${i.pattern || ''}`.trim();
50
+ if (n === 'Grep') return `Grep ${i.pattern || ''}`.trim();
51
+ if (n === 'Task') return `Task ${i.description || i.prompt || ''}`.trim();
52
+ return n;
53
+ }
54
+
55
+ export function paymentFailText(text) {
56
+ const s = String(text || '');
57
+ if (/\b(?:wallet is empty|empty wallet|wallet underfunded|underfunded)\b/i.test(s)) {
58
+ return s.includes('HTTP 402') || /payment/i.test(s)
59
+ ? s
60
+ : `(payment required — HTTP 402, the wallet is empty.) ${s}`.trim();
61
+ }
62
+ if (/\b(?:payment failed|HTTP 402|payment required)\b/i.test(s)) return s;
63
+ return '';
64
+ }
65
+
66
+ /**
67
+ * Fold one Claude Code stream-json object into a grokui-sized event.
68
+ * Not a RUN: parser — just the official print protocol.
69
+ */
70
+ export function foldClaudeEvent(ev) {
71
+ if (!ev || typeof ev !== 'object') return null;
72
+ const sessionId = ev.session_id || ev.sessionId;
73
+ if (ev.type === 'system' && ev.subtype === 'init') {
74
+ return { kind: 'init', sessionId, model: ev.model, tools: ev.tools || [] };
75
+ }
76
+ if (ev.type === 'stream_event') {
77
+ const delta = ev.event?.delta;
78
+ if (delta?.type === 'thinking_delta' && delta.thinking) {
79
+ return { kind: 'think', text: delta.thinking, sessionId };
80
+ }
81
+ if (delta?.type === 'text_delta' && delta.text) {
82
+ return { kind: 'text', text: delta.text, sessionId };
83
+ }
84
+ return { kind: 'partial', sessionId };
85
+ }
86
+ if (ev.type === 'assistant' && ev.message) {
87
+ const blocks = Array.isArray(ev.message.content) ? ev.message.content : [];
88
+ const thinking = [];
89
+ const text = [];
90
+ const tools = [];
91
+ for (const b of blocks) {
92
+ if (!b || typeof b !== 'object') continue;
93
+ if (b.type === 'thinking' && b.thinking) thinking.push(b.thinking);
94
+ else if (b.type === 'text' && b.text) text.push(b.text);
95
+ else if (b.type === 'tool_use') tools.push({ name: b.name, input: b.input || {} });
96
+ }
97
+ return {
98
+ kind: 'assistant',
99
+ thinking: thinking.join('\n'),
100
+ text: text.join(''),
101
+ tools,
102
+ sessionId,
103
+ };
104
+ }
105
+ if (ev.type === 'result') {
106
+ const raw = ev.result != null ? String(ev.result) : (ev.error != null ? String(ev.error) : '');
107
+ const pay = paymentFailText(raw);
108
+ return {
109
+ kind: 'result',
110
+ text: raw,
111
+ error: Boolean(ev.is_error || ev.subtype === 'error' || ev.subtype === 'error_during_execution'),
112
+ paymentFailed: pay,
113
+ sessionId,
114
+ };
115
+ }
116
+ return { kind: ev.type || 'other', sessionId };
117
+ }
118
+
119
+ function parseNdjsonLine(line) {
120
+ const s = String(line || '').trim();
121
+ if (!s) return null;
122
+ try { return JSON.parse(s); } catch { return null; }
123
+ }
124
+
125
+ export async function runClaudeCode({
126
+ prompt,
127
+ cwd,
128
+ sessionId,
129
+ model,
130
+ system,
131
+ env = process.env,
132
+ port,
133
+ onEvent,
134
+ signal,
135
+ } = {}) {
136
+ if (runnerOverride) {
137
+ return runnerOverride({ prompt, cwd, sessionId, model, system, env, port, onEvent, signal });
138
+ }
139
+ const zooEnv = claudeZooEnv(env, { port });
140
+ const cli = resolveClaudeCli(zooEnv);
141
+ if (!cli) {
142
+ return { text: CLAUDE_MISSING, error: true, paymentFailed: '', sessionId: sessionId || '', missing: true };
143
+ }
144
+ const args = claudePrintArgs({ prompt, sessionId, model, system });
145
+ return spawnClaudePrint({ cli, args, cwd, env: zooEnv, onEvent, signal, sessionId });
146
+ }
147
+
148
+ export function spawnClaudePrint({ cli, args, cwd, env, onEvent, signal, sessionId }) {
149
+ return new Promise((resolve) => {
150
+ let child;
151
+ try {
152
+ child = spawn(cli, args, {
153
+ cwd: cwd || process.cwd(),
154
+ env,
155
+ stdio: ['ignore', 'pipe', 'pipe'],
156
+ });
157
+ } catch (e) {
158
+ resolve({
159
+ text: `could not launch claude: ${e.message}`,
160
+ error: true,
161
+ paymentFailed: '',
162
+ sessionId: sessionId || '',
163
+ });
164
+ return;
165
+ }
166
+ let stdout = '';
167
+ let stderr = '';
168
+ let sid = sessionId || '';
169
+ let lastText = '';
170
+ let lastPay = '';
171
+ let sawError = false;
172
+ const onAbort = () => {
173
+ try { child.kill('SIGTERM'); } catch { /* gone */ }
174
+ };
175
+ if (signal) {
176
+ if (signal.aborted) onAbort();
177
+ else signal.addEventListener('abort', onAbort, { once: true });
178
+ }
179
+ const take = (chunk, which) => {
180
+ const s = String(chunk);
181
+ if (which === 'err') stderr += s;
182
+ else stdout += s;
183
+ if (which !== 'out') return;
184
+ const lines = stdout.split(/\r?\n/);
185
+ stdout = lines.pop() || '';
186
+ for (const line of lines) {
187
+ const raw = parseNdjsonLine(line);
188
+ if (!raw) continue;
189
+ const folded = foldClaudeEvent(raw);
190
+ if (!folded) continue;
191
+ if (folded.sessionId) sid = folded.sessionId;
192
+ if (folded.kind === 'result') {
193
+ lastText = folded.text || lastText;
194
+ lastPay = folded.paymentFailed || lastPay;
195
+ if (folded.error) sawError = true;
196
+ } else if (folded.kind === 'assistant' && folded.text) {
197
+ lastText = folded.text;
198
+ }
199
+ try { onEvent?.(folded); } catch { /* paint must not kill the child */ }
200
+ }
201
+ };
202
+ child.stdout.on('data', (d) => take(d, 'out'));
203
+ child.stderr.on('data', (d) => take(d, 'err'));
204
+ child.on('error', (e) => {
205
+ if (signal) signal.removeEventListener?.('abort', onAbort);
206
+ resolve({
207
+ text: `could not launch claude: ${e.message}`,
208
+ error: true,
209
+ paymentFailed: '',
210
+ sessionId: sid,
211
+ });
212
+ });
213
+ child.on('close', (code) => {
214
+ if (signal) signal.removeEventListener?.('abort', onAbort);
215
+ if (stdout.trim()) take('\n', 'out');
216
+ const errPay = paymentFailText(stderr);
217
+ const outPay = lastPay || paymentFailText(lastText) || errPay;
218
+ let text = lastText;
219
+ if (!text && stderr.trim()) text = stderr.trim().slice(0, 4000);
220
+ if (!text && code && code !== 0) text = `claude exited ${code}`;
221
+ resolve({
222
+ text: text || '',
223
+ error: sawError || (code !== 0 && code != null),
224
+ paymentFailed: outPay,
225
+ sessionId: sid,
226
+ stderr,
227
+ exitCode: code,
228
+ });
229
+ });
230
+ });
231
+ }