openzoo 0.37.0 → 0.38.0

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/bin/openzoo.js CHANGED
@@ -23,6 +23,9 @@ usage:
23
23
  by default, --terminal for the Claude Code CLI
24
24
  npx openzoo launch <cmd> [args] launch a TERMINAL Messages API client
25
25
  (claude, aider...) already pointed at the zoo
26
+ npx openzoo grokbot point Grok Bot / the grok CLI at the zoo — GROK MODELS ONLY,
27
+ paid per call by x402 instead of xAI first-party billing,
28
+ then launch it. --no-launch just writes the config.
26
29
  npx openzoo mcp stdio MCP server (tools: zoo_ask, zoo_bind, zoo_models, zoo_wallet, zoo_contexts)
27
30
  npx openzoo unblock restore the editor's own backend in the hosts file
28
31
  npx openzoo tunnel public-url-only mode (everything key-gated, no keyless localhost)
@@ -68,6 +71,13 @@ async function main() {
68
71
  // GUI editors read config files, not env vars — see lib/setup.js.
69
72
  await (await import('../lib/setup.js')).setupEditor(cmd === 'editor' ? undefined : cmd, process.argv[3]);
70
73
  break;
74
+ case 'grokbot':
75
+ case 'grok':
76
+ // Grok Bot (com.anysphere.sand) fronts the `grok` CLI, and the CLI reads
77
+ // ~/.grok/config.toml — so pointing that model table at the local proxy
78
+ // is enough; no patching of the app bundle.
79
+ await (await import('../lib/grokbot.js')).setupGrokBot(process.argv.slice(3));
80
+ break;
71
81
  case 'mcp':
72
82
  await (await import('../lib/mcp.js')).startMcp();
73
83
  break;
package/lib/grokbot.js ADDED
@@ -0,0 +1,153 @@
1
+ // `npx openzoo grokbot` — Grok Bot (xAI's agent app) on the zoo, Grok-only.
2
+ //
3
+ // WHAT GROK BOT IS: an Electron app, bundle id `com.anysphere.sand` (Anysphere,
4
+ // the Cursor company) shipped as "Grok Bot". It fronts the `grok` CLI, and the
5
+ // CLI is what reads ~/.grok/config.toml — which is why pointing the CLI's model
6
+ // table at the local proxy is enough, with no patching of the app bundle.
7
+ //
8
+ // WHY GROK-ONLY: the point of running it on the zoo is x402 per-call billing
9
+ // instead of first-party xAI billing, while keeping the product it is — a Grok
10
+ // agent. So every model written here is an x-ai/* id and nothing else is
11
+ // reachable from the picker.
12
+ //
13
+ // WHY THE LOCAL PROXY AND NOT x402-tokens.fly.dev: the gateway answers 402 on
14
+ // every call by design. The proxy is the thing that builds and signs the
15
+ // payment. Pointing base_url at the gateway makes every request fail
16
+ // "payment required".
17
+
18
+ import { spawn } from 'node:child_process';
19
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'node:fs';
20
+ import { homedir } from 'node:os';
21
+ import { join } from 'node:path';
22
+
23
+ import { config } from './config.js';
24
+
25
+ const GROK_HOME = process.env.GROK_HOME || join(homedir(), '.grok');
26
+ const CONFIG_PATH = join(GROK_HOME, 'config.toml');
27
+ const APP = '/Applications/Grok Bot.app';
28
+
29
+ /** Grok ids as the zoo serves them. Verified against GET /v1/models at write
30
+ * time so a renamed/retired model never lands in the picker as a dead row. */
31
+ const FALLBACK_MODELS = [
32
+ { key: 'openzoo-grok-46', id: 'x-ai/grok-4.6', name: 'Grok 4.6 (openzoo)' },
33
+ { key: 'openzoo-grok-45', id: 'x-ai/grok-4.5', name: 'Grok 4.5 (openzoo)' },
34
+ ];
35
+
36
+ /**
37
+ * TOML table keys cannot contain a bare dot — `[model.openzoo-grok-4.6]` parses
38
+ * as model -> "openzoo-grok-4" -> "6", silently producing a malformed entry
39
+ * with no `model` field. Slugify to keep the key flat.
40
+ */
41
+ function slug(id) {
42
+ return 'openzoo-' + id.replace(/^x-ai\//, '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/-+$/, '');
43
+ }
44
+
45
+ async function grokModels(base) {
46
+ try {
47
+ const r = await fetch(`${base}/models`, { signal: AbortSignal.timeout(8000) });
48
+ if (!r.ok) throw new Error(`models ${r.status}`);
49
+ const j = await r.json();
50
+ const rows = (j.data || [])
51
+ // VENDOR-PREFIXED IDS ONLY. The local proxy augments /v1/models with
52
+ // harness aliases ("grok-4", "openzoo-grok-4.6") so editors that validate
53
+ // a configured id upfront do not refuse to start. Those are routing
54
+ // conveniences, not catalog rows — writing them here would produce
55
+ // duplicate picker entries that all resolve to the same model.
56
+ .filter((m) => typeof m.id === 'string' && m.id.startsWith('x-ai/') && !m.kind)
57
+ .map((m) => ({ key: slug(m.id), id: m.id, name: `${m.id.replace(/^x-ai\//, 'Grok ')} (openzoo)` }));
58
+ return rows.length ? rows : FALLBACK_MODELS;
59
+ } catch {
60
+ // never write an empty picker because the catalog blipped
61
+ return FALLBACK_MODELS;
62
+ }
63
+ }
64
+
65
+ /** Strip the blocks we own so a re-run is idempotent, keeping everything else
66
+ * (mcp_servers, plugins, marketplace, privacy, ui) exactly as the user had it. */
67
+ function stripOurs(toml) {
68
+ const lines = toml.split('\n');
69
+ const out = [];
70
+ let skipping = false;
71
+ for (const line of lines) {
72
+ const header = line.match(/^\s*\[([^\]]+)\]\s*$/);
73
+ if (header) {
74
+ const name = header[1];
75
+ skipping = name === 'models' || name.startsWith('model.');
76
+ }
77
+ if (!skipping) out.push(line);
78
+ }
79
+ return out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
80
+ }
81
+
82
+ function renderModels(models, base) {
83
+ const def = models[0].key;
84
+ const body = models.map((m) => `[model.${m.key}]
85
+ model = "${m.id}"
86
+ base_url = "${base}"
87
+ api_key = "openzoo"
88
+ name = "${m.name}"`).join('\n\n');
89
+ return `
90
+ # --- openzoo: Grok-only, paid per call by x402 -------------------------------
91
+ # Written by \`npx openzoo grokbot\`. Re-running rewrites ONLY this block.
92
+ # base_url is the LOCAL proxy: the public gateway 402s every call by design,
93
+ # and the proxy is what signs the payment.
94
+ [models]
95
+ default = "${def}"
96
+
97
+ ${body}
98
+ `;
99
+ }
100
+
101
+ export async function setupGrokBot(argv = []) {
102
+ const base = `http://localhost:${config.port}/v1`;
103
+ const launch = !argv.includes('--no-launch');
104
+
105
+ // 1. proxy up, or nothing can pay
106
+ let up = false;
107
+ try { up = (await fetch(`${base}/models`, { signal: AbortSignal.timeout(3000) })).ok; } catch { up = false; }
108
+ if (!up) {
109
+ console.error('openzoo: starting the proxy...');
110
+ const { startProxy } = await import('./proxy.js');
111
+ await startProxy({ silent: true, autoTunnel: true });
112
+ for (let i = 0; i < 25 && !up; i++) {
113
+ await new Promise((r) => setTimeout(r, 300));
114
+ try { up = (await fetch(`${base}/models`, { signal: AbortSignal.timeout(2000) })).ok; } catch { /* wait */ }
115
+ }
116
+ if (!up) { console.error(`openzoo: proxy did not come up on ${base}`); process.exit(1); }
117
+ }
118
+
119
+ const models = await grokModels(base);
120
+
121
+ // 2. rewrite ONLY our block, after a timestamped backup
122
+ if (!existsSync(GROK_HOME)) mkdirSync(GROK_HOME, { recursive: true });
123
+ let existing = '';
124
+ if (existsSync(CONFIG_PATH)) {
125
+ const backup = `${CONFIG_PATH}.bak-${Date.now()}`;
126
+ copyFileSync(CONFIG_PATH, backup);
127
+ existing = readFileSync(CONFIG_PATH, 'utf8');
128
+ console.error(`openzoo: backed up ${CONFIG_PATH} -> ${backup}`);
129
+ }
130
+ let next = stripOurs(existing);
131
+ // the fork/secondary model must ride the zoo too, or forks quietly bill xAI
132
+ // first-party while the main model is on openzoo
133
+ if (/^\s*fork_secondary_model\s*=/m.test(next)) {
134
+ next = next.replace(/^\s*fork_secondary_model\s*=.*$/m, `fork_secondary_model = "${models[0].key}"`);
135
+ }
136
+ writeFileSync(CONFIG_PATH, `${next}\n${renderModels(models, base)}`, 'utf8');
137
+
138
+ console.error(`openzoo: wrote ${models.length} Grok model(s) to ${CONFIG_PATH}`);
139
+ for (const m of models) console.error(` ${m.key} -> ${m.id}`);
140
+ console.error(`openzoo: default = ${models[0].key} · all calls paid by x402 from your burner wallet`);
141
+
142
+ if (!launch) return;
143
+
144
+ // 3. launch the app if it exists, else the CLI
145
+ if (existsSync(APP)) {
146
+ console.error('openzoo: launching Grok Bot...');
147
+ spawn('open', ['-a', APP], { stdio: 'ignore', detached: true }).unref();
148
+ } else {
149
+ console.error('openzoo: Grok Bot.app not found — starting the `grok` CLI instead');
150
+ const p = spawn('grok', argv.filter((a) => a !== '--no-launch'), { stdio: 'inherit' });
151
+ p.on('exit', (c) => process.exit(c ?? 0));
152
+ }
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
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",