openzoo 0.50.73 → 0.50.75
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/lib/botlog.js +60 -2
- package/lib/cursorbackend.js +30 -4
- package/lib/grokcli.js +2 -31
- package/lib/proxy.js +35 -5
- package/package.json +1 -1
package/lib/botlog.js
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* Default is QUIET: only milestones and problems. `--verbose` / OPENZOO_DEBUG=1
|
|
10
10
|
* restores the firehose (it is still the right thing for debugging the wire).
|
|
11
11
|
*/
|
|
12
|
+
import fsSync from 'node:fs';
|
|
13
|
+
import osMod from 'node:os';
|
|
14
|
+
import pathMod from 'node:path';
|
|
12
15
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
13
16
|
|
|
14
17
|
const NOISE = [
|
|
@@ -30,6 +33,8 @@ const NOISE = [
|
|
|
30
33
|
];
|
|
31
34
|
|
|
32
35
|
const MILESTONE = [
|
|
36
|
+
/zoo POST :8402 model=/, // one line per turn: which bot asked which model
|
|
37
|
+
/<< zoo (200|4\d\d|5\d\d)/, // one line per turn: how it ended
|
|
33
38
|
/mcp ready/,
|
|
34
39
|
/mcp \S+ (mode=|FAIL|tools=\d+|re-attach|attached|appeared)/,
|
|
35
40
|
/mcp \S+ To let bots drive your real Chrome/,
|
|
@@ -49,11 +54,29 @@ export function isBotMilestone(line) {
|
|
|
49
54
|
return MILESTONE.some((re) => re.test(s));
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Terminal gets milestones (or everything with verbose); the FULL stream is
|
|
59
|
+
* always appended to `file` so quiet mode never destroys evidence.
|
|
60
|
+
* Default file: ~/.openzoo/bot.log (truncated at start of each run).
|
|
61
|
+
*/
|
|
62
|
+
export function makeBotLogger({ verbose = false, write = (m) => console.error(m), file = defaultBotLogPath(), fsMod = null } = {}) {
|
|
63
|
+
let fd = null;
|
|
64
|
+
if (file) {
|
|
65
|
+
try {
|
|
66
|
+
const fsx = fsMod || fsSync;
|
|
67
|
+
fsx.mkdirSync(pathMod.dirname(file), { recursive: true });
|
|
68
|
+
fd = fsx.openSync(file, 'w');
|
|
69
|
+
fsx.writeSync(fd, `# openzoo bot full log ${new Date().toISOString()}\n`);
|
|
70
|
+
} catch { fd = null; }
|
|
71
|
+
}
|
|
53
72
|
return (m) => {
|
|
73
|
+
if (fd != null) { try { (fsMod || fsSync).writeSync(fd, `${new Date().toISOString()} ${m}\n`); } catch { /* disk full etc. */ } }
|
|
54
74
|
if (verbose || isBotMilestone(m)) write(` backend: ${m}`);
|
|
55
75
|
};
|
|
56
76
|
}
|
|
77
|
+
export function defaultBotLogPath(home = osMod.homedir()) {
|
|
78
|
+
return pathMod.join(home, '.openzoo', 'bot.log');
|
|
79
|
+
}
|
|
57
80
|
|
|
58
81
|
/**
|
|
59
82
|
* The block a new user needs before anything else. `balances` is optional
|
|
@@ -86,7 +109,7 @@ export function payBannerLines({ solana, evm, balances = null, whop = 'https://w
|
|
|
86
109
|
lines.push(`openzoo: CHROME attached to your real browser (${chromeMode}).`);
|
|
87
110
|
}
|
|
88
111
|
lines.push('openzoo: FIRST type in any bot: "set up Grok Ship for ~/path/to/repo" — or just give it work.');
|
|
89
|
-
lines.push('openzoo: QUIET add --verbose to see every request the app makes.');
|
|
112
|
+
lines.push('openzoo: QUIET add --verbose to see every request the app makes; the full stream is always in ~/.openzoo/bot.log');
|
|
90
113
|
return lines;
|
|
91
114
|
}
|
|
92
115
|
|
|
@@ -96,3 +119,38 @@ export function walletAddresses(wallet) {
|
|
|
96
119
|
evm: privateKeyToAccount(wallet.evmPrivateKey).address,
|
|
97
120
|
};
|
|
98
121
|
}
|
|
122
|
+
|
|
123
|
+
/** Live balances for the banner: Solana USDC/TOKEN/LEOS + Base USDC. Never throws; unknowns are null. */
|
|
124
|
+
export async function quickBalances(addrs, { timeoutMs = 6000 } = {}) {
|
|
125
|
+
const within = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))]);
|
|
126
|
+
const out = { USDC: null, TOKEN_UNITS: null, LEOS_UNITS: null, BASE_USDC: null };
|
|
127
|
+
try {
|
|
128
|
+
const { Connection } = await import('@solana/web3.js');
|
|
129
|
+
const { config, FUNDING_ASSETS } = await import('./config.js');
|
|
130
|
+
const { tokenBalance } = await import('./x402.js');
|
|
131
|
+
const { loadOrCreateWallet } = await import('./wallet.js');
|
|
132
|
+
const w = loadOrCreateWallet();
|
|
133
|
+
const conn = new Connection(config.rpcUrl, 'confirmed');
|
|
134
|
+
const find = (sym) => FUNDING_ASSETS.find((a) => a.symbol === sym);
|
|
135
|
+
const [u, t, l] = await within(Promise.all(['USDC', 'TOKEN', 'LEOS'].map((sym) => (find(sym) ? tokenBalance(conn, w.keypair.publicKey, find(sym).mint) : { ui: 0 }))), timeoutMs);
|
|
136
|
+
out.USDC = Number(u?.ui ?? 0);
|
|
137
|
+
out.TOKEN_UNITS = t?.ui != null ? Number(t.ui) : null;
|
|
138
|
+
out.LEOS_UNITS = l?.ui != null ? Number(l.ui) : null;
|
|
139
|
+
} catch { /* solana unreachable */ }
|
|
140
|
+
try {
|
|
141
|
+
const { evmRpcFor, EVM_FUNDING_ASSETS } = await import('./config.js');
|
|
142
|
+
const { evmTokenBalance } = await import('./evm.js');
|
|
143
|
+
const b = (EVM_FUNDING_ASSETS.base || []).find((a) => a.symbol === 'USDC');
|
|
144
|
+
if (b) {
|
|
145
|
+
const r = await within(evmTokenBalance({ rpcUrl: evmRpcFor('base'), owner: addrs.evm, token: b.address }), timeoutMs);
|
|
146
|
+
const raw = typeof r === 'object' && r !== null ? (r.raw ?? r.ui ?? 0) : r;
|
|
147
|
+
out.BASE_USDC = Number(raw) / 10 ** (b.decimals ?? 6);
|
|
148
|
+
}
|
|
149
|
+
} catch { /* base unreachable */ }
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The banner as a chat message: same facts, no terminal prefixes. */
|
|
154
|
+
export function payBannerChat(opts) {
|
|
155
|
+
return ['[how to pay]', ...payBannerLines(opts).map((l) => l.replace(/^openzoo: /, '').replace(/^ {9}/, ''))].join('\n');
|
|
156
|
+
}
|
package/lib/cursorbackend.js
CHANGED
|
@@ -1467,6 +1467,29 @@ export function shipNudgeText() {
|
|
|
1467
1467
|
'I will create Firstmate (the one bot you talk to) and a crewmate for that repo. Then give Firstmate ship tasks: it runs a worker on a branch, a fresh review of the diff, and opens the PR only when the review is clean. You merge.',
|
|
1468
1468
|
].join('\n');
|
|
1469
1469
|
}
|
|
1470
|
+
let payNudged = false;
|
|
1471
|
+
/** The terminal banner, on the canvas too: a new user reads the chat, not the shell. */
|
|
1472
|
+
async function seedPayNudge(list, activeId, log = () => {}) {
|
|
1473
|
+
if (payNudged || !activeId) return false;
|
|
1474
|
+
payNudged = true;
|
|
1475
|
+
try {
|
|
1476
|
+
const t = agentTranscript(activeId);
|
|
1477
|
+
if ((t.entries || []).some((e) => /\[how to pay\]/.test(String(e?.content || e?.message?.content || '')))) return false;
|
|
1478
|
+
const { payBannerChat, walletAddresses, quickBalances } = await import('./botlog.js');
|
|
1479
|
+
const { loadOrCreateWallet } = await import('./wallet.js');
|
|
1480
|
+
const addrs = walletAddresses(loadOrCreateWallet());
|
|
1481
|
+
const balances = await quickBalances(addrs);
|
|
1482
|
+
const text = payBannerChat({ ...addrs, balances, chromeMode: chromeStatus().mode });
|
|
1483
|
+
const nonce = `oz-pay-nudge-${activeId}`;
|
|
1484
|
+
const line = fanoutLine(activeId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
|
|
1485
|
+
ssePush('transcript', { ...gatewayEntry(line), agentId: activeId });
|
|
1486
|
+
log(`cursor-backend: how-to-pay painted on ${activeId}`);
|
|
1487
|
+
return true;
|
|
1488
|
+
} catch (e) {
|
|
1489
|
+
log(`cursor-backend: how-to-pay nudge failed ${e.message}`);
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1470
1493
|
let shipNudged = false;
|
|
1471
1494
|
function seedShipNudge(list, activeId) {
|
|
1472
1495
|
if (shipNudged || !activeId) return false;
|
|
@@ -1847,9 +1870,11 @@ const MODEL_ALIASES = {
|
|
|
1847
1870
|
'glm-5.3': 'zai-org/glm-5.3-flash',
|
|
1848
1871
|
'glm-5.3-flash': 'zai-org/glm-5.3-flash',
|
|
1849
1872
|
flash: 'zai-org/glm-5.3-flash',
|
|
1850
|
-
|
|
1851
|
-
'
|
|
1852
|
-
|
|
1873
|
+
// NEVER OPENROUTER for Grok Bot: `auto` is the gateway router over
|
|
1874
|
+
// OpenRouter's catalog, so it lands on the door-only id instead.
|
|
1875
|
+
auto: 'grok-4.6',
|
|
1876
|
+
'openrouter/auto': 'grok-4.6',
|
|
1877
|
+
'openzoo/auto': 'grok-4.6',
|
|
1853
1878
|
deepseek: 'deepseek/deepseek-v4-pro',
|
|
1854
1879
|
'deepseek-pro': 'deepseek/deepseek-v4-pro',
|
|
1855
1880
|
'deepseek-flash': 'deepseek/deepseek-v4-flash',
|
|
@@ -1863,7 +1888,7 @@ const MODEL_ALIASES = {
|
|
|
1863
1888
|
* — the bazaar (x402 upstream) row, not OpenRouter's `x-ai/grok-4.6`. The
|
|
1864
1889
|
* gateway serves the bare id off an x402 door with an on-chain cogs receipt,
|
|
1865
1890
|
* so an OpenRouter credit outage cannot take it down. */
|
|
1866
|
-
export const DEFAULT_ZOO_MODEL = '
|
|
1891
|
+
export const DEFAULT_ZOO_MODEL = 'grok-4.6';
|
|
1867
1892
|
async function resolveModelId(raw) {
|
|
1868
1893
|
const s = String(raw || '').trim();
|
|
1869
1894
|
if (!s) return null;
|
|
@@ -3350,6 +3375,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
3350
3375
|
});
|
|
3351
3376
|
}
|
|
3352
3377
|
if (seedShipNudge(list, active)) log(`cursor-backend: ship nudge painted on ${active}`);
|
|
3378
|
+
if (active && !payNudged) setTimeout(() => { seedPayNudge(list, active, log).catch(() => {}); }, 4000);
|
|
3353
3379
|
log(`cursor-backend: listAgents local n=${list.length} account=${activeAccountId || 'none'} active=${active || 'none'}`);
|
|
3354
3380
|
return true;
|
|
3355
3381
|
}
|
package/lib/grokcli.js
CHANGED
|
@@ -334,37 +334,8 @@ export async function runBot(argv = []) {
|
|
|
334
334
|
try {
|
|
335
335
|
const { loadOrCreateWallet } = await import('./wallet.js');
|
|
336
336
|
const addrs = walletAddresses(loadOrCreateWallet());
|
|
337
|
-
|
|
338
|
-
|
|
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 leos = FUNDING_ASSETS.find((a) => a.symbol === 'LEOS');
|
|
350
|
-
const [u, t, l] = await within(Promise.all([
|
|
351
|
-
usdc ? tokenBalance(conn, w.keypair.publicKey, usdc.mint) : { ui: 0 },
|
|
352
|
-
tok ? tokenBalance(conn, w.keypair.publicKey, tok.mint) : { ui: 0 },
|
|
353
|
-
leos ? tokenBalance(conn, w.keypair.publicKey, leos.mint) : { ui: 0 },
|
|
354
|
-
]), 6000);
|
|
355
|
-
let baseUsdc = null;
|
|
356
|
-
try {
|
|
357
|
-
const { evmRpcFor, EVM_FUNDING_ASSETS } = await import('./config.js');
|
|
358
|
-
const b = (EVM_FUNDING_ASSETS.base || []).find((a) => a.symbol === 'USDC');
|
|
359
|
-
if (b) {
|
|
360
|
-
const r = await within(evmTokenBalance({ rpcUrl: evmRpcFor('base'), owner: addrs.evm, token: b.address }), 6000);
|
|
361
|
-
const raw = typeof r === 'object' && r !== null ? (r.raw ?? r.ui ?? 0) : r;
|
|
362
|
-
baseUsdc = Number(raw) / 10 ** (b.decimals ?? 6); // raw units → dollars
|
|
363
|
-
}
|
|
364
|
-
} catch { /* base unreachable: print ? */ }
|
|
365
|
-
// TOKEN is priced at the 402, not here: show units, never an invented $.
|
|
366
|
-
balances = { USDC: Number(u?.ui ?? 0), TOKEN_UNITS: t?.ui != null ? Number(t.ui) : null, LEOS_UNITS: l?.ui != null ? Number(l.ui) : null, BASE_USDC: baseUsdc };
|
|
367
|
-
} catch { balances = null; }
|
|
337
|
+
const { quickBalances } = await import('./botlog.js');
|
|
338
|
+
const balances = await quickBalances(addrs);
|
|
368
339
|
let chromeMode = 'own-profile';
|
|
369
340
|
try { const { chromeStatus } = await import('./mcpbridge.js'); chromeMode = chromeStatus().mode; } catch { /* */ }
|
|
370
341
|
for (const l of payBannerLines({ ...addrs, balances, chromeMode })) console.error(l);
|
package/lib/proxy.js
CHANGED
|
@@ -640,7 +640,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
640
640
|
return;
|
|
641
641
|
}
|
|
642
642
|
}
|
|
643
|
-
|
|
643
|
+
let init = { method: req.method, headers: upstreamHeaders(req) };
|
|
644
644
|
if (req.method !== 'GET' && req.method !== 'HEAD') init.body = bodyBuf;
|
|
645
645
|
|
|
646
646
|
// Harnesses validate their configured model BEFORE ever POSTing — some
|
|
@@ -698,15 +698,45 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
698
698
|
}
|
|
699
699
|
|
|
700
700
|
try {
|
|
701
|
+
// OUTAGE FALLBACK. OpenRouter dry does not have to mean dead bots: the
|
|
702
|
+
// x402 door for bare grok-4.6 settles cogs on chain and never touches
|
|
703
|
+
// OpenRouter. When a model is gated (or comes back "Insufficient
|
|
704
|
+
// credits" below), the same request goes out once more on the fallback.
|
|
705
|
+
const FALLBACK_MODEL = String(process.env.OPENZOO_OUTAGE_FALLBACK || 'grok-4.6');
|
|
706
|
+
const withModel = (i, model) => {
|
|
707
|
+
try { const b = JSON.parse(String(i.body || '{}')); b.model = model; return { ...i, body: JSON.stringify(b) }; } catch { return i; }
|
|
708
|
+
};
|
|
709
|
+
let usedFallback = false;
|
|
701
710
|
{
|
|
702
711
|
const gate = upstreamOutage_.get(outageKey(init));
|
|
703
712
|
if (gate && Date.now() < gate.until) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
713
|
+
if (FALLBACK_MODEL && outageKey(init) !== FALLBACK_MODEL) {
|
|
714
|
+
log(`upstream outage: ${outageKey(init)} gated -> ${FALLBACK_MODEL}`);
|
|
715
|
+
init = withModel(init, FALLBACK_MODEL);
|
|
716
|
+
usedFallback = true;
|
|
717
|
+
} else {
|
|
718
|
+
log(`upstream outage: not paying (gate) model=${outageKey(init)}`);
|
|
719
|
+
jsonErr(res, 503, gate.msg);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
let result = await client.fetch(url, init);
|
|
725
|
+
// Paid, then the gateway said its upstream is out of credits: gate this
|
|
726
|
+
// model for 60s and buy the same completion from the door instead.
|
|
727
|
+
if (result.paid && result.response?.status === 402 && !usedFallback && FALLBACK_MODEL && outageKey(init) !== FALLBACK_MODEL) {
|
|
728
|
+
let q402 = null;
|
|
729
|
+
try { q402 = await result.response.clone().json(); } catch { q402 = null; }
|
|
730
|
+
if (upstreamOutage(q402)) {
|
|
731
|
+
const tx = q402?.x402?.settle?.transaction || result.receipt?.tx || '';
|
|
732
|
+
const msg = `openzoo gateway upstream is out of credits (OpenRouter: "Insufficient credits") for ${outageKey(init)}; that payment${tx ? ` (tx ${tx})` : ''} is credited back by the gateway. Routing to ${FALLBACK_MODEL} (x402 door) for 60s.`;
|
|
733
|
+
upstreamOutage_.set(outageKey(init), { until: Date.now() + 60_000, msg });
|
|
734
|
+
log(`upstream outage: ${outageKey(init)} -> ${FALLBACK_MODEL}`);
|
|
735
|
+
init = withModel(init, FALLBACK_MODEL);
|
|
736
|
+
usedFallback = true;
|
|
737
|
+
result = await client.fetch(url, init);
|
|
707
738
|
}
|
|
708
739
|
}
|
|
709
|
-
const result = await client.fetch(url, init);
|
|
710
740
|
const { response, paid, receipt, accept } = result;
|
|
711
741
|
if (paid && receipt) {
|
|
712
742
|
if (receipt.ok && typeof receipt.billedUsd === 'number') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.75",
|
|
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",
|