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 +10 -0
- package/lib/grokbot.js +153 -0
- package/package.json +1 -1
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.
|
|
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",
|