openzoo 0.49.16 → 0.49.18
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/README.md +8 -8
- package/bin/openzoo.js +0 -11
- package/lib/config.js +18 -28
- package/lib/demo.js +7 -10
- package/lib/evm.js +22 -19
- package/lib/grokui.mjs +13 -317
- package/lib/info.js +9 -10
- package/lib/mcp.js +1 -3
- package/lib/openclaw.js +38 -5
- package/lib/pay.js +74 -270
- package/lib/podagent.mjs +5 -16
- package/lib/proxy.js +4 -10
- package/lib/x402.js +145 -54
- package/package.json +2 -2
- package/lib/evmwrap.js +0 -163
- package/lib/subscription.js +0 -207
- package/lib/wrap.js +0 -332
- package/lib/xbot.js +0 -1576
- package/lib/xburner.js +0 -80
package/lib/grokui.mjs
CHANGED
|
@@ -16,12 +16,6 @@ import { routeChatBody } from './modelroute.js';
|
|
|
16
16
|
import { peekDirectiveStatus, formatRaceStatus, STALE_THINKING_MS, summarizeRaceFailures, RACE_EVERY_FAILED } from './livestatus.js';
|
|
17
17
|
import { creditBalance } from './info.js';
|
|
18
18
|
import { guardFindCwd } from './runguard.js';
|
|
19
|
-
import {
|
|
20
|
-
SUBSCRIPTIONS_PAGE,
|
|
21
|
-
saveSubscription, clearSubscription,
|
|
22
|
-
subscriptionPublicView, parseSubscriptionPaste,
|
|
23
|
-
billingTiers, billingCheckout, fetchBillingKey, ingestBillingKeyResponse,
|
|
24
|
-
} from './subscription.js';
|
|
25
19
|
import {
|
|
26
20
|
prepareChildDir, finishChildDir, lockWorktree, unlockWorktree,
|
|
27
21
|
parsePrRef, fetchSpecsForOrigin, agentSlug,
|
|
@@ -3837,24 +3831,8 @@ const APP_HTML = `<!doctype html>
|
|
|
3837
3831
|
</div>
|
|
3838
3832
|
<div id="walletOverlay" data-component="wallet-modal">
|
|
3839
3833
|
<div id="walletBox">
|
|
3840
|
-
<h3>Pay
|
|
3841
|
-
<div class="wsub">
|
|
3842
|
-
<div id="subLane" data-component="subscribe-lane">
|
|
3843
|
-
<div class="wlanetitle">Subscribe with a card</div>
|
|
3844
|
-
<div class="wtag">Subscription key · no x402</div>
|
|
3845
|
-
<div class="wsub">Same plans as the public page. Checkout opens in the system browser — never an in-app Stripe window. After Stripe, this app polls the site’s key endpoint with the checkout session, or you paste the key from the success page.</div>
|
|
3846
|
-
<div id="subStatus"></div>
|
|
3847
|
-
<div id="subTiers">loading plans…</div>
|
|
3848
|
-
<div class="wpaste">
|
|
3849
|
-
<div class="wlab">I already subscribed — paste key</div>
|
|
3850
|
-
<input id="subKeyInp" type="text" autocomplete="off" spellcheck="false"
|
|
3851
|
-
placeholder="key, or the /billing/done?session=… URL">
|
|
3852
|
-
<button type="button" id="subKeyBtn">Save key</button>
|
|
3853
|
-
<button type="button" id="subForgetBtn" hidden>Remove key</button>
|
|
3854
|
-
</div>
|
|
3855
|
-
<div class="wnote" id="subNote"></div>
|
|
3856
|
-
<div class="wquiet"><a id="subPageLink" href="${SUBSCRIPTIONS_PAGE}" target="_blank" rel="noopener">Full subscriptions page</a></div>
|
|
3857
|
-
</div>
|
|
3834
|
+
<h3>Pay per call</h3>
|
|
3835
|
+
<div class="wsub">Wallet/x402 is the only pay lane. There is no key to buy and none to paste: every call settles on chain from the burner below.</div>
|
|
3858
3836
|
<div class="wlane" id="x402Lane" data-component="x402-lane">
|
|
3859
3837
|
<div class="wlanetitle">Wallet / x402</div>
|
|
3860
3838
|
<div class="wsub">This is <b>your</b> local burner on this machine (or this box). Keys stay in ~/.openzoo/wallet.json. It is not openzoo’s wallet, not a shared zoo account, not the model’s. You fund these deposit addresses; the app pays x402 per call from this wallet. Public addresses only — the UI never shows the key.</div>
|
|
@@ -3910,7 +3888,6 @@ const APP_HTML = `<!doctype html>
|
|
|
3910
3888
|
</div>
|
|
3911
3889
|
<div id="hud">
|
|
3912
3890
|
<div class="htitle">YOUR WALLET · THIS SESSION</div>
|
|
3913
|
-
<div class="hrow" id="hSubRow" hidden><span>subscription</span><span id="hSub" class="hlime">—</span></div>
|
|
3914
3891
|
<div class="hrow"><span>prepaid credit</span><span id="hCredit" class="hlime">—</span></div>
|
|
3915
3892
|
<div class="hrow"><span>you've paid</span><span id="hYouSpent">—</span></div>
|
|
3916
3893
|
<div class="hrow"><span>our cost (cogs)</span><span id="hYouCogs">—</span></div>
|
|
@@ -4389,9 +4366,8 @@ const APP_HTML = `<!doctype html>
|
|
|
4389
4366
|
if (!w || (!w.solana && !w.evm && w.creditUsd == null)) {
|
|
4390
4367
|
const p = document.createElement('div');
|
|
4391
4368
|
p.className = 'wnote wempty';
|
|
4392
|
-
p.textContent = 'Could not reach the local openzoo proxy on :8402. It may still be starting — try again in a few seconds.
|
|
4369
|
+
p.textContent = 'Could not reach the local openzoo proxy on :8402. It may still be starting — try again in a few seconds.';
|
|
4393
4370
|
walletBody.appendChild(p);
|
|
4394
|
-
renderSubLane(w && w.subscription ? w.subscription : null);
|
|
4395
4371
|
return;
|
|
4396
4372
|
}
|
|
4397
4373
|
if (w.creditUsd != null && w.creditUsd !== '') {
|
|
@@ -4425,197 +4401,32 @@ const APP_HTML = `<!doctype html>
|
|
|
4425
4401
|
note.className = 'wnote';
|
|
4426
4402
|
// funded === false is the genuinely-empty case. Undefined means the proxy
|
|
4427
4403
|
// did not say, and guessing "empty" there would send someone to top up a
|
|
4428
|
-
// wallet that is fine.
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
: (w.funded === false
|
|
4434
|
-
? 'This wallet is EMPTY — wallet/x402 calls will fail with HTTP 402 until you fund the addresses above, or subscribe with a card at the top. ' + (w.funding || '')
|
|
4435
|
-
: (w.funding || ''));
|
|
4436
|
-
if (w.funded === false && !subOn) note.classList.add('wempty');
|
|
4404
|
+
// wallet that is fine.
|
|
4405
|
+
note.textContent = w.funded === false
|
|
4406
|
+
? 'This wallet is EMPTY — every call will fail with HTTP 402 until you fund the addresses above. ' + (w.funding || '')
|
|
4407
|
+
: (w.funding || '');
|
|
4408
|
+
if (w.funded === false) note.classList.add('wempty');
|
|
4437
4409
|
if (note.textContent.trim()) walletBody.appendChild(note);
|
|
4438
|
-
renderSubLane(w.subscription || null);
|
|
4439
|
-
}
|
|
4440
|
-
var subPollTimer = null;
|
|
4441
|
-
var subBuying = null;
|
|
4442
|
-
function setSubNote(text, empty) {
|
|
4443
|
-
const el = document.getElementById('subNote');
|
|
4444
|
-
if (!el) return;
|
|
4445
|
-
el.textContent = text || '';
|
|
4446
|
-
el.className = empty ? 'wnote wempty' : 'wnote';
|
|
4447
|
-
}
|
|
4448
|
-
function renderSubLane(sub) {
|
|
4449
|
-
const status = document.getElementById('subStatus');
|
|
4450
|
-
const forget = document.getElementById('subForgetBtn');
|
|
4451
|
-
if (status) {
|
|
4452
|
-
status.innerHTML = '';
|
|
4453
|
-
if (sub && sub.active) {
|
|
4454
|
-
const p = document.createElement('div');
|
|
4455
|
-
p.className = 'wsubon';
|
|
4456
|
-
p.textContent = sub.label || (sub.tierName || 'Subscription') + ' · no x402';
|
|
4457
|
-
status.appendChild(p);
|
|
4458
|
-
}
|
|
4459
|
-
}
|
|
4460
|
-
if (forget) forget.hidden = !(sub && sub.active);
|
|
4461
|
-
loadSubTiers();
|
|
4462
|
-
}
|
|
4463
|
-
async function loadSubTiers() {
|
|
4464
|
-
const box = document.getElementById('subTiers');
|
|
4465
|
-
if (!box) return;
|
|
4466
|
-
let body = null;
|
|
4467
|
-
try {
|
|
4468
|
-
const r = await fetch(API + '/billing/tiers');
|
|
4469
|
-
body = r.ok ? await r.json() : null;
|
|
4470
|
-
} catch (e) { body = null; }
|
|
4471
|
-
box.innerHTML = '';
|
|
4472
|
-
const tiers = body && body.ok && Array.isArray(body.tiers) ? body.tiers : [];
|
|
4473
|
-
if (!tiers.length) {
|
|
4474
|
-
const p = document.createElement('div');
|
|
4475
|
-
p.className = 'wnote wempty';
|
|
4476
|
-
p.textContent = 'Could not load live plans from zoo.openzoo.fun — try again, or use the full subscriptions page.';
|
|
4477
|
-
box.appendChild(p);
|
|
4478
|
-
return;
|
|
4479
|
-
}
|
|
4480
|
-
tiers.forEach(function (t) {
|
|
4481
|
-
const art = document.createElement('div');
|
|
4482
|
-
art.className = 'wtier' + (t.id === 'pro' ? ' hot' : '');
|
|
4483
|
-
art.setAttribute('data-tier', t.id);
|
|
4484
|
-
const tag = document.createElement('div');
|
|
4485
|
-
tag.className = 'wtag';
|
|
4486
|
-
tag.textContent = t.id === 'pro' ? 'Most teams want this' : '';
|
|
4487
|
-
const name = document.createElement('div');
|
|
4488
|
-
name.className = 'wtn';
|
|
4489
|
-
name.textContent = t.name || t.id;
|
|
4490
|
-
const price = document.createElement('div');
|
|
4491
|
-
price.className = 'wtp';
|
|
4492
|
-
price.textContent = '$' + ((Number(t.monthlyCents) || 0) / 100).toFixed(0) + '/mo';
|
|
4493
|
-
const share = document.createElement('div');
|
|
4494
|
-
share.className = 'wts';
|
|
4495
|
-
share.textContent = (t.savingsSharePct != null ? t.savingsSharePct : '?') + '% savings share';
|
|
4496
|
-
const blurb = document.createElement('div');
|
|
4497
|
-
blurb.className = 'wtb';
|
|
4498
|
-
blurb.textContent = t.blurb || '';
|
|
4499
|
-
const btn = document.createElement('button');
|
|
4500
|
-
btn.type = 'button';
|
|
4501
|
-
btn.textContent = subBuying === t.id ? 'Opening checkout…' : ('Get ' + (t.name || t.id));
|
|
4502
|
-
btn.disabled = subBuying != null;
|
|
4503
|
-
btn.addEventListener('click', function () { buyTier(t.id); });
|
|
4504
|
-
art.append(tag, name, price, share, blurb, btn);
|
|
4505
|
-
box.appendChild(art);
|
|
4506
|
-
});
|
|
4507
|
-
}
|
|
4508
|
-
function openSystemBrowser(url) {
|
|
4509
|
-
// Electron's setWindowOpenHandler routes target=_blank to shell.openExternal.
|
|
4510
|
-
// Never load Stripe inside this window.
|
|
4511
|
-
window.open(url, '_blank', 'noopener,noreferrer');
|
|
4512
|
-
}
|
|
4513
|
-
function stopSubPoll() {
|
|
4514
|
-
if (subPollTimer) { clearInterval(subPollTimer); subPollTimer = null; }
|
|
4515
|
-
}
|
|
4516
|
-
function startSubPoll(sessionId) {
|
|
4517
|
-
stopSubPoll();
|
|
4518
|
-
var tries = 0;
|
|
4519
|
-
async function tick() {
|
|
4520
|
-
tries += 1;
|
|
4521
|
-
try {
|
|
4522
|
-
const r = await fetch(API + '/billing/key?session=' + encodeURIComponent(sessionId));
|
|
4523
|
-
const j = r.ok ? await r.json() : null;
|
|
4524
|
-
if (j && j.saved) {
|
|
4525
|
-
stopSubPoll();
|
|
4526
|
-
setSubNote('Subscription key saved · no x402');
|
|
4527
|
-
await openWallet();
|
|
4528
|
-
return;
|
|
4529
|
-
}
|
|
4530
|
-
if (j && j.pending) setSubNote('Waiting for Stripe to confirm…');
|
|
4531
|
-
else if (j && j.error && j.error !== 'session required') setSubNote(j.error, true);
|
|
4532
|
-
} catch (e) { /* keep polling */ }
|
|
4533
|
-
if (tries >= 60) {
|
|
4534
|
-
stopSubPoll();
|
|
4535
|
-
setSubNote('Still waiting on Stripe — paste the key from the success page if you have it.');
|
|
4536
|
-
}
|
|
4537
|
-
}
|
|
4538
|
-
subPollTimer = setInterval(tick, 2000);
|
|
4539
|
-
tick();
|
|
4540
|
-
}
|
|
4541
|
-
async function buyTier(tier) {
|
|
4542
|
-
subBuying = tier;
|
|
4543
|
-
setSubNote('');
|
|
4544
|
-
loadSubTiers();
|
|
4545
|
-
try {
|
|
4546
|
-
const r = await fetch(API + '/billing/checkout', {
|
|
4547
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
4548
|
-
body: JSON.stringify({ tier: tier }),
|
|
4549
|
-
});
|
|
4550
|
-
const j = await r.json();
|
|
4551
|
-
if (!j || !j.ok || !j.url) throw new Error((j && j.error) || 'checkout failed');
|
|
4552
|
-
openSystemBrowser(j.url);
|
|
4553
|
-
setSubNote('Checkout opened in your system browser. This window will pick up the key when Stripe confirms.');
|
|
4554
|
-
if (j.sessionId) startSubPoll(j.sessionId);
|
|
4555
|
-
} catch (e) {
|
|
4556
|
-
setSubNote(e.message || String(e), true);
|
|
4557
|
-
}
|
|
4558
|
-
subBuying = null;
|
|
4559
|
-
loadSubTiers();
|
|
4560
|
-
}
|
|
4561
|
-
async function savePastedSub() {
|
|
4562
|
-
const inp = document.getElementById('subKeyInp');
|
|
4563
|
-
const paste = inp ? inp.value.trim() : '';
|
|
4564
|
-
if (!paste) { setSubNote('Paste a key or the billing/done URL.', true); return; }
|
|
4565
|
-
try {
|
|
4566
|
-
const r = await fetch(API + '/billing/key', {
|
|
4567
|
-
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
4568
|
-
body: JSON.stringify({ paste: paste }),
|
|
4569
|
-
});
|
|
4570
|
-
const j = await r.json();
|
|
4571
|
-
if (j && j.pending && j.session) {
|
|
4572
|
-
setSubNote('Waiting for Stripe to confirm…');
|
|
4573
|
-
startSubPoll(j.session);
|
|
4574
|
-
return;
|
|
4575
|
-
}
|
|
4576
|
-
if (!j || !j.saved) throw new Error((j && j.error) || 'could not save key');
|
|
4577
|
-
if (inp) inp.value = '';
|
|
4578
|
-
setSubNote('Subscription key saved · no x402');
|
|
4579
|
-
await openWallet();
|
|
4580
|
-
} catch (e) {
|
|
4581
|
-
setSubNote(e.message || String(e), true);
|
|
4582
|
-
}
|
|
4583
|
-
}
|
|
4584
|
-
async function forgetSub() {
|
|
4585
|
-
try {
|
|
4586
|
-
await fetch(API + '/billing/key', { method: 'DELETE' });
|
|
4587
|
-
} catch (e) { /* still refresh */ }
|
|
4588
|
-
setSubNote('Subscription key removed. Wallet/x402 is the pay method again.');
|
|
4589
|
-
await openWallet();
|
|
4590
4410
|
}
|
|
4591
4411
|
document.getElementById('walletBtn').addEventListener('click', () => {
|
|
4592
4412
|
echoSlash('/pay');
|
|
4593
4413
|
openWallet();
|
|
4594
4414
|
});
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
if (subForgetBtn) subForgetBtn.addEventListener('click', forgetSub);
|
|
4599
|
-
const subKeyInp = document.getElementById('subKeyInp');
|
|
4600
|
-
if (subKeyInp) subKeyInp.addEventListener('keydown', function (e) {
|
|
4601
|
-
if (e.key === 'Enter') { e.preventDefault(); savePastedSub(); }
|
|
4602
|
-
});
|
|
4603
|
-
// First launch: if the burner is empty AND there is no subscription, open
|
|
4604
|
-
// the pay modal once so they see card plans first — and burner addresses
|
|
4605
|
-
// below. localStorage so a funded session, a saved key, or a dismiss does
|
|
4606
|
-
// not keep popping it.
|
|
4415
|
+
// First launch: if the burner is empty, open the pay modal once so the
|
|
4416
|
+
// deposit addresses are the first thing seen. localStorage so a funded
|
|
4417
|
+
// session or a dismiss does not keep popping it.
|
|
4607
4418
|
(async function maybeOpenWalletOnce() {
|
|
4608
4419
|
if (localStorage.getItem('openzoo.wallet.seen')) return;
|
|
4609
4420
|
for (let i = 0; i < 8; i++) {
|
|
4610
4421
|
try {
|
|
4611
4422
|
const r = await fetch(API + '/wallet');
|
|
4612
4423
|
const w = r.ok ? await r.json() : null;
|
|
4613
|
-
if (!w || (!w.solana && !w.evm
|
|
4424
|
+
if (!w || (!w.solana && !w.evm)) {
|
|
4614
4425
|
await new Promise((res) => setTimeout(res, 400));
|
|
4615
4426
|
continue;
|
|
4616
4427
|
}
|
|
4617
4428
|
localStorage.setItem('openzoo.wallet.seen', '1');
|
|
4618
|
-
if (w.funded === false
|
|
4429
|
+
if (w.funded === false) await openWallet();
|
|
4619
4430
|
return;
|
|
4620
4431
|
} catch (e) {
|
|
4621
4432
|
await new Promise((res) => setTimeout(res, 400));
|
|
@@ -6077,16 +5888,6 @@ const APP_HTML = `<!doctype html>
|
|
|
6077
5888
|
const you = await (await fetch(API + '/hud-summary')).json();
|
|
6078
5889
|
const creditEl = document.getElementById('hCredit');
|
|
6079
5890
|
if (creditEl) creditEl.textContent = (you.creditUsd == null) ? '—' : usd(Number(you.creditUsd) || 0);
|
|
6080
|
-
const subRow = document.getElementById('hSubRow');
|
|
6081
|
-
const subEl = document.getElementById('hSub');
|
|
6082
|
-
if (subRow && subEl) {
|
|
6083
|
-
if (you.subscription && you.subscription.active) {
|
|
6084
|
-
subRow.hidden = false;
|
|
6085
|
-
subEl.textContent = you.subscription.label || you.subscription.tierName || 'Subscription key · no x402';
|
|
6086
|
-
} else {
|
|
6087
|
-
subRow.hidden = true;
|
|
6088
|
-
}
|
|
6089
|
-
}
|
|
6090
5891
|
const spent = Number(you.spentUsd) || 0;
|
|
6091
5892
|
const cogs = Number(you.cogsUsd) || 0;
|
|
6092
5893
|
const direct = Number(you.directUsd) || 0;
|
|
@@ -6185,116 +5986,12 @@ const server = http.createServer((req, res) => {
|
|
|
6185
5986
|
try {
|
|
6186
5987
|
w.creditUsd = await creditBalance();
|
|
6187
5988
|
} catch { /* leave credit off if the gateway is down */ }
|
|
6188
|
-
// Subscription is local (~/.openzoo/subscription.json). Merge it here so
|
|
6189
|
-
// an older :8402 that does not yet know about Stripe still shows the lane.
|
|
6190
|
-
w.subscription = subscriptionPublicView();
|
|
6191
5989
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6192
5990
|
res.end(JSON.stringify(w));
|
|
6193
5991
|
})();
|
|
6194
5992
|
return;
|
|
6195
5993
|
}
|
|
6196
5994
|
|
|
6197
|
-
// Live Stripe plans — never a stale hardcoded $9/$29/$99. Same origin so
|
|
6198
|
-
// the renderer does not have to talk to zoo.openzoo.fun itself.
|
|
6199
|
-
if (req.method === 'GET' && req.url === '/billing/tiers') {
|
|
6200
|
-
(async () => {
|
|
6201
|
-
try {
|
|
6202
|
-
const body = await billingTiers();
|
|
6203
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6204
|
-
res.end(JSON.stringify(body));
|
|
6205
|
-
} catch (e) {
|
|
6206
|
-
res.writeHead(502, { 'content-type': 'application/json' });
|
|
6207
|
-
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
6208
|
-
}
|
|
6209
|
-
})();
|
|
6210
|
-
return;
|
|
6211
|
-
}
|
|
6212
|
-
|
|
6213
|
-
if (req.method === 'POST' && req.url === '/billing/checkout') {
|
|
6214
|
-
const chunks = [];
|
|
6215
|
-
req.on('data', (d) => chunks.push(d));
|
|
6216
|
-
req.on('end', async () => {
|
|
6217
|
-
let tier = '';
|
|
6218
|
-
try { tier = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}').tier || ''; }
|
|
6219
|
-
catch { /* ignore */ }
|
|
6220
|
-
try {
|
|
6221
|
-
const body = await billingCheckout(tier);
|
|
6222
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6223
|
-
res.end(JSON.stringify(body));
|
|
6224
|
-
} catch (e) {
|
|
6225
|
-
res.writeHead(502, { 'content-type': 'application/json' });
|
|
6226
|
-
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
6227
|
-
}
|
|
6228
|
-
});
|
|
6229
|
-
return;
|
|
6230
|
-
}
|
|
6231
|
-
|
|
6232
|
-
// GET ?session= polls the same endpoint the public /billing/done page uses.
|
|
6233
|
-
// On a key, persist it locally and return a public view — never the secret
|
|
6234
|
-
// (this UI can sit on a public box URL).
|
|
6235
|
-
if (req.method === 'GET' && (req.url || '').startsWith('/billing/key')) {
|
|
6236
|
-
(async () => {
|
|
6237
|
-
const q = new URL(req.url, 'http://x').searchParams;
|
|
6238
|
-
const session = q.get('session') || q.get('session_id') || '';
|
|
6239
|
-
try {
|
|
6240
|
-
const body = await fetchBillingKey(session);
|
|
6241
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6242
|
-
res.end(JSON.stringify(ingestBillingKeyResponse(body, {
|
|
6243
|
-
sessionId: session,
|
|
6244
|
-
tier: q.get('tier') || null,
|
|
6245
|
-
})));
|
|
6246
|
-
} catch (e) {
|
|
6247
|
-
res.writeHead(502, { 'content-type': 'application/json' });
|
|
6248
|
-
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
6249
|
-
}
|
|
6250
|
-
})();
|
|
6251
|
-
return;
|
|
6252
|
-
}
|
|
6253
|
-
|
|
6254
|
-
if (req.method === 'POST' && req.url === '/billing/key') {
|
|
6255
|
-
const chunks = [];
|
|
6256
|
-
req.on('data', (d) => chunks.push(d));
|
|
6257
|
-
req.on('end', async () => {
|
|
6258
|
-
let paste = '';
|
|
6259
|
-
let tier = '';
|
|
6260
|
-
try {
|
|
6261
|
-
const j = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
|
6262
|
-
paste = j.paste || j.key || '';
|
|
6263
|
-
tier = j.tier || '';
|
|
6264
|
-
} catch { /* ignore */ }
|
|
6265
|
-
const parsed = parseSubscriptionPaste(paste);
|
|
6266
|
-
if (parsed.error) {
|
|
6267
|
-
res.writeHead(400, { 'content-type': 'application/json' });
|
|
6268
|
-
res.end(JSON.stringify({ ok: false, error: parsed.error }));
|
|
6269
|
-
return;
|
|
6270
|
-
}
|
|
6271
|
-
if (parsed.session) {
|
|
6272
|
-
try {
|
|
6273
|
-
const body = await fetchBillingKey(parsed.session);
|
|
6274
|
-
const out = ingestBillingKeyResponse(body, { sessionId: parsed.session, tier });
|
|
6275
|
-
if (out.pending) out.session = parsed.session;
|
|
6276
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6277
|
-
res.end(JSON.stringify(out));
|
|
6278
|
-
} catch (e) {
|
|
6279
|
-
res.writeHead(502, { 'content-type': 'application/json' });
|
|
6280
|
-
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
6281
|
-
}
|
|
6282
|
-
return;
|
|
6283
|
-
}
|
|
6284
|
-
saveSubscription({ key: parsed.key, tier: tier || null });
|
|
6285
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6286
|
-
res.end(JSON.stringify({ ok: true, saved: true, ...subscriptionPublicView() }));
|
|
6287
|
-
});
|
|
6288
|
-
return;
|
|
6289
|
-
}
|
|
6290
|
-
|
|
6291
|
-
if (req.method === 'DELETE' && req.url === '/billing/key') {
|
|
6292
|
-
clearSubscription();
|
|
6293
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6294
|
-
res.end(JSON.stringify({ ok: true, saved: false, ...subscriptionPublicView() }));
|
|
6295
|
-
return;
|
|
6296
|
-
}
|
|
6297
|
-
|
|
6298
5995
|
// Restart grokui in place — and ACTUALLY PICK UP THE NEW BUILD.
|
|
6299
5996
|
//
|
|
6300
5997
|
// Exiting is the restart: on a production box, box-server's ensureOz() poll
|
|
@@ -6351,7 +6048,6 @@ const server = http.createServer((req, res) => {
|
|
|
6351
6048
|
try {
|
|
6352
6049
|
you.creditUsd = await creditBalance();
|
|
6353
6050
|
} catch { /* credit is advisory */ }
|
|
6354
|
-
you.subscription = subscriptionPublicView();
|
|
6355
6051
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
6356
6052
|
res.end(JSON.stringify(you));
|
|
6357
6053
|
})();
|
package/lib/info.js
CHANGED
|
@@ -39,8 +39,11 @@ function fmtUi(raw, decimals) {
|
|
|
39
39
|
* The quote already carries `extra.tokenUsd` for every asset it settles in —
|
|
40
40
|
* the very "priced at the 402" this file kept promising in a footnote while
|
|
41
41
|
* printing `$?`. So a wallet holding 845,486 TOKEN summed to "≈ $0.11 known".
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
*
|
|
43
|
+
* Symbols are the plain tickers the wallet holds. They used to arrive as
|
|
44
|
+
* wrapper twins that had to be mapped back to the underlying before a holding
|
|
45
|
+
* could find its price; the gateway quotes the native mint now, so the symbol
|
|
46
|
+
* on the row is already the symbol in the wallet.
|
|
44
47
|
*/
|
|
45
48
|
export async function quotedPrices() {
|
|
46
49
|
const out = {};
|
|
@@ -71,10 +74,7 @@ export async function quotedPrices() {
|
|
|
71
74
|
const usd = Number(row?.extra?.tokenUsd);
|
|
72
75
|
const sym = String(row?.extra?.symbol || '');
|
|
73
76
|
if (!sym || !Number.isFinite(usd)) continue;
|
|
74
|
-
// wTOKENx -> TOKEN, wUSDGx -> USDG, yUSDCx -> USDC
|
|
75
|
-
const plain = sym.replace(/^[wy]/, '').replace(/x$/, '').toUpperCase();
|
|
76
77
|
out[sym.toUpperCase()] = usd;
|
|
77
|
-
if (!(plain in out)) out[plain] = usd;
|
|
78
78
|
}
|
|
79
79
|
} catch { /* offline: fall back to printing what we know */ }
|
|
80
80
|
return out;
|
|
@@ -179,11 +179,10 @@ export async function affordableUsd() {
|
|
|
179
179
|
const perUsd = BigInt(row.maxAmountRequired || '0');
|
|
180
180
|
if (perUsd <= 0n) continue;
|
|
181
181
|
try {
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
// sent them off to fund an account that was already funded.
|
|
182
|
+
// The quote names the raw mint, so the wallet's balance of it IS what it
|
|
183
|
+
// can spend. Back when rows named a Token-2022 twin this had to count the
|
|
184
|
+
// convertible plain token too, and not doing so told a user holding
|
|
185
|
+
// 14,847 TOKEN their wallet "covers only $1.0230".
|
|
187
186
|
let usd;
|
|
188
187
|
if (client.spendableUsdForAccept) {
|
|
189
188
|
usd = await client.spendableUsdForAccept(row);
|
package/lib/mcp.js
CHANGED
|
@@ -8,7 +8,6 @@ import { tokenBalance } from './x402.js';
|
|
|
8
8
|
import { askWithContext, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
|
|
9
9
|
import { listContexts } from './contexts.js';
|
|
10
10
|
import { withNamespace } from './namespace.js';
|
|
11
|
-
import { subscriptionPublicView } from './subscription.js';
|
|
12
11
|
|
|
13
12
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
14
13
|
// The model zoo_ask uses when the caller does not name one. Opus 5 by default:
|
|
@@ -383,10 +382,9 @@ export function buildMcpServer() {
|
|
|
383
382
|
railsLiveNow: rails?.live ?? null,
|
|
384
383
|
fundWith: hint || `${FUNDING_ASSETS.map((a) => a.symbol).join(' or ')} on Solana`,
|
|
385
384
|
solanaMints: Object.fromEntries(FUNDING_ASSETS.map((a) => [a.symbol, a.mint])),
|
|
386
|
-
fundHint: 'send a few cents of a listed asset to the address for that rail — Solana assets to solanaAddress, Base assets to evmAddress.
|
|
385
|
+
fundHint: 'send a few cents of a listed asset to the address for that rail — Solana assets to solanaAddress, Base and Robinhood assets to evmAddress. These are the exact mints the 402 quotes: whatever you fund is what gets spent, with no conversion or wrapping step. Force a rail with OPENZOO_RAIL=solana|base|robinhood.',
|
|
387
386
|
balances,
|
|
388
387
|
receipts: client.receipts.map((r) => ({ at: r.at, line: r.line })),
|
|
389
|
-
subscription: subscriptionPublicView(),
|
|
390
388
|
});
|
|
391
389
|
});
|
|
392
390
|
|
package/lib/openclaw.js
CHANGED
|
@@ -133,7 +133,13 @@ async function fetchCatalogRows() {
|
|
|
133
133
|
const r = await fetchHeaders(`${config.apiBase}/v1/models`);
|
|
134
134
|
if (!r.ok) throw new Error(`model catalog fetch failed: HTTP ${r.status}`);
|
|
135
135
|
const d = await r.json();
|
|
136
|
-
|
|
136
|
+
// AUTO STAYS IN THE CATALOG. It used to be stripped here, which made it
|
|
137
|
+
// unreachable by EVERY route — `--models openzoo/auto` failed with "not in
|
|
138
|
+
// the live catalog", and OpenClaw refused a hand-edited config with
|
|
139
|
+
// `model not allowed: openzoo/auto`. The price objection below is about the
|
|
140
|
+
// cost BLOCK, and the answer to that is to quote a ceiling, not to delete
|
|
141
|
+
// the router we tell people to use.
|
|
142
|
+
return quoteableRows(d.data);
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
function pickRows(rows, { all, wanted }) {
|
|
@@ -146,10 +152,25 @@ function pickRows(rows, { all, wanted }) {
|
|
|
146
152
|
return wanted.map((id) => byId.get(id));
|
|
147
153
|
}
|
|
148
154
|
if (all) return rows;
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
// Same flagship picker Claude Code gets, WITH Auto pinned first.
|
|
156
|
+
//
|
|
157
|
+
// Auto's price genuinely varies per route, so no fixed cost block is exact.
|
|
158
|
+
// But omitting it entirely is the worse lie: it tells the harness the router
|
|
159
|
+
// does not exist. The command already prints "prices are the CEILING
|
|
160
|
+
// (openrouter-direct)" over every row it writes, and a ceiling is precisely
|
|
161
|
+
// what an upper bound across the picked set is — see the cost fix-up in
|
|
162
|
+
// setupOpenClaw.
|
|
163
|
+
const picked = pickClaudePickerRows(rows);
|
|
164
|
+
const withoutAuto = picked.filter((m) => !isAutoModel(m.id));
|
|
165
|
+
// PREFER THE BARE `auto` ID. The catalog offers `auto`, `openzoo/auto` and
|
|
166
|
+
// `openzoo-auto` for the same router. OpenClaw renders a model as
|
|
167
|
+
// `<provider>/<id>` and our provider key is already `openzoo`, so taking the
|
|
168
|
+
// prefixed row writes `openzoo/openzoo/auto` — which is not the string the
|
|
169
|
+
// user types and OpenClaw rejects it as "model not allowed".
|
|
170
|
+
const autos = rows.filter((m) => isAutoModel(m.id));
|
|
171
|
+
const auto = autos.find((m) => m.id === "auto") ?? autos[0];
|
|
172
|
+
const ordered = auto ? [auto, ...withoutAuto] : withoutAuto;
|
|
173
|
+
return ordered.length ? ordered : rows.slice(0, 8);
|
|
153
174
|
}
|
|
154
175
|
|
|
155
176
|
/**
|
|
@@ -170,6 +191,18 @@ export async function setupOpenClaw(argv = []) {
|
|
|
170
191
|
const rows = pickRows(await fetchCatalogRows(), opt);
|
|
171
192
|
if (!rows.length) throw new Error('live catalog returned no quoteable models');
|
|
172
193
|
const entries = rows.map(openclawModelEntry);
|
|
194
|
+
// AUTO IS PRICED AT THE CEILING OF WHAT IT CAN ROUTE TO. Its own catalog row
|
|
195
|
+
// carries no usable price (the route is chosen per request), and a zero there
|
|
196
|
+
// reads to a budgeting harness as "free", which is the one number guaranteed
|
|
197
|
+
// wrong. The max across everything written alongside it cannot be exceeded by
|
|
198
|
+
// any route Auto picks from that set, so it over-states and never under-states.
|
|
199
|
+
const autoEntry = entries.find((e) => isAutoModel(e.id));
|
|
200
|
+
if (autoEntry) {
|
|
201
|
+
const others = entries.filter((e) => e !== autoEntry);
|
|
202
|
+
const ceil = (k) => others.reduce((m, e) => Math.max(m, Number(e.cost?.[k]) || 0), 0);
|
|
203
|
+
autoEntry.cost = { input: ceil("input"), output: ceil("output"), cacheRead: 0, cacheWrite: 0 };
|
|
204
|
+
}
|
|
205
|
+
// Auto is written first, so it is also the default unless overridden.
|
|
173
206
|
const defaultId = opt.defaultId || entries[0].id;
|
|
174
207
|
if (!entries.some((e) => e.id === defaultId)) {
|
|
175
208
|
throw new Error(`--default ${defaultId} is not among the written models (add it via --models)`);
|