openzoo 0.50.38 → 0.50.39
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/cursorbackend.js +1 -1
- package/lib/grokbotweb-shim.js +1 -192
- package/lib/grokbotweb.js +3 -2
- package/lib/grokcli.js +43 -22
- package/lib/ozSpendChip.js +482 -0
- package/lib/pay.js +14 -24
- package/lib/proxy.js +24 -10
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -1443,7 +1443,7 @@ function promptFromSendBody(raw) {
|
|
|
1443
1443
|
|
|
1444
1444
|
let walletUsdCache = { usd: null, at: 0 };
|
|
1445
1445
|
async function walletUsdCached() {
|
|
1446
|
-
if (walletUsdCache.usd != null && Date.now() - walletUsdCache.at <
|
|
1446
|
+
if (walletUsdCache.usd != null && Date.now() - walletUsdCache.at < 8_000) return walletUsdCache.usd;
|
|
1447
1447
|
try {
|
|
1448
1448
|
const { affordableUsd } = await import('./info.js');
|
|
1449
1449
|
const n = await Promise.race([
|
package/lib/grokbotweb-shim.js
CHANGED
|
@@ -492,12 +492,6 @@
|
|
|
492
492
|
'background:color-mix(in srgb,var(--oz-who,#3db8e8) 20%,transparent)}',
|
|
493
493
|
'.oz-who-chip::after{content:"";position:absolute;top:-4px;right:-4px;width:9px;height:9px;border-radius:50%;',
|
|
494
494
|
'background:var(--oz-who,#3db8e8);box-shadow:0 0 0 2px #141414;pointer-events:none}',
|
|
495
|
-
'.oz-spend{margin:.55rem 0 0;font-size:12px;color:inherit;opacity:.82;max-width:36em}',
|
|
496
|
-
'.oz-spend>summary{cursor:help;list-style:none;display:inline-flex;align-items:center;gap:.35rem;',
|
|
497
|
-
'padding:3px 9px;border-radius:999px;border:1px solid rgba(255,255,255,.18)}',
|
|
498
|
-
'.oz-spend>summary::-webkit-details-marker{display:none}',
|
|
499
|
-
'.oz-spend-body{white-space:pre-wrap;margin:.55rem 0 0;font-size:11px;line-height:1.45;opacity:.88;overflow-wrap:anywhere}',
|
|
500
|
-
'[data-oz-spend-hide]{display:none!important}',
|
|
501
495
|
].join('');
|
|
502
496
|
(document.head || document.documentElement).appendChild(s);
|
|
503
497
|
}
|
|
@@ -575,192 +569,7 @@
|
|
|
575
569
|
setInterval(run, 2500);
|
|
576
570
|
}
|
|
577
571
|
ozWatchChips();
|
|
578
|
-
|
|
579
|
-
function ozSplitSpend(text) {
|
|
580
|
-
const s = String(text || '');
|
|
581
|
-
let m = s.match(/::oz-spend::(\$[0-9.]+|[^\n]*)\s*([\s\S]*)$/);
|
|
582
|
-
if (m && (m[2] || '').length > 8) {
|
|
583
|
-
let summary = (m[1] || '').trim();
|
|
584
|
-
const body = m[2].trim();
|
|
585
|
-
if (!/^\$[0-9.]+$/.test(summary) || summary === '$0.0000') {
|
|
586
|
-
const spent = body.match(/spent \$([0-9.]+)/i);
|
|
587
|
-
const call = body.match(/this call \$([0-9.]+)/i);
|
|
588
|
-
const n = Number((call && Number(call[1]) > 0.00005 ? call[1] : null) || (spent && spent[1]) || 0);
|
|
589
|
-
if (n > 0) summary = `$${n.toFixed(4)}`;
|
|
590
|
-
}
|
|
591
|
-
return { head: s.slice(0, m.index), summary: summary || 'spend', body };
|
|
592
|
-
}
|
|
593
|
-
m = s.match(/(?:this call \$|spent \$)[\s\S]*$/i);
|
|
594
|
-
if (m) {
|
|
595
|
-
const body = m[0].trim();
|
|
596
|
-
const call = body.match(/this call \$([0-9.]+)/i);
|
|
597
|
-
const spent = body.match(/spent \$([0-9.]+)/i);
|
|
598
|
-
const n = Number((call && Number(call[1]) > 0.00005 ? call[1] : null) || (spent && spent[1]) || 0);
|
|
599
|
-
return { head: s.slice(0, m.index), summary: n > 0 ? `$${n.toFixed(4)}` : 'spend', body };
|
|
600
|
-
}
|
|
601
|
-
return null;
|
|
602
|
-
}
|
|
603
|
-
function ozSpendHost(el) {
|
|
604
|
-
let cur = el;
|
|
605
|
-
let found = el;
|
|
606
|
-
let card = null;
|
|
607
|
-
for (let i = 0; i < 16 && cur && cur !== document.body && cur.id !== 'root'; i += 1) {
|
|
608
|
-
const t = cur.textContent || '';
|
|
609
|
-
if (/::oz-spend::|this call \$|spent \$/i.test(t)) found = cur;
|
|
610
|
-
const cls = cur.className && String(cur.className);
|
|
611
|
-
if (cls && /sand-message-card|sand-message-block/.test(cls)) {
|
|
612
|
-
card = cur;
|
|
613
|
-
return cur;
|
|
614
|
-
}
|
|
615
|
-
if (cls && /(^|\s)sand-message(\s|$)/.test(cls)) card = cur;
|
|
616
|
-
cur = cur.parentElement;
|
|
617
|
-
}
|
|
618
|
-
if (card) return card;
|
|
619
|
-
if (found && found !== document.body && found.id !== 'root' && (found.textContent || '').length < 12000) {
|
|
620
|
-
return found;
|
|
621
|
-
}
|
|
622
|
-
return null;
|
|
623
|
-
}
|
|
624
|
-
function ozVisibleSpendText(root) {
|
|
625
|
-
let s = '';
|
|
626
|
-
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
|
|
627
|
-
while (w.nextNode()) {
|
|
628
|
-
const tn = w.currentNode;
|
|
629
|
-
if (tn.parentElement && tn.parentElement.closest && tn.parentElement.closest('.oz-spend')) continue;
|
|
630
|
-
s += tn.nodeValue || '';
|
|
631
|
-
}
|
|
632
|
-
return s;
|
|
633
|
-
}
|
|
634
|
-
function ozSpendOnlyText(t) {
|
|
635
|
-
const s = String(t || '').replace(/\u00a0/g, ' ').trim();
|
|
636
|
-
if (!s) return false;
|
|
637
|
-
if (!/::oz-spend::|this call \$|spent \$[0-9.]+[\s\S]*OpenRouter/i.test(s)) return false;
|
|
638
|
-
const rest = s
|
|
639
|
-
.replace(/::oz-spend::\$?[0-9.]*/gi, ' ')
|
|
640
|
-
.replace(/this call \$[0-9.]+/gi, ' ')
|
|
641
|
-
.replace(/OpenRouter(?: would)? \$[0-9.]+/gi, ' ')
|
|
642
|
-
.replace(/spent \$[0-9.]+/gi, ' ')
|
|
643
|
-
.replace(/saved \$[0-9.]+/gi, ' ')
|
|
644
|
-
.replace(/balance \$[0-9.]+/gi, ' ')
|
|
645
|
-
.replace(/\(\s*\d+%\s*\)/g, ' ')
|
|
646
|
-
.replace(/\b(?:tx|memo|proves)\b[^\n]*/gi, ' ')
|
|
647
|
-
.replace(/https?:\/\/\S+/gi, ' ')
|
|
648
|
-
.replace(/[·•.,;:/$%\d\s()[\]+\-]/g, '');
|
|
649
|
-
return rest.length < 4;
|
|
650
|
-
}
|
|
651
|
-
function ozBlankAfter(root, keepLen) {
|
|
652
|
-
let off = 0;
|
|
653
|
-
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
|
|
654
|
-
const tns = [];
|
|
655
|
-
while (w.nextNode()) tns.push(w.currentNode);
|
|
656
|
-
for (let i = 0; i < tns.length; i += 1) {
|
|
657
|
-
const tn = tns[i];
|
|
658
|
-
if (tn.parentElement && tn.parentElement.closest && tn.parentElement.closest('.oz-spend')) continue;
|
|
659
|
-
const s = tn.nodeValue || '';
|
|
660
|
-
const start = off;
|
|
661
|
-
off += s.length;
|
|
662
|
-
if (off <= keepLen) continue;
|
|
663
|
-
if (start >= keepLen) tn.nodeValue = '';
|
|
664
|
-
else tn.nodeValue = s.slice(0, keepLen - start);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
function ozHideSpendLeftovers() {
|
|
668
|
-
const nodes = document.body.querySelectorAll('div, p, span, li');
|
|
669
|
-
for (let i = 0; i < nodes.length; i += 1) {
|
|
670
|
-
const el = nodes[i];
|
|
671
|
-
if (el.closest && el.closest('.oz-spend')) continue;
|
|
672
|
-
if (el.querySelector && el.querySelector('.oz-spend')) continue;
|
|
673
|
-
const vis = ozVisibleSpendText(el);
|
|
674
|
-
if (!ozSpendOnlyText(vis)) continue;
|
|
675
|
-
if (vis.length > 4000) continue;
|
|
676
|
-
if (el.children && el.children.length > 8) continue;
|
|
677
|
-
el.setAttribute('data-oz-spend-hide', '1');
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
function ozPreviousMessageCard(el) {
|
|
681
|
-
let cur = el;
|
|
682
|
-
for (let i = 0; i < 8 && cur && cur !== document.body; i += 1) {
|
|
683
|
-
let sib = cur.previousElementSibling;
|
|
684
|
-
while (sib) {
|
|
685
|
-
if (sib === el) {
|
|
686
|
-
sib = sib.previousElementSibling;
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
689
|
-
const cls = String(sib.className || '');
|
|
690
|
-
if (/sand-message-card|sand-message-block/.test(cls) || /(^|\s)sand-message(\s|$)/.test(cls)) {
|
|
691
|
-
return sib;
|
|
692
|
-
}
|
|
693
|
-
const inner = sib.querySelector && sib.querySelector('.sand-message-card, .sand-message-block');
|
|
694
|
-
if (inner) return inner;
|
|
695
|
-
sib = sib.previousElementSibling;
|
|
696
|
-
}
|
|
697
|
-
cur = cur.parentElement;
|
|
698
|
-
}
|
|
699
|
-
return null;
|
|
700
|
-
}
|
|
701
|
-
function ozAttachSpendChip(host, split) {
|
|
702
|
-
if (!host || !split) return;
|
|
703
|
-
if (host.querySelector && host.querySelector('.oz-spend')) return;
|
|
704
|
-
const d = document.createElement('details');
|
|
705
|
-
d.className = 'oz-spend';
|
|
706
|
-
const sum = document.createElement('summary');
|
|
707
|
-
sum.textContent = 'ⓘ ' + split.summary;
|
|
708
|
-
sum.title = split.body;
|
|
709
|
-
const body = document.createElement('div');
|
|
710
|
-
body.className = 'oz-spend-body';
|
|
711
|
-
body.textContent = split.body;
|
|
712
|
-
d.appendChild(sum);
|
|
713
|
-
d.appendChild(body);
|
|
714
|
-
try { host.appendChild(d); } catch { /* react owns some nodes */ }
|
|
715
|
-
}
|
|
716
|
-
function ozCollapseSpend() {
|
|
717
|
-
if (!document.body) return;
|
|
718
|
-
const hosts = [];
|
|
719
|
-
const seen = new Set();
|
|
720
|
-
const nodes = document.body.querySelectorAll('div, p, span, li');
|
|
721
|
-
for (let i = 0; i < nodes.length; i += 1) {
|
|
722
|
-
const el = nodes[i];
|
|
723
|
-
if (el.closest && el.closest('.oz-spend')) continue;
|
|
724
|
-
const t = ozVisibleSpendText(el);
|
|
725
|
-
if (!/::oz-spend::|this call \$|spent \$/i.test(t)) continue;
|
|
726
|
-
const host = ozSpendHost(el);
|
|
727
|
-
if (!host || seen.has(host)) continue;
|
|
728
|
-
seen.add(host);
|
|
729
|
-
hosts.push(host);
|
|
730
|
-
}
|
|
731
|
-
for (let h = 0; h < hosts.length; h += 1) {
|
|
732
|
-
let host = hosts[h];
|
|
733
|
-
const vis = ozVisibleSpendText(host);
|
|
734
|
-
const split = ozSplitSpend(vis);
|
|
735
|
-
if (!split || !split.body || split.body.length < 12) continue;
|
|
736
|
-
ozEnsureChipCss();
|
|
737
|
-
if (ozSpendOnlyText(vis)) {
|
|
738
|
-
const prev = ozPreviousMessageCard(host);
|
|
739
|
-
if (prev && prev !== host) {
|
|
740
|
-
ozAttachSpendChip(prev, split);
|
|
741
|
-
host.setAttribute('data-oz-spend-hide', '1');
|
|
742
|
-
ozBlankAfter(host, 0);
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
ozBlankAfter(host, split.head.length);
|
|
747
|
-
ozAttachSpendChip(host, split);
|
|
748
|
-
}
|
|
749
|
-
ozHideSpendLeftovers();
|
|
750
|
-
}
|
|
751
|
-
function ozWatchSpend() {
|
|
752
|
-
const run = () => { try { ozCollapseSpend(); } catch { /* */ } };
|
|
753
|
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run);
|
|
754
|
-
else run();
|
|
755
|
-
try {
|
|
756
|
-
const mo = new MutationObserver(run);
|
|
757
|
-
const start = () => { if (document.body) mo.observe(document.body, { childList: true, subtree: true }); };
|
|
758
|
-
if (document.body) start();
|
|
759
|
-
else document.addEventListener('DOMContentLoaded', start);
|
|
760
|
-
} catch { /* */ }
|
|
761
|
-
setInterval(run, 1500);
|
|
762
|
-
}
|
|
763
|
-
ozWatchSpend();
|
|
572
|
+
/* spend chip IIFE is concatenated from lib/ozSpendChip.js */
|
|
764
573
|
|
|
765
574
|
function ozIsPhone() {
|
|
766
575
|
try {
|
package/lib/grokbotweb.js
CHANGED
|
@@ -18,6 +18,7 @@ import { exec } from 'node:child_process';
|
|
|
18
18
|
import { config } from './config.js';
|
|
19
19
|
import { readHouseRoster } from './grokbotAccount.js';
|
|
20
20
|
import { ingestUpload, lookupUpload } from './grokbotUploads.js';
|
|
21
|
+
import { spendChipSource } from './ozSpendChip.js';
|
|
21
22
|
|
|
22
23
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const SHIM_PATH = path.join(HERE, 'grokbotweb-shim.js');
|
|
@@ -738,7 +739,7 @@ export async function startGrokBotWeb(opts = {}) {
|
|
|
738
739
|
throw new Error('Grok Bot.app not found (looked in ./Grok Bot.app and /Applications).');
|
|
739
740
|
}
|
|
740
741
|
const archive = loadAsar(asarPath);
|
|
741
|
-
const shim = fs.readFileSync(SHIM_PATH);
|
|
742
|
+
const shim = fs.readFileSync(SHIM_PATH, 'utf8') + '\n' + spendChipSource();
|
|
742
743
|
|
|
743
744
|
if (!opts.skipBackend) {
|
|
744
745
|
await ensureProxy(log);
|
|
@@ -843,7 +844,7 @@ export async function startGrokBotWeb(opts = {}) {
|
|
|
843
844
|
const prelude = `window.__OZ_WHO_PALETTE__=${JSON.stringify(visitorPaletteMap())};\n`
|
|
844
845
|
+ `window.__OZ_WHO__=${JSON.stringify(visitor)};\n`;
|
|
845
846
|
res.writeHead(200, headers);
|
|
846
|
-
res.end(prelude + shim
|
|
847
|
+
res.end(prelude + shim);
|
|
847
848
|
return;
|
|
848
849
|
}
|
|
849
850
|
let rel = urlPath === '/' ? 'dist/renderer/index.html' : path.posix.normalize(urlPath).replace(/^\/+/, '');
|
package/lib/grokcli.js
CHANGED
|
@@ -22,6 +22,11 @@ import { dirname, join } from 'node:path';
|
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
|
|
24
24
|
import { config } from './config.js';
|
|
25
|
+
import {
|
|
26
|
+
GROKBOT_CDP_PORT,
|
|
27
|
+
grokBotChromiumArgs,
|
|
28
|
+
injectSpendChipInBackground,
|
|
29
|
+
} from './ozSpendChip.js';
|
|
25
30
|
|
|
26
31
|
const GROK_HOME = process.env.GROK_HOME || join(homedir(), '.grok');
|
|
27
32
|
const CONFIG_PATH = join(GROK_HOME, 'config.toml');
|
|
@@ -134,12 +139,14 @@ function somethingTalksToPort(port, run = execSync) {
|
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
/** Is the running Grok Bot already pointed at our aiserver? */
|
|
137
|
-
export function inspectGrokBotHijack(url, port = 8443, run = execSync) {
|
|
142
|
+
export function inspectGrokBotHijack(url, port = 8443, run = execSync, cdpPort = GROKBOT_CDP_PORT) {
|
|
138
143
|
const pids = grokBotPids(run);
|
|
139
144
|
const needle = `CURSOR_API_BASE_URL=${url}`;
|
|
140
145
|
const envHit = pids.some((pid) => pidHasNeedle(pid, needle, run));
|
|
141
146
|
const tcpHit = somethingTalksToPort(port, run);
|
|
142
|
-
|
|
147
|
+
const cdpNeedle = `--remote-debugging-port=${Number(cdpPort)}`;
|
|
148
|
+
const chipDebug = pids.some((pid) => pidHasNeedle(pid, cdpNeedle, run));
|
|
149
|
+
return { running: pids.length > 0, pids, hijacked: envHit || tcpHit, chipDebug };
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
/**
|
|
@@ -147,7 +154,13 @@ export function inspectGrokBotHijack(url, port = 8443, run = execSync) {
|
|
|
147
154
|
* forceQuit = --quit / OZ_NO_QUIT=0
|
|
148
155
|
* neverQuit = --no-quit / OZ_NO_QUIT=1
|
|
149
156
|
*/
|
|
150
|
-
export function grokBotQuitPlan({
|
|
157
|
+
export function grokBotQuitPlan({
|
|
158
|
+
forceQuit = false,
|
|
159
|
+
neverQuit = false,
|
|
160
|
+
running = false,
|
|
161
|
+
hijacked = false,
|
|
162
|
+
chipDebug,
|
|
163
|
+
} = {}) {
|
|
151
164
|
if (forceQuit) return { quit: true, spawn: true, reason: 'forced --quit' };
|
|
152
165
|
if (neverQuit) {
|
|
153
166
|
return {
|
|
@@ -159,6 +172,13 @@ export function grokBotQuitPlan({ forceQuit = false, neverQuit = false, running
|
|
|
159
172
|
};
|
|
160
173
|
}
|
|
161
174
|
if (!running) return { quit: false, spawn: true, reason: 'Grok Bot not running' };
|
|
175
|
+
if (hijacked && chipDebug === false) {
|
|
176
|
+
return {
|
|
177
|
+
quit: true,
|
|
178
|
+
spawn: true,
|
|
179
|
+
reason: 'hijacked but no spend-chip debug port — bouncing so the ⓘ bubble can inject',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
162
182
|
if (hijacked) return { quit: false, spawn: false, reason: 'already hijacked — leaving the session' };
|
|
163
183
|
return {
|
|
164
184
|
quit: true,
|
|
@@ -194,13 +214,9 @@ export async function runBot(argv = []) {
|
|
|
194
214
|
}
|
|
195
215
|
|
|
196
216
|
const proxyBase = `http://localhost:${config.port}/v1`;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
console.error('openzoo: starting proxy on', proxyBase);
|
|
201
|
-
const { startProxy } = await import('./proxy.js');
|
|
202
|
-
await startProxy({ silent: true, autoTunnel: process.env.OPENZOO_NO_TUNNEL === '1' ? false : true });
|
|
203
|
-
}
|
|
217
|
+
console.error('openzoo: starting proxy on', proxyBase);
|
|
218
|
+
const { startProxy } = await import('./proxy.js');
|
|
219
|
+
await startProxy({ silent: true, autoTunnel: process.env.OPENZOO_NO_TUNNEL === '1' ? false : true });
|
|
204
220
|
|
|
205
221
|
process.env.OPENZOO_BYOK = '1';
|
|
206
222
|
const sniff = argv.includes('--sniff') || argv.includes('--passthru');
|
|
@@ -268,6 +284,7 @@ export async function runBot(argv = []) {
|
|
|
268
284
|
neverQuit: argv.includes('--no-quit') || process.env.OZ_NO_QUIT === '1',
|
|
269
285
|
running: state.running,
|
|
270
286
|
hijacked: state.hijacked,
|
|
287
|
+
chipDebug: state.chipDebug,
|
|
271
288
|
});
|
|
272
289
|
console.error(`openzoo: grokbot ${plan.reason}`);
|
|
273
290
|
if (plan.quit) {
|
|
@@ -278,7 +295,7 @@ export async function runBot(argv = []) {
|
|
|
278
295
|
if (plan.spawn) {
|
|
279
296
|
console.error('openzoo: launching Grok Bot');
|
|
280
297
|
console.error(` CURSOR_API_BASE_URL=${url}`);
|
|
281
|
-
spawn(bin,
|
|
298
|
+
spawn(bin, grokBotChromiumArgs(), {
|
|
282
299
|
stdio: 'ignore',
|
|
283
300
|
detached: true,
|
|
284
301
|
env: {
|
|
@@ -298,6 +315,12 @@ export async function runBot(argv = []) {
|
|
|
298
315
|
console.error('openzoo: Grok Bot already hijacked — not spawning another copy');
|
|
299
316
|
}
|
|
300
317
|
|
|
318
|
+
injectSpendChipInBackground({
|
|
319
|
+
port: GROKBOT_CDP_PORT,
|
|
320
|
+
log: (m) => console.error(m),
|
|
321
|
+
delayMs: plan.spawn ? 500 : 200,
|
|
322
|
+
});
|
|
323
|
+
|
|
301
324
|
console.error('openzoo: leave this running. ctrl-c stops the backend.');
|
|
302
325
|
if (argv.includes('--once')) return;
|
|
303
326
|
await new Promise(() => {});
|
|
@@ -307,19 +330,17 @@ export async function setupGrokBot(argv = []) {
|
|
|
307
330
|
const base = `http://localhost:${config.port}/v1`;
|
|
308
331
|
const launch = !argv.includes('--no-launch');
|
|
309
332
|
|
|
310
|
-
// 1. proxy up, or nothing can pay
|
|
333
|
+
// 1. proxy up, or nothing can pay. Always startProxy: it kills a stale
|
|
334
|
+
// listener on the port instead of reusing a PayClient that still thinks $0.
|
|
335
|
+
console.error('openzoo: starting the proxy...');
|
|
336
|
+
const { startProxy } = await import('./proxy.js');
|
|
337
|
+
await startProxy({ silent: true, autoTunnel: true });
|
|
311
338
|
let up = false;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const { startProxy } = await import('./proxy.js');
|
|
316
|
-
await startProxy({ silent: true, autoTunnel: true });
|
|
317
|
-
for (let i = 0; i < 25 && !up; i++) {
|
|
318
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
319
|
-
try { up = (await fetch(`${base}/models`, { signal: AbortSignal.timeout(2000) })).ok; } catch { /* wait */ }
|
|
320
|
-
}
|
|
321
|
-
if (!up) { console.error(`openzoo: proxy did not come up on ${base}`); process.exit(1); }
|
|
339
|
+
for (let i = 0; i < 25 && !up; i++) {
|
|
340
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
341
|
+
try { up = (await fetch(`${base}/models`, { signal: AbortSignal.timeout(2000) })).ok; } catch { /* wait */ }
|
|
322
342
|
}
|
|
343
|
+
if (!up) { console.error(`openzoo: proxy did not come up on ${base}`); process.exit(1); }
|
|
323
344
|
|
|
324
345
|
const models = await grokModels(base);
|
|
325
346
|
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cafe spend chip — the ⓘ $N <details> that folds formatSpendFooter.
|
|
3
|
+
*
|
|
4
|
+
* Cafe injects this via grokbotweb-shim concatenation. Grok Bot.app cannot
|
|
5
|
+
* be patched (asar integrity), so `openzoo bot` launches Chromium with a
|
|
6
|
+
* localhost CDP port and evaluates the same IIFE in the renderer.
|
|
7
|
+
*/
|
|
8
|
+
import net from 'node:net';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
export const GROKBOT_CDP_PORT = Number(process.env.OZ_GROKBOT_CDP_PORT || 9444);
|
|
12
|
+
|
|
13
|
+
export function grokBotChromiumArgs(port = GROKBOT_CDP_PORT) {
|
|
14
|
+
return [
|
|
15
|
+
'--ignore-certificate-errors',
|
|
16
|
+
`--remote-debugging-port=${Number(port)}`,
|
|
17
|
+
'--remote-allow-origins=*',
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Pure. Same split the renderer IIFE uses. */
|
|
22
|
+
export function splitSpendText(text) {
|
|
23
|
+
const s = String(text || '');
|
|
24
|
+
let m = s.match(/::oz-spend::(\$[0-9.]+|[^\n]*)\s*([\s\S]*)$/);
|
|
25
|
+
if (m && (m[2] || '').length > 8) {
|
|
26
|
+
let summary = (m[1] || '').trim();
|
|
27
|
+
const body = m[2].trim();
|
|
28
|
+
if (!/^\$[0-9.]+$/.test(summary) || summary === '$0.0000') {
|
|
29
|
+
const spent = body.match(/spent \$([0-9.]+)/i);
|
|
30
|
+
const call = body.match(/this call \$([0-9.]+)/i);
|
|
31
|
+
const n = Number((call && Number(call[1]) > 0.00005 ? call[1] : null) || (spent && spent[1]) || 0);
|
|
32
|
+
if (n > 0) summary = `$${n.toFixed(4)}`;
|
|
33
|
+
}
|
|
34
|
+
return { head: s.slice(0, m.index), summary: summary || 'spend', body };
|
|
35
|
+
}
|
|
36
|
+
m = s.match(/(?:this call \$|spent \$)[\s\S]*$/i);
|
|
37
|
+
if (m) {
|
|
38
|
+
const body = m[0].trim();
|
|
39
|
+
const call = body.match(/this call \$([0-9.]+)/i);
|
|
40
|
+
const spent = body.match(/spent \$([0-9.]+)/i);
|
|
41
|
+
const n = Number((call && Number(call[1]) > 0.00005 ? call[1] : null) || (spent && spent[1]) || 0);
|
|
42
|
+
return { head: s.slice(0, m.index), summary: n > 0 ? `$${n.toFixed(4)}` : 'spend', body };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function spendOnlyText(t) {
|
|
48
|
+
const s = String(t || '').replace(/\u00a0/g, ' ').trim();
|
|
49
|
+
if (!s) return false;
|
|
50
|
+
if (!/::oz-spend::|this call \$|spent \$[0-9.]+[\s\S]*OpenRouter/i.test(s)) return false;
|
|
51
|
+
const rest = s
|
|
52
|
+
.replace(/::oz-spend::\$?[0-9.]*/gi, ' ')
|
|
53
|
+
.replace(/this call \$[0-9.]+/gi, ' ')
|
|
54
|
+
.replace(/OpenRouter(?: would)? \$[0-9.]+/gi, ' ')
|
|
55
|
+
.replace(/spent \$[0-9.]+/gi, ' ')
|
|
56
|
+
.replace(/saved \$[0-9.]+/gi, ' ')
|
|
57
|
+
.replace(/balance \$[0-9.]+/gi, ' ')
|
|
58
|
+
.replace(/\(\s*\d+%\s*\)/g, ' ')
|
|
59
|
+
.replace(/\b(?:tx|memo|proves)\b[^\n]*/gi, ' ')
|
|
60
|
+
.replace(/https?:\/\/\S+/gi, ' ')
|
|
61
|
+
.replace(/[·•.,;:/$%\d\s()[\]+\-]/g, '');
|
|
62
|
+
return rest.length < 4;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ozEnsureSpendCss() {
|
|
66
|
+
if (document.getElementById('oz-spend-css')) return;
|
|
67
|
+
const s = document.createElement('style');
|
|
68
|
+
s.id = 'oz-spend-css';
|
|
69
|
+
s.textContent = [
|
|
70
|
+
'.oz-spend{margin:.55rem 0 0;font-size:12px;color:inherit;opacity:.82;max-width:36em}',
|
|
71
|
+
'.oz-spend>summary{cursor:help;list-style:none;display:inline-flex;align-items:center;gap:.35rem;',
|
|
72
|
+
'padding:3px 9px;border-radius:999px;border:1px solid rgba(255,255,255,.18)}',
|
|
73
|
+
'.oz-spend>summary::-webkit-details-marker{display:none}',
|
|
74
|
+
'.oz-spend-body{white-space:pre-wrap;margin:.55rem 0 0;font-size:11px;line-height:1.45;opacity:.88;overflow-wrap:anywhere}',
|
|
75
|
+
'[data-oz-spend-hide]{display:none!important}',
|
|
76
|
+
].join('');
|
|
77
|
+
(document.head || document.documentElement).appendChild(s);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ozSpendHost(el) {
|
|
81
|
+
let cur = el;
|
|
82
|
+
let found = el;
|
|
83
|
+
let card = null;
|
|
84
|
+
for (let i = 0; i < 16 && cur && cur !== document.body && cur.id !== 'root'; i += 1) {
|
|
85
|
+
const t = cur.textContent || '';
|
|
86
|
+
if (/::oz-spend::|this call \$|spent \$/i.test(t)) found = cur;
|
|
87
|
+
const cls = cur.className && String(cur.className);
|
|
88
|
+
if (cls && /sand-message-card|sand-message-block/.test(cls)) {
|
|
89
|
+
card = cur;
|
|
90
|
+
return cur;
|
|
91
|
+
}
|
|
92
|
+
if (cls && /(^|\s)sand-message(\s|$)/.test(cls)) card = cur;
|
|
93
|
+
cur = cur.parentElement;
|
|
94
|
+
}
|
|
95
|
+
if (card) return card;
|
|
96
|
+
if (found && found !== document.body && found.id !== 'root' && (found.textContent || '').length < 12000) {
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ozVisibleSpendText(root) {
|
|
103
|
+
let s = '';
|
|
104
|
+
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
|
|
105
|
+
while (w.nextNode()) {
|
|
106
|
+
const tn = w.currentNode;
|
|
107
|
+
if (tn.parentElement && tn.parentElement.closest && tn.parentElement.closest('.oz-spend')) continue;
|
|
108
|
+
s += tn.nodeValue || '';
|
|
109
|
+
}
|
|
110
|
+
return s;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function ozBlankAfter(root, keepLen) {
|
|
114
|
+
let off = 0;
|
|
115
|
+
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
|
|
116
|
+
const tns = [];
|
|
117
|
+
while (w.nextNode()) tns.push(w.currentNode);
|
|
118
|
+
for (let i = 0; i < tns.length; i += 1) {
|
|
119
|
+
const tn = tns[i];
|
|
120
|
+
if (tn.parentElement && tn.parentElement.closest && tn.parentElement.closest('.oz-spend')) continue;
|
|
121
|
+
const s = tn.nodeValue || '';
|
|
122
|
+
const start = off;
|
|
123
|
+
off += s.length;
|
|
124
|
+
if (off <= keepLen) continue;
|
|
125
|
+
if (start >= keepLen) tn.nodeValue = '';
|
|
126
|
+
else tn.nodeValue = s.slice(0, keepLen - start);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ozHideSpendLeftovers() {
|
|
131
|
+
const nodes = document.body.querySelectorAll('div, p, span, li');
|
|
132
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
133
|
+
const el = nodes[i];
|
|
134
|
+
if (el.closest && el.closest('.oz-spend')) continue;
|
|
135
|
+
if (el.querySelector && el.querySelector('.oz-spend')) continue;
|
|
136
|
+
const vis = ozVisibleSpendText(el);
|
|
137
|
+
if (!spendOnlyText(vis)) continue;
|
|
138
|
+
if (vis.length > 4000) continue;
|
|
139
|
+
if (el.children && el.children.length > 8) continue;
|
|
140
|
+
el.setAttribute('data-oz-spend-hide', '1');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ozPreviousMessageCard(el) {
|
|
145
|
+
let cur = el;
|
|
146
|
+
for (let i = 0; i < 8 && cur && cur !== document.body; i += 1) {
|
|
147
|
+
let sib = cur.previousElementSibling;
|
|
148
|
+
while (sib) {
|
|
149
|
+
if (sib === el) {
|
|
150
|
+
sib = sib.previousElementSibling;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const cls = String(sib.className || '');
|
|
154
|
+
if (/sand-message-card|sand-message-block/.test(cls) || /(^|\s)sand-message(\s|$)/.test(cls)) {
|
|
155
|
+
return sib;
|
|
156
|
+
}
|
|
157
|
+
const inner = sib.querySelector && sib.querySelector('.sand-message-card, .sand-message-block');
|
|
158
|
+
if (inner) return inner;
|
|
159
|
+
sib = sib.previousElementSibling;
|
|
160
|
+
}
|
|
161
|
+
cur = cur.parentElement;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ozAttachSpendChip(host, split) {
|
|
167
|
+
if (!host || !split) return;
|
|
168
|
+
if (host.querySelector && host.querySelector('.oz-spend')) return;
|
|
169
|
+
const d = document.createElement('details');
|
|
170
|
+
d.className = 'oz-spend';
|
|
171
|
+
const sum = document.createElement('summary');
|
|
172
|
+
sum.textContent = 'ⓘ ' + split.summary;
|
|
173
|
+
sum.title = split.body;
|
|
174
|
+
const body = document.createElement('div');
|
|
175
|
+
body.className = 'oz-spend-body';
|
|
176
|
+
body.textContent = split.body;
|
|
177
|
+
d.appendChild(sum);
|
|
178
|
+
d.appendChild(body);
|
|
179
|
+
try { host.appendChild(d); } catch { /* react owns some nodes */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function ozCollapseSpend() {
|
|
183
|
+
if (!document.body) return;
|
|
184
|
+
const hosts = [];
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
const nodes = document.body.querySelectorAll('div, p, span, li');
|
|
187
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
188
|
+
const el = nodes[i];
|
|
189
|
+
if (el.closest && el.closest('.oz-spend')) continue;
|
|
190
|
+
const t = ozVisibleSpendText(el);
|
|
191
|
+
if (!/::oz-spend::|this call \$|spent \$/i.test(t)) continue;
|
|
192
|
+
const host = ozSpendHost(el);
|
|
193
|
+
if (!host || seen.has(host)) continue;
|
|
194
|
+
seen.add(host);
|
|
195
|
+
hosts.push(host);
|
|
196
|
+
}
|
|
197
|
+
for (let h = 0; h < hosts.length; h += 1) {
|
|
198
|
+
const host = hosts[h];
|
|
199
|
+
const vis = ozVisibleSpendText(host);
|
|
200
|
+
const split = splitSpendText(vis);
|
|
201
|
+
if (!split || !split.body || split.body.length < 12) continue;
|
|
202
|
+
ozEnsureSpendCss();
|
|
203
|
+
if (spendOnlyText(vis)) {
|
|
204
|
+
const prev = ozPreviousMessageCard(host);
|
|
205
|
+
if (prev && prev !== host) {
|
|
206
|
+
ozAttachSpendChip(prev, split);
|
|
207
|
+
host.setAttribute('data-oz-spend-hide', '1');
|
|
208
|
+
ozBlankAfter(host, 0);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
ozBlankAfter(host, split.head.length);
|
|
213
|
+
ozAttachSpendChip(host, split);
|
|
214
|
+
}
|
|
215
|
+
ozHideSpendLeftovers();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function ozWatchSpend() {
|
|
219
|
+
const run = () => { try { ozCollapseSpend(); } catch { /* */ } };
|
|
220
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run);
|
|
221
|
+
else run();
|
|
222
|
+
try {
|
|
223
|
+
const mo = new MutationObserver(run);
|
|
224
|
+
const start = () => { if (document.body) mo.observe(document.body, { childList: true, subtree: true }); };
|
|
225
|
+
if (document.body) start();
|
|
226
|
+
else document.addEventListener('DOMContentLoaded', start);
|
|
227
|
+
} catch { /* */ }
|
|
228
|
+
setInterval(run, 1500);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function spendChipSource() {
|
|
232
|
+
return [
|
|
233
|
+
'(function ozSpendChip(){',
|
|
234
|
+
"'use strict';",
|
|
235
|
+
'if (window.__OZ_SPEND_CHIP__) return;',
|
|
236
|
+
'window.__OZ_SPEND_CHIP__ = 1;',
|
|
237
|
+
splitSpendText.toString(),
|
|
238
|
+
spendOnlyText.toString(),
|
|
239
|
+
ozEnsureSpendCss.toString(),
|
|
240
|
+
ozSpendHost.toString(),
|
|
241
|
+
ozVisibleSpendText.toString(),
|
|
242
|
+
ozBlankAfter.toString(),
|
|
243
|
+
ozHideSpendLeftovers.toString(),
|
|
244
|
+
ozPreviousMessageCard.toString(),
|
|
245
|
+
ozAttachSpendChip.toString(),
|
|
246
|
+
ozCollapseSpend.toString(),
|
|
247
|
+
ozWatchSpend.toString(),
|
|
248
|
+
'ozWatchSpend();',
|
|
249
|
+
'})();',
|
|
250
|
+
].join('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function wsMaskFrame(payload, opcode = 0x1) {
|
|
254
|
+
const data = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload));
|
|
255
|
+
const mask = crypto.randomBytes(4);
|
|
256
|
+
const len = data.length;
|
|
257
|
+
let header;
|
|
258
|
+
if (len < 126) {
|
|
259
|
+
header = Buffer.alloc(6);
|
|
260
|
+
header[0] = 0x80 | opcode;
|
|
261
|
+
header[1] = 0x80 | len;
|
|
262
|
+
mask.copy(header, 2);
|
|
263
|
+
} else if (len < 65536) {
|
|
264
|
+
header = Buffer.alloc(8);
|
|
265
|
+
header[0] = 0x80 | opcode;
|
|
266
|
+
header[1] = 0x80 | 126;
|
|
267
|
+
header.writeUInt16BE(len, 2);
|
|
268
|
+
mask.copy(header, 4);
|
|
269
|
+
} else {
|
|
270
|
+
header = Buffer.alloc(14);
|
|
271
|
+
header[0] = 0x80 | opcode;
|
|
272
|
+
header[1] = 0x80 | 127;
|
|
273
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
274
|
+
mask.copy(header, 10);
|
|
275
|
+
}
|
|
276
|
+
const masked = Buffer.alloc(len);
|
|
277
|
+
for (let i = 0; i < len; i += 1) masked[i] = data[i] ^ mask[i % 4];
|
|
278
|
+
return Buffer.concat([header, masked]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function decodeWsFrames(buf) {
|
|
282
|
+
const frames = [];
|
|
283
|
+
let rest = buf;
|
|
284
|
+
while (rest.length >= 2) {
|
|
285
|
+
const opcode = rest[0] & 0x0f;
|
|
286
|
+
const masked = (rest[1] & 0x80) !== 0;
|
|
287
|
+
let len = rest[1] & 0x7f;
|
|
288
|
+
let offset = 2;
|
|
289
|
+
if (len === 126) {
|
|
290
|
+
if (rest.length < 4) break;
|
|
291
|
+
len = rest.readUInt16BE(2);
|
|
292
|
+
offset = 4;
|
|
293
|
+
} else if (len === 127) {
|
|
294
|
+
if (rest.length < 10) break;
|
|
295
|
+
len = Number(rest.readBigUInt64BE(2));
|
|
296
|
+
offset = 10;
|
|
297
|
+
}
|
|
298
|
+
const maskLen = masked ? 4 : 0;
|
|
299
|
+
if (rest.length < offset + maskLen + len) break;
|
|
300
|
+
let payload = rest.subarray(offset + maskLen, offset + maskLen + len);
|
|
301
|
+
if (masked) {
|
|
302
|
+
const mask = rest.subarray(offset, offset + 4);
|
|
303
|
+
const decoded = Buffer.alloc(len);
|
|
304
|
+
for (let i = 0; i < len; i += 1) decoded[i] = payload[i] ^ mask[i % 4];
|
|
305
|
+
payload = decoded;
|
|
306
|
+
}
|
|
307
|
+
frames.push({ opcode, payload });
|
|
308
|
+
rest = rest.subarray(offset + maskLen + len);
|
|
309
|
+
}
|
|
310
|
+
return { frames, rest };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function cdpSession(wsUrl, { timeoutMs = 8000 } = {}) {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
let u;
|
|
316
|
+
try { u = new URL(wsUrl); } catch (e) { reject(e); return; }
|
|
317
|
+
const key = crypto.randomBytes(16).toString('base64');
|
|
318
|
+
const sock = net.connect({ host: u.hostname, port: Number(u.port || 80) });
|
|
319
|
+
let buf = Buffer.alloc(0);
|
|
320
|
+
let upgraded = false;
|
|
321
|
+
let settled = false;
|
|
322
|
+
let nextId = 0;
|
|
323
|
+
const pending = new Map();
|
|
324
|
+
const timer = setTimeout(() => {
|
|
325
|
+
if (!upgraded) fail(new Error('cdp timeout'));
|
|
326
|
+
}, timeoutMs);
|
|
327
|
+
|
|
328
|
+
function fail(err) {
|
|
329
|
+
if (settled) {
|
|
330
|
+
sock.destroy();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
settled = true;
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
sock.destroy();
|
|
336
|
+
reject(err);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
sock.on('error', fail);
|
|
340
|
+
sock.on('connect', () => {
|
|
341
|
+
sock.write(
|
|
342
|
+
`GET ${u.pathname}${u.search} HTTP/1.1\r\n`
|
|
343
|
+
+ `Host: ${u.host}\r\n`
|
|
344
|
+
+ 'Upgrade: websocket\r\n'
|
|
345
|
+
+ 'Connection: Upgrade\r\n'
|
|
346
|
+
+ 'Sec-WebSocket-Version: 13\r\n'
|
|
347
|
+
+ `Sec-WebSocket-Key: ${key}\r\n`
|
|
348
|
+
+ `Origin: http://${u.host}\r\n`
|
|
349
|
+
+ '\r\n',
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
sock.on('data', (chunk) => {
|
|
353
|
+
buf = Buffer.concat([buf, chunk]);
|
|
354
|
+
if (!upgraded) {
|
|
355
|
+
const idx = buf.indexOf('\r\n\r\n');
|
|
356
|
+
if (idx < 0) return;
|
|
357
|
+
const head = buf.subarray(0, idx).toString('utf8');
|
|
358
|
+
buf = buf.subarray(idx + 4);
|
|
359
|
+
if (!/^HTTP\/1\.1 101/i.test(head)) {
|
|
360
|
+
fail(new Error(`cdp upgrade ${head.split('\r\n')[0] || 'failed'}`));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
upgraded = true;
|
|
364
|
+
settled = true;
|
|
365
|
+
clearTimeout(timer);
|
|
366
|
+
resolve({
|
|
367
|
+
send(method, params) {
|
|
368
|
+
const id = ++nextId;
|
|
369
|
+
return new Promise((res, rej) => {
|
|
370
|
+
const t = setTimeout(() => {
|
|
371
|
+
pending.delete(id);
|
|
372
|
+
rej(new Error(`cdp ${method} timeout`));
|
|
373
|
+
}, Math.max(1000, timeoutMs));
|
|
374
|
+
pending.set(id, {
|
|
375
|
+
res: (v) => { clearTimeout(t); res(v); },
|
|
376
|
+
rej: (e) => { clearTimeout(t); rej(e); },
|
|
377
|
+
});
|
|
378
|
+
try { sock.write(wsMaskFrame(JSON.stringify({ id, method, params }))); } catch (e) {
|
|
379
|
+
clearTimeout(t);
|
|
380
|
+
pending.delete(id);
|
|
381
|
+
rej(e);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
close() {
|
|
386
|
+
clearTimeout(timer);
|
|
387
|
+
try { sock.destroy(); } catch { /* */ }
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const decoded = decodeWsFrames(buf);
|
|
392
|
+
buf = decoded.rest;
|
|
393
|
+
for (const f of decoded.frames) {
|
|
394
|
+
if (f.opcode === 0x9) {
|
|
395
|
+
try { sock.write(wsMaskFrame(f.payload, 0xa)); } catch { /* */ }
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (f.opcode === 0x8) {
|
|
399
|
+
sock.destroy();
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (f.opcode !== 0x1 && f.opcode !== 0x0) continue;
|
|
403
|
+
let msg;
|
|
404
|
+
try { msg = JSON.parse(f.payload.toString('utf8')); } catch { continue; }
|
|
405
|
+
if (msg && msg.id != null && pending.has(msg.id)) {
|
|
406
|
+
const { res, rej } = pending.get(msg.id);
|
|
407
|
+
pending.delete(msg.id);
|
|
408
|
+
if (msg.error) rej(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
409
|
+
else res(msg.result);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export async function listCdpPages(port, fetchImpl = fetch) {
|
|
417
|
+
for (const path of ['/json/list', '/json']) {
|
|
418
|
+
try {
|
|
419
|
+
const r = await fetchImpl(`http://127.0.0.1:${Number(port)}${path}`, {
|
|
420
|
+
signal: AbortSignal.timeout(1500),
|
|
421
|
+
});
|
|
422
|
+
if (!r.ok) continue;
|
|
423
|
+
const j = await r.json();
|
|
424
|
+
const arr = Array.isArray(j) ? j : [];
|
|
425
|
+
return arr.filter((t) => t && t.webSocketDebuggerUrl && t.type !== 'service_worker' && t.type !== 'worker');
|
|
426
|
+
} catch { /* try next */ }
|
|
427
|
+
}
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function injectTarget(target, source, connect) {
|
|
432
|
+
const session = await connect(target.webSocketDebuggerUrl);
|
|
433
|
+
try {
|
|
434
|
+
await session.send('Page.enable').catch(() => {});
|
|
435
|
+
await session.send('Runtime.enable').catch(() => {});
|
|
436
|
+
await session.send('Page.addScriptToEvaluateOnNewDocument', { source }).catch(() => {});
|
|
437
|
+
await session.send('Runtime.evaluate', { expression: source, awaitPromise: false });
|
|
438
|
+
} finally {
|
|
439
|
+
session.close();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function injectSpendChip({
|
|
444
|
+
port = GROKBOT_CDP_PORT,
|
|
445
|
+
source = spendChipSource(),
|
|
446
|
+
log = () => {},
|
|
447
|
+
fetchImpl = fetch,
|
|
448
|
+
connect = cdpSession,
|
|
449
|
+
tries = 40,
|
|
450
|
+
delayMs = 400,
|
|
451
|
+
} = {}) {
|
|
452
|
+
let targets = [];
|
|
453
|
+
for (let i = 0; i < Math.max(1, tries); i += 1) {
|
|
454
|
+
targets = await listCdpPages(port, fetchImpl);
|
|
455
|
+
if (targets.length) break;
|
|
456
|
+
if (i + 1 < tries && delayMs) await new Promise((r) => setTimeout(r, delayMs));
|
|
457
|
+
}
|
|
458
|
+
if (!targets.length) {
|
|
459
|
+
log(`openzoo: spend chip CDP :${port} has no pages yet`);
|
|
460
|
+
return { ok: false, injected: 0 };
|
|
461
|
+
}
|
|
462
|
+
let injected = 0;
|
|
463
|
+
for (const t of targets) {
|
|
464
|
+
try {
|
|
465
|
+
await injectTarget(t, source, connect);
|
|
466
|
+
injected += 1;
|
|
467
|
+
} catch (e) {
|
|
468
|
+
log(`openzoo: spend chip inject ${e.message}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (injected) log(`openzoo: spend chip folded into ${injected} renderer(s)`);
|
|
472
|
+
return { ok: injected > 0, injected };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Fire-and-forget wrapper used by `openzoo bot`. */
|
|
476
|
+
export function injectSpendChipInBackground(opts = {}) {
|
|
477
|
+
const log = opts.log || ((m) => console.error(m));
|
|
478
|
+
injectSpendChip({ ...opts, log }).catch((e) => {
|
|
479
|
+
log(`openzoo: spend chip inject failed ${e.message}`);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
package/lib/pay.js
CHANGED
|
@@ -53,21 +53,16 @@ export class UnderfundedError extends Error {
|
|
|
53
53
|
* that gets funded mid-session recovers on its own.
|
|
54
54
|
*/
|
|
55
55
|
/**
|
|
56
|
-
* BALANCE CACHE —
|
|
56
|
+
* BALANCE CACHE — positive balances SWR for a few seconds; zeros are live.
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* Stale-while-revalidate: a cached value is returned IMMEDIATELY and a refresh is
|
|
65
|
-
* kicked off in the background when it is older than the TTL, so no request ever
|
|
66
|
-
* waits on the network for it. Only a cold cache (first call of the process)
|
|
67
|
-
* blocks. A payment decrements the cached figure locally so a burst of calls
|
|
68
|
-
* cannot overdraw between refreshes.
|
|
58
|
+
* A per-request probe used to add seconds to every call. A 60s SWR of $0 is
|
|
59
|
+
* worse: `openzoo balance` is live, so a TOKEN top-up shows in the CLI while
|
|
60
|
+
* the still-running proxy keeps throwing "underfunded" until the TTL dies.
|
|
61
|
+
* Restarting Grok Bot does not restart the proxy, so four app restarts do
|
|
62
|
+
* nothing. Re-read zeros (the just-funded case) and keep a short TTL on the
|
|
63
|
+
* rest. A payment still decrements the cached figure so a burst cannot overdraw.
|
|
69
64
|
*/
|
|
70
|
-
const BALANCE_TTL_MS = Number(process.env.OPENZOO_BALANCE_TTL_MS ||
|
|
65
|
+
const BALANCE_TTL_MS = Number(process.env.OPENZOO_BALANCE_TTL_MS || 8_000);
|
|
71
66
|
const balanceCache = new Map(); // key -> { raw, ui, at, refreshing }
|
|
72
67
|
|
|
73
68
|
const balKey = (owner, mint) => `${owner}|${mint}`;
|
|
@@ -75,14 +70,14 @@ const balKey = (owner, mint) => `${owner}|${mint}`;
|
|
|
75
70
|
async function cachedTokenBalance(connection, owner, mint, { force = false } = {}) {
|
|
76
71
|
const key = balKey(owner.toBase58 ? owner.toBase58() : String(owner), mint);
|
|
77
72
|
const hit = balanceCache.get(key);
|
|
78
|
-
const
|
|
79
|
-
|
|
73
|
+
const zero = !!(hit && hit.raw === 0n);
|
|
74
|
+
const fresh = hit && !force && !zero && (Date.now() - hit.at) < BALANCE_TTL_MS;
|
|
75
|
+
if (hit && !force && !zero) {
|
|
80
76
|
if (!fresh && !hit.refreshing) {
|
|
81
|
-
// SEMI-FREQUENT REFRESH: fire and forget, so the caller is never blocked.
|
|
82
77
|
hit.refreshing = true;
|
|
83
78
|
tokenBalance(connection, owner, mint)
|
|
84
79
|
.then((b) => balanceCache.set(key, { raw: b.raw, ui: b.ui, at: Date.now(), refreshing: false }))
|
|
85
|
-
.catch(() => { hit.refreshing = false; });
|
|
80
|
+
.catch(() => { hit.refreshing = false; });
|
|
86
81
|
}
|
|
87
82
|
return { raw: hit.raw, ui: hit.ui, cached: true, ageMs: Date.now() - hit.at };
|
|
88
83
|
}
|
|
@@ -101,7 +96,7 @@ function debitCachedBalance(owner, mint, amount) {
|
|
|
101
96
|
/** Test seam. */
|
|
102
97
|
export function resetBalanceCache() { balanceCache.clear(); }
|
|
103
98
|
|
|
104
|
-
const RAIL_MEMO_MS = Number(process.env.OPENZOO_RAIL_MEMO_MS ||
|
|
99
|
+
const RAIL_MEMO_MS = Number(process.env.OPENZOO_RAIL_MEMO_MS || 15_000);
|
|
105
100
|
const underfundedUntil = new Map(); // asset -> epoch ms after which to re-try it
|
|
106
101
|
let lastGoodAsset = null;
|
|
107
102
|
|
|
@@ -213,12 +208,7 @@ export class PayClient {
|
|
|
213
208
|
const rail = railOf(accept);
|
|
214
209
|
if (rail === 'solana') {
|
|
215
210
|
const need = BigInt(accept.maxAmountRequired);
|
|
216
|
-
|
|
217
|
-
// Never declare a wallet short on a STALE read — the expensive top-up path
|
|
218
|
-
// and the underfunded error both deserve a live number.
|
|
219
|
-
if (bal.raw < need && bal.cached) {
|
|
220
|
-
bal = await cachedTokenBalance(this.connection, this.keypair.publicKey, accept.asset, { force: true });
|
|
221
|
-
}
|
|
211
|
+
const bal = await cachedTokenBalance(this.connection, this.keypair.publicKey, accept.asset, { force: true });
|
|
222
212
|
// SHORT IS SHORT. The 402 quotes the raw native mint, so there is nothing
|
|
223
213
|
// to convert and no pool to walk: the wallet either holds the asset or it
|
|
224
214
|
// does not. This branch used to run resolvePool + poolState + a wrap
|
package/lib/proxy.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Readable } from 'node:stream';
|
|
|
7
7
|
import {
|
|
8
8
|
config, FUNDING_ASSETS, EVM_FUNDING_ASSETS, evmRpcFor, fundingLine, liveRails, railFundingHint, railFundingAddresses, unfundableRails, RAIL_FUNDING,
|
|
9
9
|
} from './config.js';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
10
11
|
import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
|
|
11
12
|
import { withOnrampLink } from './stripeOnramp.js';
|
|
12
13
|
import { tokenBalance } from './x402.js';
|
|
@@ -37,6 +38,21 @@ import { receiptUsedCogs, receiptDirectUsd, pairActualBilled } from './racesettl
|
|
|
37
38
|
import { fetchHeaders } from './fetch.js';
|
|
38
39
|
import { attachX402Proof } from './spendProof.js';
|
|
39
40
|
|
|
41
|
+
/** Kill whatever is LISTEN on this port except this process. */
|
|
42
|
+
export function killListen(port, run = execSync) {
|
|
43
|
+
try {
|
|
44
|
+
const pids = run(`lsof -nP -iTCP:${Number(port)} -sTCP:LISTEN -t`, {
|
|
45
|
+
encoding: 'utf8', timeout: 2000,
|
|
46
|
+
}).trim().split('\n').map(Number).filter((n) => Number.isInteger(n) && n > 0 && n !== process.pid);
|
|
47
|
+
for (const pid of pids) {
|
|
48
|
+
try { run(`kill ${pid}`, { stdio: 'ignore', timeout: 2000 }); } catch { /* already gone */ }
|
|
49
|
+
}
|
|
50
|
+
return pids;
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
// THE SHIM IS A FACILITATOR, NOT A MIDDLEBOX.
|
|
41
57
|
//
|
|
42
58
|
// Everything that used to rewrite the request on the way through — model-id
|
|
@@ -817,9 +833,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
817
833
|
const bindHost = process.env.OPENZOO_BIND || '127.0.0.1';
|
|
818
834
|
// SELF-HEAL A TAKEN PORT. Walk up to the next free port instead of dying;
|
|
819
835
|
// the caller reads config.port back out, so every URL printed afterwards is
|
|
820
|
-
// the one we actually bound.
|
|
821
|
-
//
|
|
822
|
-
// spend across two wallets.
|
|
836
|
+
// the one we actually bound. A healthy proxy already on the port is KILLED
|
|
837
|
+
// — reusing it left a stale PayClient serving $0 after a TOKEN top-up.
|
|
823
838
|
const wanted = config.port;
|
|
824
839
|
for (let attempt = 0; ; attempt++) {
|
|
825
840
|
try {
|
|
@@ -832,13 +847,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
832
847
|
} catch (e) {
|
|
833
848
|
if (e?.code !== 'EADDRINUSE' || attempt >= 12) throw e;
|
|
834
849
|
if (attempt === 0) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
} catch { /* not ours, or wedged — take the next port */ }
|
|
850
|
+
const pids = killListen(config.port);
|
|
851
|
+
if (pids.length) {
|
|
852
|
+
say(`openzoo: killed proxy on :${config.port} (pids ${pids.join(',')})`);
|
|
853
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
842
856
|
}
|
|
843
857
|
config.port += 1;
|
|
844
858
|
say(`openzoo: :${config.port - 1} busy — trying :${config.port}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.39",
|
|
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",
|