openzoo 0.50.97 → 0.50.99
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/cursor-backend.js +1 -1
- package/bin/openzoo.js +9 -0
- package/lib/cursorapi.js +65 -0
- package/lib/cursorbackend.js +879 -77
- package/lib/grokbotAccount.js +192 -3
- package/lib/grokbotFetch.js +258 -0
- package/lib/grokbotweb.js +23 -7
- package/lib/grokcli.js +454 -44
- package/lib/grokui.mjs +6524 -0
- package/lib/launch.js +1 -1
- package/lib/models.js +11 -1
- package/lib/openzooPathShim.js +60 -0
- package/lib/ozSpendChip.js +80 -3
- package/lib/pay.js +30 -2
- package/lib/podagent.mjs +3 -1
- package/lib/proxy.js +17 -2
- package/lib/setup.js +1 -1
- package/package.json +1 -1
- package/lib/cursorbackend.js.bak-hiddenbots +0 -4072
package/lib/launch.js
CHANGED
|
@@ -559,7 +559,7 @@ export async function launchClaude(argv) {
|
|
|
559
559
|
try {
|
|
560
560
|
const { blockBackend, bindBackend443, unblockBackend, CLAUDE_HOSTS } = await import('./hosts.js');
|
|
561
561
|
const { ensureCert } = await import('./cursorbackend.js');
|
|
562
|
-
ensureCert(console.error);
|
|
562
|
+
await ensureCert(console.error);
|
|
563
563
|
const r = blockBackend(CLAUDE_HOSTS);
|
|
564
564
|
console.error(r.already ? 'openzoo: api.anthropic.com already routed to us'
|
|
565
565
|
: `openzoo: routing ${CLAUDE_HOSTS.join(', ')} -> 127.0.0.1 (sudo)`);
|
package/lib/models.js
CHANGED
|
@@ -192,7 +192,17 @@ export function unopenrouter(requested, ids) {
|
|
|
192
192
|
const bare = s.slice(slash + 1);
|
|
193
193
|
if (!bare || bare.includes('/')) return null;
|
|
194
194
|
if (mode === '1') return bare;
|
|
195
|
-
|
|
195
|
+
if (!Array.isArray(ids) || !ids.length) return null;
|
|
196
|
+
if (ids.includes(s)) return null;
|
|
197
|
+
if (ids.includes(bare)) return bare;
|
|
198
|
+
// Telnyx advertises `zai-org/GLM-5.2`, not OpenRouter's `z-ai/glm-5.2`.
|
|
199
|
+
const want = bare.toLowerCase().replace(/[._]+/g, '-');
|
|
200
|
+
const hit = ids.find((id) => {
|
|
201
|
+
if (id === s) return false;
|
|
202
|
+
const suffix = String(id).slice(String(id).lastIndexOf('/') + 1).toLowerCase().replace(/[._]+/g, '-');
|
|
203
|
+
return suffix === want;
|
|
204
|
+
});
|
|
205
|
+
return hit || null;
|
|
196
206
|
}
|
|
197
207
|
|
|
198
208
|
export function resolveModel(requested, ids) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Install a PATH shim so `openzoo` works without a system Node.
|
|
2
|
+
//
|
|
3
|
+
// grokui-app already ran the sidecar with Electron's own binary
|
|
4
|
+
// (`ELECTRON_RUN_AS_NODE=1`). That only helps the GUI. A packaged OpenZoo Bot
|
|
5
|
+
// also drops `~/.local/bin/openzoo` (Windows: %LOCALAPPDATA%\openzoo\bin)
|
|
6
|
+
// pointing at the same Electron + bundled bin/openzoo.js, so the CLI
|
|
7
|
+
// (`openzoo claude`, `openzoo balance`, …) works from a Finder/Dock launch
|
|
8
|
+
// that has no ~/.zshrc PATH and no npx.
|
|
9
|
+
|
|
10
|
+
import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export function shimDir(home = homedir(), platform = process.platform) {
|
|
15
|
+
if (platform === 'win32') return join(home, 'AppData', 'Local', 'openzoo', 'bin');
|
|
16
|
+
return join(home, '.local', 'bin');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function shimPath(home = homedir(), platform = process.platform) {
|
|
20
|
+
const name = platform === 'win32' ? 'openzoo.cmd' : 'openzoo';
|
|
21
|
+
return join(shimDir(home, platform), name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function unixShimBody({ execPath, openzooJs }) {
|
|
25
|
+
return `#!/bin/sh
|
|
26
|
+
# Written by OpenZoo Bot. Electron-as-node — no system Node required.
|
|
27
|
+
export ELECTRON_RUN_AS_NODE=1
|
|
28
|
+
exec ${shellQuote(execPath)} ${shellQuote(openzooJs)} "$@"
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function winShimBody({ execPath, openzooJs }) {
|
|
33
|
+
return `@echo off
|
|
34
|
+
set ELECTRON_RUN_AS_NODE=1
|
|
35
|
+
"${execPath}" "${openzooJs}" %*
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function shellQuote(s) {
|
|
40
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function writeOpenzooShim({
|
|
44
|
+
execPath,
|
|
45
|
+
openzooJs,
|
|
46
|
+
home = homedir(),
|
|
47
|
+
platform = process.platform,
|
|
48
|
+
} = {}) {
|
|
49
|
+
if (!execPath || !openzooJs) throw new Error('writeOpenzooShim needs execPath + openzooJs');
|
|
50
|
+
if (!existsSync(openzooJs)) throw new Error(`openzoo.js missing: ${openzooJs}`);
|
|
51
|
+
const dir = shimDir(home, platform);
|
|
52
|
+
mkdirSync(dir, { recursive: true });
|
|
53
|
+
const dest = shimPath(home, platform);
|
|
54
|
+
const body = platform === 'win32'
|
|
55
|
+
? winShimBody({ execPath, openzooJs })
|
|
56
|
+
: unixShimBody({ execPath, openzooJs });
|
|
57
|
+
writeFileSync(dest, body, { encoding: 'utf8' });
|
|
58
|
+
if (platform !== 'win32') chmodSync(dest, 0o755);
|
|
59
|
+
return dest;
|
|
60
|
+
}
|
package/lib/ozSpendChip.js
CHANGED
|
@@ -237,6 +237,14 @@ function ozEnsureSpendCss() {
|
|
|
237
237
|
'[data-oz-spend-hide]{display:none!important}',
|
|
238
238
|
'button[aria-label="Start voice input"],button[aria-label*="voice input" i],',
|
|
239
239
|
'button[aria-label*="steminvoer" i],button[aria-label^="Microphone"]{display:none!important}',
|
|
240
|
+
'#oz-share-hud{position:fixed;right:16px;bottom:148px;z-index:2147483646;max-width:min(28em,calc(100vw - 24px));',
|
|
241
|
+
'pointer-events:auto;font:600 13px/1.3 system-ui,sans-serif}',
|
|
242
|
+
'#oz-share-hud[data-feature="oz-share-hud"]{display:block}',
|
|
243
|
+
'#oz-share-hud>button,#oz-share-hud>a{display:block;width:100%;text-align:left;cursor:pointer;',
|
|
244
|
+
'background:#2563eb;color:#fff;border:0;border-radius:999px;padding:6px 12px;box-shadow:0 4px 18px rgba(0,0,0,.5);',
|
|
245
|
+
'text-decoration:none}',
|
|
246
|
+
'#oz-share-hud .oz-share-url{display:block;margin-top:6px;padding:8px 10px;border-radius:10px;',
|
|
247
|
+
'background:rgba(20,20,22,.94);color:#93c5fd;font:12px/1.4 ui-monospace,monospace;overflow-wrap:anywhere}',
|
|
240
248
|
].join('');
|
|
241
249
|
}
|
|
242
250
|
|
|
@@ -560,6 +568,71 @@ function ozPollSpend() {
|
|
|
560
568
|
})();
|
|
561
569
|
}
|
|
562
570
|
|
|
571
|
+
function ozShareCopy(url) {
|
|
572
|
+
const u = String(url || '');
|
|
573
|
+
if (!u) return;
|
|
574
|
+
try { navigator.clipboard.writeText(u); } catch (e) {}
|
|
575
|
+
try { window.open(u, '_blank', 'noopener'); } catch (e) {}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function ozEnsureShareHud(state) {
|
|
579
|
+
const url = state && typeof state.url === 'string' ? state.url.trim() : '';
|
|
580
|
+
const rooms = (state && Array.isArray(state.rooms) ? state.rooms : []).filter((r) => r && r.url);
|
|
581
|
+
const show = !!url || !!(state && state.isGroup && rooms.length);
|
|
582
|
+
let el = document.getElementById('oz-share-hud');
|
|
583
|
+
if (!show) {
|
|
584
|
+
if (el) el.remove();
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
ozEnsureSpendCss();
|
|
588
|
+
if (!el) {
|
|
589
|
+
el = document.createElement('div');
|
|
590
|
+
el.id = 'oz-share-hud';
|
|
591
|
+
el.setAttribute('data-feature', 'oz-share-hud');
|
|
592
|
+
el.setAttribute('data-testid', 'oz-share-hud');
|
|
593
|
+
document.body.appendChild(el);
|
|
594
|
+
}
|
|
595
|
+
const primary = url || (rooms[0] && rooms[0].url) || '';
|
|
596
|
+
const lines = url
|
|
597
|
+
? [url]
|
|
598
|
+
: rooms.map((r) => (r.name ? r.name + ' ' : '') + r.url);
|
|
599
|
+
el.replaceChildren();
|
|
600
|
+
const btn = document.createElement('button');
|
|
601
|
+
btn.type = 'button';
|
|
602
|
+
btn.setAttribute('data-testid', 'oz-share-copy');
|
|
603
|
+
btn.textContent = url ? 'Share group' : 'Share grokroom';
|
|
604
|
+
btn.addEventListener('click', () => ozShareCopy(primary));
|
|
605
|
+
el.appendChild(btn);
|
|
606
|
+
const box = document.createElement('div');
|
|
607
|
+
box.className = 'oz-share-url';
|
|
608
|
+
box.setAttribute('data-testid', 'oz-share-url');
|
|
609
|
+
box.textContent = lines.join('\n');
|
|
610
|
+
el.appendChild(box);
|
|
611
|
+
if (primary) {
|
|
612
|
+
const hint = document.createElement('div');
|
|
613
|
+
hint.setAttribute('data-testid', 'oz-share-join-hint');
|
|
614
|
+
hint.style.cssText = 'margin-top:6px;font:12px/1.35 ui-sans-serif,system-ui,sans-serif;opacity:.85;word-break:break-all';
|
|
615
|
+
hint.textContent = 'Others in OpenZoo Bot: /join ' + primary + ' — then add their bots to the group.';
|
|
616
|
+
el.appendChild(hint);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function ozPollShare() {
|
|
621
|
+
const urls = ['/oz-share', '/api/ozShare', 'https://127.0.0.1:8443/api/ozShare'];
|
|
622
|
+
(async () => {
|
|
623
|
+
for (let i = 0; i < urls.length; i += 1) {
|
|
624
|
+
try {
|
|
625
|
+
const r = await fetch(urls[i], { signal: AbortSignal.timeout(1500) });
|
|
626
|
+
if (!r || !r.ok) continue;
|
|
627
|
+
const j = await r.json();
|
|
628
|
+
if (!j || j.ok === false) continue;
|
|
629
|
+
ozEnsureShareHud(j);
|
|
630
|
+
return;
|
|
631
|
+
} catch (e) {}
|
|
632
|
+
}
|
|
633
|
+
})();
|
|
634
|
+
}
|
|
635
|
+
|
|
563
636
|
function ozWatchSpend() {
|
|
564
637
|
let t = 0;
|
|
565
638
|
const inComposer = () => ozInComposer(document.activeElement);
|
|
@@ -583,16 +656,17 @@ function ozWatchSpend() {
|
|
|
583
656
|
else document.addEventListener('DOMContentLoaded', start);
|
|
584
657
|
} catch (e) {}
|
|
585
658
|
try { window.addEventListener('resize', debounced); } catch (e) {}
|
|
586
|
-
try { setInterval(() => { run(); ozPollSpend(); }, 2000); } catch (e) {}
|
|
659
|
+
try { setInterval(() => { run(); ozPollSpend(); ozPollShare(); }, 2000); } catch (e) {}
|
|
587
660
|
try { ozPollSpend(); } catch (e) {}
|
|
661
|
+
try { ozPollShare(); } catch (e) {}
|
|
588
662
|
}
|
|
589
663
|
|
|
590
664
|
export function spendChipSource() {
|
|
591
665
|
return [
|
|
592
666
|
'(function ozSpendChip(){',
|
|
593
667
|
"'use strict';",
|
|
594
|
-
'if (window.__OZ_SPEND_CHIP__ ===
|
|
595
|
-
'window.__OZ_SPEND_CHIP__ =
|
|
668
|
+
'if (window.__OZ_SPEND_CHIP__ === 18) return;',
|
|
669
|
+
'window.__OZ_SPEND_CHIP__ = 18;',
|
|
596
670
|
chipUsd.toString(),
|
|
597
671
|
labelFromSpendBody.toString(),
|
|
598
672
|
spendLinesOnly.toString(),
|
|
@@ -615,6 +689,9 @@ export function spendChipSource() {
|
|
|
615
689
|
ozDragFloat.toString(),
|
|
616
690
|
ozLabelFromInfo.toString(),
|
|
617
691
|
ozPollSpend.toString(),
|
|
692
|
+
ozShareCopy.toString(),
|
|
693
|
+
ozEnsureShareHud.toString(),
|
|
694
|
+
ozPollShare.toString(),
|
|
618
695
|
ozWatchSpend.toString(),
|
|
619
696
|
'ozWatchSpend();',
|
|
620
697
|
'})();',
|
package/lib/pay.js
CHANGED
|
@@ -63,16 +63,39 @@ export class UnderfundedError extends Error {
|
|
|
63
63
|
* rest. A payment still decrements the cached figure so a burst cannot overdraw.
|
|
64
64
|
*/
|
|
65
65
|
const BALANCE_TTL_MS = Number(process.env.OPENZOO_BALANCE_TTL_MS || 8_000);
|
|
66
|
+
/**
|
|
67
|
+
* HARD CEILING ON STALENESS — the SWR branch has no age bound of its own.
|
|
68
|
+
*
|
|
69
|
+
* It returns the cached figure and merely KICKS OFF a refresh, so a positive
|
|
70
|
+
* entry was served forever whenever the refresh kept losing: an RPC that keeps
|
|
71
|
+
* throwing leaves `refreshing` false and the old number in place, and several
|
|
72
|
+
* workers sharing one wallet never see each other's spends at all — each
|
|
73
|
+
* process only ever debits what IT paid.
|
|
74
|
+
*
|
|
75
|
+
* MEASURED 2026-09-03: deposit wallet 4QtgGwMt… holding 0.003603 USDC kept
|
|
76
|
+
* signing the 0.009204 USDC row. Every settle came back `Error: insufficient
|
|
77
|
+
* funds` (Tokenkeg custom program error 0x1) in a tight retry loop, while the
|
|
78
|
+
* TOKEN row it could pay (47,411 held vs 19.28 needed) sat next to it in the
|
|
79
|
+
* same accepts[]. Past this ceiling, block on a live read instead of guessing.
|
|
80
|
+
*/
|
|
81
|
+
const BALANCE_STALE_MAX_MS = Number(process.env.OPENZOO_BALANCE_STALE_MAX_MS || 60_000);
|
|
66
82
|
const balanceCache = new Map(); // key -> { raw, ui, at, refreshing }
|
|
67
83
|
|
|
68
84
|
const balKey = (owner, mint) => `${owner}|${mint}`;
|
|
69
85
|
|
|
86
|
+
/** Drop a cached figure so the next read is live. Called when the gateway
|
|
87
|
+
* refuses a payment we already signed — the cache is what mispriced it. */
|
|
88
|
+
export function forgetCachedBalance(owner, mint) {
|
|
89
|
+
balanceCache.delete(balKey(owner?.toBase58 ? owner.toBase58() : String(owner), mint));
|
|
90
|
+
}
|
|
91
|
+
|
|
70
92
|
async function cachedTokenBalance(connection, owner, mint, { force = false } = {}) {
|
|
71
93
|
const key = balKey(owner.toBase58 ? owner.toBase58() : String(owner), mint);
|
|
72
94
|
const hit = balanceCache.get(key);
|
|
73
95
|
const zero = !!(hit && hit.raw === 0n);
|
|
74
|
-
const
|
|
75
|
-
|
|
96
|
+
const age = hit ? Date.now() - hit.at : Infinity;
|
|
97
|
+
const fresh = hit && !force && !zero && age < BALANCE_TTL_MS;
|
|
98
|
+
if (hit && !force && !zero && age < BALANCE_STALE_MAX_MS) {
|
|
76
99
|
if (!fresh && !hit.refreshing) {
|
|
77
100
|
hit.refreshing = true;
|
|
78
101
|
tokenBalance(connection, owner, mint)
|
|
@@ -402,6 +425,11 @@ export class PayClient {
|
|
|
402
425
|
// into a silent spend loop.
|
|
403
426
|
if (response.status === 402 && !this._rebuilt) {
|
|
404
427
|
this._rebuilt = true;
|
|
428
|
+
// The gateway just refused a payment we signed. If the reason was funds
|
|
429
|
+
// rather than a dead blockhash, the cached balance is exactly what got it
|
|
430
|
+
// wrong — drop it so the rebuild re-reads live and the candidate walk can
|
|
431
|
+
// fall through to a rail the wallet can actually pay.
|
|
432
|
+
try { forgetCachedBalance(this.keypair?.publicKey, accept?.asset); } catch { /* advisory */ }
|
|
405
433
|
try {
|
|
406
434
|
// TAKE THE GATEWAY'S ADVICE BEFORE RE-QUOTING BLIND. On "payer balance
|
|
407
435
|
// insufficient" the 402 now carries advice.retryWithMaxTokens — the
|
package/lib/podagent.mjs
CHANGED
|
@@ -196,7 +196,9 @@ function execFrame(command, cwd = '/tmp') {
|
|
|
196
196
|
approvalId: randomUUID(),
|
|
197
197
|
// agent.v1.ExecServerMessage as protobuf-es JSON (camelCase). The daemon
|
|
198
198
|
// assigns `id`; we only supply the shell variant.
|
|
199
|
-
|
|
199
|
+
// `timeout` is MILLISECONDS (the daemon reads it via resolveShellTimeoutMs).
|
|
200
|
+
// This said 120, which aborted every command after 120ms — exit 0, no output.
|
|
201
|
+
serverMessage: { shellArgs: { command: guardFindCwd(command, cwd), workingDirectory: cwd, timeout: 120_000 } },
|
|
200
202
|
};
|
|
201
203
|
}
|
|
202
204
|
|
package/lib/proxy.js
CHANGED
|
@@ -40,9 +40,10 @@ import { attachX402Proof } from './spendProof.js';
|
|
|
40
40
|
|
|
41
41
|
/** Kill whatever is LISTEN on this port except this process.
|
|
42
42
|
* lsof first (macOS), then fuser, then /proc (Omarchy/Arch with neither).
|
|
43
|
+
* Windows: netstat -ano + taskkill — lsof/kill do not exist (measured 2026-09-03).
|
|
43
44
|
* Every openzoo subcommand that binds :8402 goes through this. Hopping to
|
|
44
45
|
* 8403 was the second-burner bug. */
|
|
45
|
-
export function killListen(port, run = execSync) {
|
|
46
|
+
export function killListen(port, run = execSync, platform = process.platform) {
|
|
46
47
|
const n = Number(port);
|
|
47
48
|
const seen = new Set();
|
|
48
49
|
const addFrom = (out) => {
|
|
@@ -56,9 +57,23 @@ export function killListen(port, run = execSync) {
|
|
|
56
57
|
return run(cmd, { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], ...opts });
|
|
57
58
|
} catch { return ''; }
|
|
58
59
|
};
|
|
60
|
+
if (platform === 'win32') {
|
|
61
|
+
const ns = tryRun('netstat -ano -p tcp');
|
|
62
|
+
for (const line of String(ns).split(/\r?\n/)) {
|
|
63
|
+
if (!/LISTENING/i.test(line)) continue;
|
|
64
|
+
const m = line.match(new RegExp(`[:\\]]${n}\\s+\\S+\\s+LISTENING\\s+(\\d+)\\s*$`, 'i'));
|
|
65
|
+
if (!m) continue;
|
|
66
|
+
const pid = Number(m[1]);
|
|
67
|
+
if (Number.isInteger(pid) && pid > 0 && pid !== process.pid) seen.add(pid);
|
|
68
|
+
}
|
|
69
|
+
for (const pid of seen) {
|
|
70
|
+
try { run(`taskkill /PID ${pid} /F`, { stdio: 'ignore', timeout: 2000 }); } catch { /* already gone */ }
|
|
71
|
+
}
|
|
72
|
+
return [...seen];
|
|
73
|
+
}
|
|
59
74
|
addFrom(tryRun(`lsof -nP -iTCP:${n} -sTCP:LISTEN -t`));
|
|
60
75
|
addFrom(tryRun(`fuser -n tcp ${n}`));
|
|
61
|
-
if (
|
|
76
|
+
if (platform === 'linux' && seen.size === 0) {
|
|
62
77
|
addFrom(tryRun(`python3 - ${n}`, {
|
|
63
78
|
timeout: 2500,
|
|
64
79
|
input: `import os, glob, sys
|
package/lib/setup.js
CHANGED
|
@@ -478,7 +478,7 @@ export async function setupEditor(which, target) {
|
|
|
478
478
|
const backModels = models.map((m) => ({ name: m, label: m }));
|
|
479
479
|
// Cert is minted as the USER (root can still read it); models handed to the
|
|
480
480
|
// privileged listener via a temp file.
|
|
481
|
-
ensureCert(console.log);
|
|
481
|
+
await ensureCert(console.log);
|
|
482
482
|
const modelsFile = path.join(os.tmpdir(), 'openzoo-cursor-models.json');
|
|
483
483
|
fs.writeFileSync(modelsFile, JSON.stringify(backModels));
|
|
484
484
|
const backendLog = path.join(os.homedir(), '.openzoo', 'cursor-backend.log');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.99",
|
|
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",
|