openzoo 0.50.70 → 0.50.72
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 +2 -0
- package/lib/botlog.js +88 -0
- package/lib/grokcli.js +46 -1
- package/lib/mcpbridge.js +1 -1
- package/lib/proxy.js +16 -11
- package/package.json +1 -1
package/bin/openzoo.js
CHANGED
|
@@ -97,6 +97,8 @@ usage:
|
|
|
97
97
|
that env — bot bounces it so send works. Already-
|
|
98
98
|
hijacked sessions are left alone.
|
|
99
99
|
--quit force bounce · --no-quit never bounce
|
|
100
|
+
--verbose print every request the app makes
|
|
101
|
+
(default is quiet: milestones + problems)
|
|
100
102
|
--web serve the renderer in a browser instead of
|
|
101
103
|
launching the .app (http://127.0.0.1:4174)
|
|
102
104
|
npx openzoo web Grok Bot renderer in the browser. Same hijack as
|
package/lib/botlog.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `openzoo bot` prints to a first-time user's terminal.
|
|
3
|
+
*
|
|
4
|
+
* MEASURED 2026-09-01 on a fresh macOS account: the console was ~60 lines of
|
|
5
|
+
* `#N POST /aiserver.v1.…`, `cursor-tls: connected`, `-> empty-ok`, `/health`
|
|
6
|
+
* before the app even painted, and nothing said how to pay. The human's words:
|
|
7
|
+
* "onboarding is not great mate" / "it's not telling me straight up how to pay".
|
|
8
|
+
*
|
|
9
|
+
* Default is QUIET: only milestones and problems. `--verbose` / OPENZOO_DEBUG=1
|
|
10
|
+
* restores the firehose (it is still the right thing for debugging the wire).
|
|
11
|
+
*/
|
|
12
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
13
|
+
|
|
14
|
+
const NOISE = [
|
|
15
|
+
/^cursor-backend: #\d+ (GET|POST|PUT|DELETE) /,
|
|
16
|
+
/^cursor-tls:/,
|
|
17
|
+
/-> empty-ok/,
|
|
18
|
+
/-> pod /,
|
|
19
|
+
/-> transcript /,
|
|
20
|
+
/-> getHostStatus/,
|
|
21
|
+
/-> GetMe/,
|
|
22
|
+
/-> WatchSandBoxMigration/,
|
|
23
|
+
/GetGrokBotSendStatus/,
|
|
24
|
+
/listAgents local n=/,
|
|
25
|
+
/discovered roster/,
|
|
26
|
+
/SNIFF real pod/,
|
|
27
|
+
/POST \/oauth\/token/,
|
|
28
|
+
/mcp \S+ (chrome-devtools-mcp exposes|Performance tools|\[chrome-devtools-mcp\] The connecting client)/,
|
|
29
|
+
/ProofNetwork MCP server running/,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const MILESTONE = [
|
|
33
|
+
/mcp ready/,
|
|
34
|
+
/mcp \S+ (mode=|FAIL|tools=\d+|re-attach|attached|appeared)/,
|
|
35
|
+
/mcp \S+ To let bots drive your real Chrome/,
|
|
36
|
+
/x402 402|upstream outage|underfunded|wallet/i,
|
|
37
|
+
/ERROR|FAIL|uncaught|rejection|timed out/,
|
|
38
|
+
/wakeups restored|wakeup fire/,
|
|
39
|
+
/ozRevive|ship_|ship:/,
|
|
40
|
+
/sendPrompt done/,
|
|
41
|
+
/create_agent tool|deleteAgents n=/,
|
|
42
|
+
/x_compose|chrome reattach/,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Pure: should this backend line reach a quiet terminal? */
|
|
46
|
+
export function isBotMilestone(line) {
|
|
47
|
+
const s = String(line || '');
|
|
48
|
+
if (NOISE.some((re) => re.test(s))) return false;
|
|
49
|
+
return MILESTONE.some((re) => re.test(s));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function makeBotLogger({ verbose = false, write = (m) => console.error(m) } = {}) {
|
|
53
|
+
return (m) => {
|
|
54
|
+
if (verbose || isBotMilestone(m)) write(` backend: ${m}`);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The block a new user needs before anything else. `balances` is optional
|
|
60
|
+
* ({ USDC, TOKEN } in USD) — printed as `?` when unknown so nothing is invented.
|
|
61
|
+
*/
|
|
62
|
+
export function payBannerLines({ solana, evm, balances = null, whop = 'https://whop.com/staccoverflow/openzoo', chromeMode = 'own-profile' } = {}) {
|
|
63
|
+
const usd = (v) => (typeof v === 'number' && Number.isFinite(v) ? `$${v >= 0.01 || v === 0 ? v.toFixed(2) : v.toFixed(4)}` : '?');
|
|
64
|
+
const lines = [
|
|
65
|
+
'openzoo: HOW TO PAY — no account, no API key, every call is paid from this wallet:',
|
|
66
|
+
` Solana ${solana}`,
|
|
67
|
+
' send USDC or TOKEN here (TOKEN is half price)',
|
|
68
|
+
` Base ${evm}`,
|
|
69
|
+
' send USDC here',
|
|
70
|
+
balances ? ` balance Solana USDC ${usd(balances.USDC)} · TOKEN ${balances.TOKEN_UNITS != null ? `${balances.TOKEN_UNITS} units` : '?'} · Base USDC ${usd(balances.BASE_USDC)} (recheck: openzoo balance)` : ' balance unknown right now — run: openzoo balance',
|
|
71
|
+
` card ${whop} — paste the Solana address above when it asks`,
|
|
72
|
+
];
|
|
73
|
+
if (chromeMode === 'own-profile') {
|
|
74
|
+
lines.push('openzoo: CHROME bots open a blank Chrome until you flip chrome://inspect/#remote-debugging → "Allow remote debugging for this browser", then restart openzoo bot. After that they drive YOUR logged-in Chrome.');
|
|
75
|
+
} else {
|
|
76
|
+
lines.push(`openzoo: CHROME attached to your real browser (${chromeMode}).`);
|
|
77
|
+
}
|
|
78
|
+
lines.push('openzoo: FIRST type in any bot: "set up Grok Ship for ~/path/to/repo" — or just give it work.');
|
|
79
|
+
lines.push('openzoo: QUIET add --verbose to see every request the app makes.');
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function walletAddresses(wallet) {
|
|
84
|
+
return {
|
|
85
|
+
solana: wallet.keypair.publicKey.toBase58(),
|
|
86
|
+
evm: privateKeyToAccount(wallet.evmPrivateKey).address,
|
|
87
|
+
};
|
|
88
|
+
}
|
package/lib/grokcli.js
CHANGED
|
@@ -251,7 +251,9 @@ export async function runBot(argv = []) {
|
|
|
251
251
|
const models = [
|
|
252
252
|
'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-3.5-turbo',
|
|
253
253
|
].map((n) => ({ name: n, label: n }));
|
|
254
|
-
const
|
|
254
|
+
const { makeBotLogger, payBannerLines, walletAddresses } = await import('./botlog.js');
|
|
255
|
+
const verbose = argv.includes('--verbose') || !!process.env.OPENZOO_DEBUG || !!process.env.OPENZOO_VERBOSE;
|
|
256
|
+
const log = makeBotLogger({ verbose });
|
|
255
257
|
const stale = killListen(port);
|
|
256
258
|
if (stale.length) console.error(`openzoo: killed stale hijack on :${port} pids=${stale.join(',')}`);
|
|
257
259
|
try {
|
|
@@ -326,6 +328,49 @@ export async function runBot(argv = []) {
|
|
|
326
328
|
});
|
|
327
329
|
|
|
328
330
|
console.error('openzoo: leave this running. ctrl-c stops the backend.');
|
|
331
|
+
// HOW TO PAY, up front. A first run used to be sixty lines of wire noise and
|
|
332
|
+
// the first 402 was the only thing that ever mentioned money.
|
|
333
|
+
const payBanner = async () => {
|
|
334
|
+
try {
|
|
335
|
+
const { loadOrCreateWallet } = await import('./wallet.js');
|
|
336
|
+
const addrs = walletAddresses(loadOrCreateWallet());
|
|
337
|
+
let balances = null;
|
|
338
|
+
try {
|
|
339
|
+
const { Connection } = await import('@solana/web3.js');
|
|
340
|
+
const { config, FUNDING_ASSETS } = await import('./config.js');
|
|
341
|
+
const { tokenBalance } = await import('./x402.js');
|
|
342
|
+
const { evmTokenBalance } = await import('./evm.js');
|
|
343
|
+
const { loadOrCreateWallet: lw } = await import('./wallet.js');
|
|
344
|
+
const w = lw();
|
|
345
|
+
const conn = new Connection(config.rpcUrl, 'confirmed');
|
|
346
|
+
const within = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))]);
|
|
347
|
+
const usdc = FUNDING_ASSETS.find((a) => a.symbol === 'USDC');
|
|
348
|
+
const tok = FUNDING_ASSETS.find((a) => a.symbol === 'TOKEN');
|
|
349
|
+
const [u, t] = await within(Promise.all([
|
|
350
|
+
usdc ? tokenBalance(conn, w.keypair.publicKey, usdc.mint) : { ui: 0 },
|
|
351
|
+
tok ? tokenBalance(conn, w.keypair.publicKey, tok.mint) : { ui: 0 },
|
|
352
|
+
]), 6000);
|
|
353
|
+
let baseUsdc = null;
|
|
354
|
+
try {
|
|
355
|
+
const { evmRpcFor, EVM_FUNDING_ASSETS } = await import('./config.js');
|
|
356
|
+
const b = (EVM_FUNDING_ASSETS.base || []).find((a) => a.symbol === 'USDC');
|
|
357
|
+
if (b) {
|
|
358
|
+
const r = await within(evmTokenBalance({ rpcUrl: evmRpcFor('base'), owner: addrs.evm, token: b.address }), 6000);
|
|
359
|
+
const raw = typeof r === 'object' && r !== null ? (r.raw ?? r.ui ?? 0) : r;
|
|
360
|
+
baseUsdc = Number(raw) / 10 ** (b.decimals ?? 6); // raw units → dollars
|
|
361
|
+
}
|
|
362
|
+
} catch { /* base unreachable: print ? */ }
|
|
363
|
+
// TOKEN is priced at the 402, not here: show units, never an invented $.
|
|
364
|
+
balances = { USDC: Number(u?.ui ?? 0), TOKEN_UNITS: t?.ui != null ? Number(t.ui) : null, BASE_USDC: baseUsdc };
|
|
365
|
+
} catch { balances = null; }
|
|
366
|
+
let chromeMode = 'own-profile';
|
|
367
|
+
try { const { chromeStatus } = await import('./mcpbridge.js'); chromeMode = chromeStatus().mode; } catch { /* */ }
|
|
368
|
+
for (const l of payBannerLines({ ...addrs, balances, chromeMode })) console.error(l);
|
|
369
|
+
} catch (e) {
|
|
370
|
+
console.error(`openzoo: (pay banner unavailable: ${e.message}) — run: openzoo balance`);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
setTimeout(() => { payBanner().catch(() => {}); }, plan.spawn ? 7000 : 2500);
|
|
329
374
|
if (argv.includes('--once')) return;
|
|
330
375
|
await new Promise(() => {});
|
|
331
376
|
}
|
package/lib/mcpbridge.js
CHANGED
|
@@ -349,7 +349,7 @@ async function connectOne(cfg, log) {
|
|
|
349
349
|
try {
|
|
350
350
|
transport.stderr?.on?.('data', (buf) => {
|
|
351
351
|
const line = String(buf).trim().split('\n')[0];
|
|
352
|
-
if (line && !/No handler registered for issue code/.test(line)) log?.(`cursor-backend: mcp ${cfg.name} ${line.slice(0, 160)}`);
|
|
352
|
+
if (line && !/No handler registered for issue code|exposes content of the browser|Performance tools may send|did not negotiate the MCP roots/.test(line)) log?.(`cursor-backend: mcp ${cfg.name} ${line.slice(0, 160)}`);
|
|
353
353
|
});
|
|
354
354
|
} catch { /* */ }
|
|
355
355
|
}
|
package/lib/proxy.js
CHANGED
|
@@ -309,8 +309,10 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
309
309
|
// (OpenRouter) was out of credits. Paying again buys nothing, and the
|
|
310
310
|
// "wallet underfunded" copy blamed the user's burner. When the paid 402 says
|
|
311
311
|
// upstream credits, stop paying for a minute and say what is actually wrong.
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
// Keyed by model: an OpenRouter outage must not gate a door-only id like
|
|
313
|
+
// bare grok-4.6 that never touches OpenRouter.
|
|
314
|
+
const upstreamOutage_ = new Map(); // model -> { until, msg }
|
|
315
|
+
const outageKey = (init) => { try { return String(JSON.parse(String(init?.body || '{}')).model || ''); } catch { return ''; } };
|
|
314
316
|
const upstreamOutage = (body) => {
|
|
315
317
|
const m = String(body?.error?.message || body?.message || '');
|
|
316
318
|
const src = String(body?.error?.metadata?.limit_source || '');
|
|
@@ -696,10 +698,13 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
696
698
|
}
|
|
697
699
|
|
|
698
700
|
try {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
701
|
+
{
|
|
702
|
+
const gate = upstreamOutage_.get(outageKey(init));
|
|
703
|
+
if (gate && Date.now() < gate.until) {
|
|
704
|
+
log(`upstream outage: not paying (gate) model=${outageKey(init)}`);
|
|
705
|
+
jsonErr(res, 503, gate.msg);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
703
708
|
}
|
|
704
709
|
const result = await client.fetch(url, init);
|
|
705
710
|
const { response, paid, receipt, accept } = result;
|
|
@@ -808,13 +813,13 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
808
813
|
if (paid && upstreamOutage(q402)) {
|
|
809
814
|
const settle = q402?.x402?.settle;
|
|
810
815
|
const tx = settle?.transaction || result.receipt?.tx || '';
|
|
811
|
-
|
|
816
|
+
const msg = 'openzoo gateway upstream is out of credits (OpenRouter: "Insufficient credits"). '
|
|
812
817
|
+ `Your payment settled${tx ? ` (tx ${tx})` : ''} — this is NOT your wallet. `
|
|
813
818
|
+ 'The gateway operator must top up OpenRouter or route this model to another upstream. '
|
|
814
|
-
+
|
|
815
|
-
|
|
816
|
-
log(`upstream outage: ${
|
|
817
|
-
jsonErr(res, 503,
|
|
819
|
+
+ `Pausing paid retries for ${outageKey(init) || 'this model'} for 60s.`;
|
|
820
|
+
upstreamOutage_.set(outageKey(init), { until: Date.now() + 60_000, msg });
|
|
821
|
+
log(`upstream outage: model=${outageKey(init)} ${msg.slice(0, 100)}`);
|
|
822
|
+
jsonErr(res, 503, msg);
|
|
818
823
|
return;
|
|
819
824
|
}
|
|
820
825
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.72",
|
|
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",
|