openzoo 0.50.26 → 0.50.27
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 +288 -73
- package/lib/grokbotAccount.js +49 -0
- package/lib/pay.js +18 -4
- package/lib/proxy.js +14 -5
- package/lib/stripeOnramp.js +92 -0
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -37,6 +37,9 @@ import { promisify } from 'node:util';
|
|
|
37
37
|
import tls from 'node:tls';
|
|
38
38
|
import { randomUUID } from 'node:crypto';
|
|
39
39
|
import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetGrokBotSendStatus, decodeProtoFields, unwrapConnect } from './cursorapi.js';
|
|
40
|
+
import {
|
|
41
|
+
accountPodPath, accountAgentsPath, rosterForAccount,
|
|
42
|
+
} from './grokbotAccount.js';
|
|
40
43
|
|
|
41
44
|
const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
|
|
42
45
|
const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'repo42.cursor.sh'];
|
|
@@ -290,27 +293,76 @@ const RESP_DROP = new Set([
|
|
|
290
293
|
|
|
291
294
|
const SNIFF_FILE = path.join(os.homedir(), '.openzoo', 'grokbot-sniff.jsonl');
|
|
292
295
|
const POD_FILE = path.join(os.homedir(), '.openzoo', 'grokbot-pod.json');
|
|
296
|
+
const AGENTS_FILE = path.join(os.homedir(), '.openzoo', 'grokbot-agents.json');
|
|
297
|
+
const HOME = os.homedir();
|
|
298
|
+
|
|
299
|
+
function readJsonFile(p) {
|
|
300
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
301
|
+
}
|
|
302
|
+
function writeJsonFile(p, v) {
|
|
303
|
+
try {
|
|
304
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
305
|
+
fs.writeFileSync(p, JSON.stringify(v));
|
|
306
|
+
} catch { /* */ }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let activeAccountId = null;
|
|
310
|
+
|
|
311
|
+
function migrateLegacyPod(accountId) {
|
|
312
|
+
if (!accountId) return;
|
|
313
|
+
const dest = accountPodPath(HOME, accountId);
|
|
314
|
+
const destA = accountAgentsPath(HOME, accountId);
|
|
315
|
+
if (!dest) return;
|
|
316
|
+
const legacy = readJsonFile(POD_FILE);
|
|
317
|
+
if (legacy?.agent && (!legacy.accountId || legacy.accountId === accountId) && !readJsonFile(dest)) {
|
|
318
|
+
writeJsonFile(dest, legacy);
|
|
319
|
+
}
|
|
320
|
+
if (legacy?.accountId === accountId && !readJsonFile(destA)) {
|
|
321
|
+
const agents = readJsonFile(AGENTS_FILE);
|
|
322
|
+
if (Array.isArray(agents) && agents.length) writeJsonFile(destA, agents);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
293
326
|
function loadPod() {
|
|
294
|
-
|
|
327
|
+
const legacy = readJsonFile(POD_FILE);
|
|
328
|
+
const id = legacy?.accountId || activeAccountId;
|
|
329
|
+
if (id) {
|
|
330
|
+
migrateLegacyPod(id);
|
|
331
|
+
const scoped = readJsonFile(accountPodPath(HOME, id));
|
|
332
|
+
if (scoped?.agent) return scoped;
|
|
333
|
+
}
|
|
334
|
+
return legacy?.agent ? legacy : null;
|
|
295
335
|
}
|
|
296
336
|
function savePod(p) {
|
|
297
337
|
if (!p?.agent) return;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
338
|
+
writeJsonFile(POD_FILE, p); // last-used pointer for same-user relaunch
|
|
339
|
+
if (p.accountId) writeJsonFile(accountPodPath(HOME, p.accountId), p);
|
|
340
|
+
}
|
|
341
|
+
let realPod = loadPod();
|
|
342
|
+
activeAccountId = realPod?.accountId || null;
|
|
343
|
+
if (activeAccountId) migrateLegacyPod(activeAccountId);
|
|
344
|
+
|
|
345
|
+
function agentsPath() {
|
|
346
|
+
if (!activeAccountId) return null;
|
|
347
|
+
return accountAgentsPath(HOME, activeAccountId) || AGENTS_FILE;
|
|
302
348
|
}
|
|
303
|
-
let realPod = loadPod(); // { agent, vnc, token, p1340, ... } — persist so api2 timeout does not UUID-stub the sidebar
|
|
304
|
-
const AGENTS_FILE = path.join(os.homedir(), '.openzoo', 'grokbot-agents.json');
|
|
305
349
|
function loadAgents() {
|
|
306
|
-
|
|
307
|
-
|
|
350
|
+
// No live account → no tray. Serving the machine-global file here is how a
|
|
351
|
+
// second household login saw the wrong chats (or none of theirs).
|
|
352
|
+
if (!activeAccountId) return null;
|
|
353
|
+
const scoped = readJsonFile(accountAgentsPath(HOME, activeAccountId));
|
|
354
|
+
if (Array.isArray(scoped)) return scoped;
|
|
355
|
+
const legacyPod = readJsonFile(POD_FILE);
|
|
356
|
+
if (legacyPod?.accountId === activeAccountId) {
|
|
357
|
+
const a = readJsonFile(AGENTS_FILE);
|
|
308
358
|
return Array.isArray(a) ? a : null;
|
|
309
|
-
}
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
310
361
|
}
|
|
311
362
|
function saveAgents(a) {
|
|
312
|
-
if (!Array.isArray(a) || !a.length) return;
|
|
313
|
-
|
|
363
|
+
if (!Array.isArray(a) || !a.length || !activeAccountId) return;
|
|
364
|
+
const p = agentsPath();
|
|
365
|
+
if (p) writeJsonFile(p, a);
|
|
314
366
|
}
|
|
315
367
|
function cachedAgentList() {
|
|
316
368
|
const a = loadAgents();
|
|
@@ -346,6 +398,10 @@ function copyReqHeaders(req, host) {
|
|
|
346
398
|
}
|
|
347
399
|
if (host) headers.host = host;
|
|
348
400
|
delete headers['content-length'];
|
|
401
|
+
// Electron POSTs Expect: 100-continue. cursorvm 1340 answers 417 and the
|
|
402
|
+
// splash never leaves "Setting up Grok Bot's computer" (measured 2026-08-29).
|
|
403
|
+
delete headers.expect;
|
|
404
|
+
delete headers.Expect;
|
|
349
405
|
return headers;
|
|
350
406
|
}
|
|
351
407
|
function lookupPinned(ip) {
|
|
@@ -459,6 +515,13 @@ function rememberPod(fields, log) {
|
|
|
459
515
|
if (!fields || !fields[6]) return null;
|
|
460
516
|
const execDaemon = String(fields[6] || '');
|
|
461
517
|
const gateway = String(fields[10] || fields[6] || '');
|
|
518
|
+
const accountId = String(fields[2] || '');
|
|
519
|
+
if (activeAccountId && accountId && accountId !== activeAccountId) {
|
|
520
|
+
log(`cursor-backend: account ${activeAccountId} -> ${accountId} (drop previous tray)`);
|
|
521
|
+
cacheFallbackOk = true;
|
|
522
|
+
}
|
|
523
|
+
activeAccountId = accountId || activeAccountId;
|
|
524
|
+
if (activeAccountId) migrateLegacyPod(activeAccountId);
|
|
462
525
|
realPod = {
|
|
463
526
|
execDaemon,
|
|
464
527
|
agent: gateway, // /api/sendPrompt lives on gateway_url (1340), not exec_daemon (1337)
|
|
@@ -469,18 +532,19 @@ function rememberPod(fields, log) {
|
|
|
469
532
|
p1340: gateway,
|
|
470
533
|
p6081: fields[12] ? String(fields[12]) : undefined,
|
|
471
534
|
region: String(fields[1] || 'us1'),
|
|
472
|
-
accountId
|
|
535
|
+
accountId,
|
|
473
536
|
podId: String(fields[3] || ''),
|
|
474
537
|
};
|
|
475
538
|
sniffDump({ kind: 'pod', fields, realPod });
|
|
476
539
|
savePod(realPod);
|
|
477
|
-
|
|
540
|
+
podStale = false;
|
|
541
|
+
log(`cursor-backend: SNIFF real pod account=${accountId || '?'} ${realPod.agent}`);
|
|
478
542
|
return realPod;
|
|
479
543
|
}
|
|
480
544
|
|
|
481
|
-
function rewrittenBox() {
|
|
545
|
+
function rewrittenBox({ confirmed = true } = {}) {
|
|
482
546
|
const self = sniffSelf();
|
|
483
|
-
const p = realPod
|
|
547
|
+
const p = (confirmed && realPod) ? realPod : {};
|
|
484
548
|
return encodeEnsureSandBox({
|
|
485
549
|
region: p.region || 'us1',
|
|
486
550
|
accountId: p.accountId || 'openzoo',
|
|
@@ -494,6 +558,126 @@ function rewrittenBox() {
|
|
|
494
558
|
});
|
|
495
559
|
}
|
|
496
560
|
|
|
561
|
+
function replyEnsureBox(req, res, payload, full = '') {
|
|
562
|
+
const ct = String(req.headers['content-type'] || '');
|
|
563
|
+
if (/WatchSandBoxMigration/.test(full) || ct.includes('connect+proto')) {
|
|
564
|
+
res.writeHead(200, {
|
|
565
|
+
'content-type': 'application/connect+proto',
|
|
566
|
+
'grpc-status': '0',
|
|
567
|
+
...CORS,
|
|
568
|
+
});
|
|
569
|
+
const end = Buffer.from('{}');
|
|
570
|
+
const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
|
|
571
|
+
res.end(Buffer.concat([envelope(payload), h, end]));
|
|
572
|
+
} else if (ct.includes('grpc-web')) {
|
|
573
|
+
res.writeHead(200, {
|
|
574
|
+
'content-type': ct.includes('text') ? 'application/grpc-web-text+proto' : 'application/grpc-web+proto',
|
|
575
|
+
'grpc-status': '0', ...CORS,
|
|
576
|
+
});
|
|
577
|
+
res.end(Buffer.concat([envelope(payload), grpcWebTrailer()]));
|
|
578
|
+
} else {
|
|
579
|
+
res.writeHead(200, { 'content-type': 'application/proto', ...CORS });
|
|
580
|
+
res.end(payload);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const discover = { promise: null, at: 0, ok: false };
|
|
585
|
+
let lastBoxReq = null;
|
|
586
|
+
let podStale = false;
|
|
587
|
+
let lastOauthAt = 0;
|
|
588
|
+
// Same-user relaunch may fall back to the last pod if api2 times out.
|
|
589
|
+
// A second login is detected by EnsureSandBox accountId, not by rotating Bearer.
|
|
590
|
+
let cacheFallbackOk = true;
|
|
591
|
+
|
|
592
|
+
function boxDiscoverHeaders(req) {
|
|
593
|
+
const headers = copyReqHeaders(req, 'api2.cursor.sh');
|
|
594
|
+
headers['content-type'] = 'application/proto';
|
|
595
|
+
headers.accept = 'application/proto';
|
|
596
|
+
delete headers['connect-protocol-version'];
|
|
597
|
+
delete headers['connect-timeout-ms'];
|
|
598
|
+
delete headers['grpc-timeout'];
|
|
599
|
+
delete headers.te;
|
|
600
|
+
return headers;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function fetchEnsureSandBox(req, body, log) {
|
|
604
|
+
const upstream = 'api2.cursor.sh';
|
|
605
|
+
const headers = boxDiscoverHeaders(req);
|
|
606
|
+
const cap = await upstreamUnary({
|
|
607
|
+
host: upstream,
|
|
608
|
+
path: '/aiserver.v1.GrokBotService/EnsureSandBox',
|
|
609
|
+
method: 'POST',
|
|
610
|
+
headers,
|
|
611
|
+
body: body && body.length ? body : Buffer.alloc(0),
|
|
612
|
+
timeoutMs: 60000,
|
|
613
|
+
});
|
|
614
|
+
const raw = inflateBody(cap.buf, cap.respHeaders);
|
|
615
|
+
const proto = unwrapConnect(raw);
|
|
616
|
+
const fields = decodeProtoFields(proto);
|
|
617
|
+
if (!fields?.[6]) {
|
|
618
|
+
throw new Error(`EnsureSandBox empty (${cap.status}, ${cap.buf.length}b)`);
|
|
619
|
+
}
|
|
620
|
+
rememberPod(fields, log);
|
|
621
|
+
try {
|
|
622
|
+
const remote = await podJson('/api/listAgents', {}, log);
|
|
623
|
+
if (Array.isArray(remote) && remote.length) {
|
|
624
|
+
const merged = mergeAgentLists(remote);
|
|
625
|
+
saveAgents(merged);
|
|
626
|
+
const active = focusedAgentId || merged[0]?.id;
|
|
627
|
+
ssePush('agents', { agents: merged.slice(0, 80).map(stampActivity), activeAgentId: active });
|
|
628
|
+
log(`cursor-backend: discovered roster n=${merged.length} account=${realPod.accountId}`);
|
|
629
|
+
}
|
|
630
|
+
} catch (e) {
|
|
631
|
+
log(`cursor-backend: discover roster ${e.message}`);
|
|
632
|
+
}
|
|
633
|
+
return realPod;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function ensureDiscover(req, body, log) {
|
|
637
|
+
lastBoxReq = { headers: { ...req.headers }, body: Buffer.from(body || []) };
|
|
638
|
+
if (discover.promise) return discover.promise;
|
|
639
|
+
const oauthFresh = lastOauthAt > discover.at;
|
|
640
|
+
if (discover.ok && realPod?.agent && !podStale && !oauthFresh) {
|
|
641
|
+
return Promise.resolve(realPod);
|
|
642
|
+
}
|
|
643
|
+
discover.at = Date.now();
|
|
644
|
+
const fakeReq = { headers: lastBoxReq.headers };
|
|
645
|
+
discover.promise = fetchEnsureSandBox(fakeReq, Buffer.alloc(0), log)
|
|
646
|
+
.then((p) => {
|
|
647
|
+
discover.ok = !!p?.agent;
|
|
648
|
+
discover.promise = null;
|
|
649
|
+
return p;
|
|
650
|
+
})
|
|
651
|
+
.catch((e) => {
|
|
652
|
+
discover.ok = false;
|
|
653
|
+
discover.promise = null;
|
|
654
|
+
log(`cursor-backend: EnsureSandBox discover failed (${e.message})`);
|
|
655
|
+
return null;
|
|
656
|
+
});
|
|
657
|
+
return discover.promise;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function waitForAccountPod(log, ms = 45000) {
|
|
661
|
+
if (realPod?.agent && !podStale && discover.ok) return realPod;
|
|
662
|
+
if (!discover.promise && lastBoxReq) {
|
|
663
|
+
ensureDiscover({ headers: lastBoxReq.headers }, lastBoxReq.body, log);
|
|
664
|
+
}
|
|
665
|
+
if (!discover.promise) return realPod && !podStale ? realPod : null;
|
|
666
|
+
try {
|
|
667
|
+
const got = await Promise.race([
|
|
668
|
+
discover.promise,
|
|
669
|
+
new Promise((resolve) => setTimeout(() => resolve('timeout'), ms)),
|
|
670
|
+
]);
|
|
671
|
+
if (got === 'timeout') {
|
|
672
|
+
log('cursor-backend: listAgents waited on discover — still going');
|
|
673
|
+
return realPod && !podStale && cacheFallbackOk ? realPod : null;
|
|
674
|
+
}
|
|
675
|
+
return got && !podStale ? got : (realPod && !podStale && cacheFallbackOk ? realPod : null);
|
|
676
|
+
} catch {
|
|
677
|
+
return realPod && !podStale && cacheFallbackOk ? realPod : null;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
497
681
|
async function sniffEnsureSandBox(req, res, body, host, full, log) {
|
|
498
682
|
const upstream = cursorUpstream(host);
|
|
499
683
|
const headers = copyReqHeaders(req, upstream);
|
|
@@ -539,7 +723,7 @@ async function sniffEnsureSandBox(req, res, body, host, full, log) {
|
|
|
539
723
|
}
|
|
540
724
|
|
|
541
725
|
async function proxyPodHttp(req, res, full, body, log) {
|
|
542
|
-
if (!realPod?.agent) return false;
|
|
726
|
+
if (podStale || !realPod?.agent) return false;
|
|
543
727
|
const agent = new URL(realPod.agent);
|
|
544
728
|
const headers = copyReqHeaders(req, agent.host);
|
|
545
729
|
// Incoming Authorization is Grok Bot oauth to api2. 1340 wants EnsureSandBox
|
|
@@ -593,6 +777,22 @@ async function proxyPodHttp(req, res, full, body, log) {
|
|
|
593
777
|
body,
|
|
594
778
|
timeoutMs: path0 === '/health' ? 8000 : 120000,
|
|
595
779
|
});
|
|
780
|
+
if (cap.status === 417 || cap.status === 404) {
|
|
781
|
+
podStale = true;
|
|
782
|
+
log(`cursor-backend: 1340 ${cap.status} ${path0} — pod stale, local`);
|
|
783
|
+
if (path0 === '/api/listAgents') {
|
|
784
|
+
const cached = rosterForAccount({
|
|
785
|
+
liveAccountId: activeAccountId,
|
|
786
|
+
cachedAccountId: activeAccountId,
|
|
787
|
+
cached: loadAgents(),
|
|
788
|
+
});
|
|
789
|
+
if (cached.length) {
|
|
790
|
+
jsonSend(res, mergeAgentLists(cached));
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
596
796
|
if (path0 === '/api/listAgents' && cap.status === 200) {
|
|
597
797
|
try {
|
|
598
798
|
const parsed = JSON.parse(String(inflateBody(cap.buf, cap.respHeaders)));
|
|
@@ -600,16 +800,20 @@ async function proxyPodHttp(req, res, full, body, log) {
|
|
|
600
800
|
const merged = mergeAgentLists(parsed);
|
|
601
801
|
saveAgents(merged);
|
|
602
802
|
jsonSend(res, merged);
|
|
603
|
-
log(`cursor-backend: listAgents 200 merged n=${merged.length}`);
|
|
803
|
+
log(`cursor-backend: listAgents 200 merged n=${merged.length} account=${activeAccountId || '?'}`);
|
|
604
804
|
return true;
|
|
605
805
|
}
|
|
606
806
|
} catch { /* */ }
|
|
607
807
|
}
|
|
608
|
-
if (cap.status
|
|
609
|
-
const cached =
|
|
808
|
+
if (cap.status !== 200 && path0 === '/api/listAgents') {
|
|
809
|
+
const cached = rosterForAccount({
|
|
810
|
+
liveAccountId: activeAccountId,
|
|
811
|
+
cachedAccountId: activeAccountId,
|
|
812
|
+
cached: loadAgents(),
|
|
813
|
+
});
|
|
610
814
|
if (cached.length) {
|
|
611
|
-
jsonSend(res, cached);
|
|
612
|
-
log(`cursor-backend: listAgents
|
|
815
|
+
jsonSend(res, mergeAgentLists(cached));
|
|
816
|
+
log(`cursor-backend: listAgents ${cap.status} — cached ${cached.length} named agents account=${activeAccountId}`);
|
|
613
817
|
return true;
|
|
614
818
|
}
|
|
615
819
|
}
|
|
@@ -1169,7 +1373,13 @@ async function writeLocalBytes(abs, bytes, log) {
|
|
|
1169
1373
|
return `wrote ${abs} (${buf.length} bytes)`;
|
|
1170
1374
|
}
|
|
1171
1375
|
async function execLocal(command, cwd, log) {
|
|
1172
|
-
|
|
1376
|
+
let dir = cwd ? expandUserPath(cwd) : os.homedir();
|
|
1377
|
+
try {
|
|
1378
|
+
if (!fs.statSync(dir).isDirectory()) dir = os.homedir();
|
|
1379
|
+
} catch {
|
|
1380
|
+
// Node reports spawn /bin/zsh ENOENT when cwd is missing — not a missing shell.
|
|
1381
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch { dir = os.homedir(); }
|
|
1382
|
+
}
|
|
1173
1383
|
if (localExecSse.size > 0) {
|
|
1174
1384
|
log(`cursor-backend: local-exec exec ${JSON.stringify(command).slice(0, 80)}`);
|
|
1175
1385
|
const got = await localExecAsk({
|
|
@@ -1180,6 +1390,7 @@ async function execLocal(command, cwd, log) {
|
|
|
1180
1390
|
const err = got.stderr ? `\nstderr:\n${got.stderr}` : '';
|
|
1181
1391
|
return `${out}${err}`.trim() || `(exit ${got.exitCode ?? 0})`;
|
|
1182
1392
|
}
|
|
1393
|
+
log(`cursor-backend: exec cwd=${dir} ${JSON.stringify(String(command).slice(0, 80))}`);
|
|
1183
1394
|
const { stdout, stderr } = await execFileAsync('/bin/zsh', ['-lc', command], {
|
|
1184
1395
|
cwd: dir,
|
|
1185
1396
|
timeout: 30000,
|
|
@@ -1351,6 +1562,22 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1351
1562
|
r = await post();
|
|
1352
1563
|
}
|
|
1353
1564
|
const data = await r.json();
|
|
1565
|
+
if (r.status === 402) {
|
|
1566
|
+
const raw = zooTextFromMessage(data?.choices?.[0]?.message, data)
|
|
1567
|
+
|| data?.error?.message
|
|
1568
|
+
|| 'openzoo wallet underfunded.';
|
|
1569
|
+
try {
|
|
1570
|
+
const { withOnrampLink } = await import('./stripeOnramp.js');
|
|
1571
|
+
const { loadOrCreateWallet } = await import('./wallet.js');
|
|
1572
|
+
const w = loadOrCreateWallet();
|
|
1573
|
+
const usd = Number(String(raw).match(/≈\$([0-9.]+)/)?.[1]);
|
|
1574
|
+
data.error = data.error || {};
|
|
1575
|
+
data.error.message = await withOnrampLink(raw, {
|
|
1576
|
+
solana: w.keypair.publicKey.toBase58(),
|
|
1577
|
+
usd,
|
|
1578
|
+
});
|
|
1579
|
+
} catch { /* keep proxy copy */ }
|
|
1580
|
+
}
|
|
1354
1581
|
return { r, data };
|
|
1355
1582
|
};
|
|
1356
1583
|
for (let step = 0; step < 8; step++) {
|
|
@@ -1465,6 +1692,11 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1465
1692
|
}
|
|
1466
1693
|
if (!path0.startsWith('/api/')) return false;
|
|
1467
1694
|
const name = path0.slice('/api/'.length);
|
|
1695
|
+
if (name === 'getHostStatus') {
|
|
1696
|
+
jsonSend(res, { status: 'ready', ready: true, state: 'ready', hostStatus: 'ready' });
|
|
1697
|
+
log('cursor-backend: -> getHostStatus ready');
|
|
1698
|
+
return true;
|
|
1699
|
+
}
|
|
1468
1700
|
// Roster/settings come from the REAL 1340 gateway (names, avatars, trays).
|
|
1469
1701
|
// Chat stays local so inference is zoo. Discovered on EnsureSandBox rewrite.
|
|
1470
1702
|
const roster = new Set([
|
|
@@ -1472,7 +1704,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1472
1704
|
'setHostSettings', 'getAgentChannels', 'getAgentWorkflows', 'skillsCatalog',
|
|
1473
1705
|
'getSubagents', 'getAsyncTasks', 'getForeverBoxStatus', 'getSharingState',
|
|
1474
1706
|
'getBotTemplateExportPolicy', 'getTeachRecordingStatus', 'isGlobalSearchEnabled',
|
|
1475
|
-
'isEgressTunnelAvailable', 'listBoxMcpServers',
|
|
1707
|
+
'isEgressTunnelAvailable', 'listBoxMcpServers',
|
|
1476
1708
|
'setWindowFocused', 'getAgentAutomations',
|
|
1477
1709
|
'createGroup', 'setGroupMembers',
|
|
1478
1710
|
'interruptAgentRun', 'requestDiskSaverAudit', 'broadcastToAgents',
|
|
@@ -1528,15 +1760,20 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1528
1760
|
log(`cursor-backend: deleteAgents n=${ids.size}`);
|
|
1529
1761
|
return true;
|
|
1530
1762
|
}
|
|
1531
|
-
if (name === 'listAgents') {
|
|
1532
|
-
if (!sniffOn()
|
|
1533
|
-
const
|
|
1534
|
-
if (
|
|
1763
|
+
if (name === 'listAgents' || name === 'getTrays' || name === 'countAgents' || name === 'searchAgents') {
|
|
1764
|
+
if (!sniffOn()) {
|
|
1765
|
+
const pod = await waitForAccountPod(log);
|
|
1766
|
+
if (pod?.agent) {
|
|
1767
|
+
const proxied = await proxyPodHttp(req, res, full, body, log);
|
|
1768
|
+
if (proxied) return true;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (name === 'listAgents') {
|
|
1772
|
+
const list = mergeAgentLists([]);
|
|
1773
|
+
jsonSend(res, list);
|
|
1774
|
+
log(`cursor-backend: listAgents local n=${list.length} account=${activeAccountId || 'none'}`);
|
|
1775
|
+
return true;
|
|
1535
1776
|
}
|
|
1536
|
-
const list = mergeAgentLists([]);
|
|
1537
|
-
jsonSend(res, list);
|
|
1538
|
-
log(`cursor-backend: listAgents local n=${list.length}`);
|
|
1539
|
-
return true;
|
|
1540
1777
|
}
|
|
1541
1778
|
|
|
1542
1779
|
if (!sniffOn() && realPod?.agent && roster.has(name)) {
|
|
@@ -1595,7 +1832,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
1595
1832
|
const line = fanoutLine(agentId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
|
|
1596
1833
|
ssePush('transcript', { ...gatewayEntry(line), agentId });
|
|
1597
1834
|
bumpAgent(agentId, { preview: text, notify: true });
|
|
1598
|
-
log(`cursor-backend: sendPrompt done agent=${agentId} seq=${line.seq} text=${JSON.stringify(text.slice(0,
|
|
1835
|
+
log(`cursor-backend: sendPrompt done agent=${agentId} seq=${line.seq} text=${JSON.stringify(text.slice(0, 220))}`);
|
|
1599
1836
|
});
|
|
1600
1837
|
return true;
|
|
1601
1838
|
}
|
|
@@ -1855,6 +2092,7 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
1855
2092
|
if (oauth || sandBox || grokCred || needRealPod) {
|
|
1856
2093
|
const upstream = /cursor\.sh$/.test(host) ? host : 'api2.cursor.sh';
|
|
1857
2094
|
await passthroughToRealAnthropic(req, res, body, upstream, full, log);
|
|
2095
|
+
if (oauth) lastOauthAt = Date.now();
|
|
1858
2096
|
return;
|
|
1859
2097
|
}
|
|
1860
2098
|
if (await handleLocalExecHttp(req, res, path0, body, log)) return;
|
|
@@ -1867,50 +2105,27 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
1867
2105
|
// with OUR box so Grok Bot's UI wires to our sandbox; everything else
|
|
1868
2106
|
// still passes through so the app loads normally.
|
|
1869
2107
|
if (/GrokBotService\/(EnsureSandBox|WatchSandBoxMigration)/.test(full) && process.env.OZ_HIJACK_POD) {
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2108
|
+
// Watch is a connect STREAM. Never await api2 on it — that is the
|
|
2109
|
+
// splash hang. Reply immediately, discover THIS caller's 1340 in
|
|
2110
|
+
// the background so listAgents can wait for their tray, not the
|
|
2111
|
+
// machine-global cache from the last login on this Mac.
|
|
2112
|
+
process.env.OZ_SNIFF_SELF = process.env.OZ_SNIFF_SELF || 'https://127.0.0.1:8443';
|
|
2113
|
+
const confirmed = discover.ok && realPod?.agent && !podStale;
|
|
2114
|
+
if (/WatchSandBoxMigration/.test(full)) {
|
|
2115
|
+
replyEnsureBox(req, res, rewrittenBox({ confirmed }), full);
|
|
2116
|
+
ensureDiscover(req, Buffer.alloc(0), log);
|
|
2117
|
+
log(`cursor-backend: -> WatchSandBoxMigration ready (${confirmed ? `account ${realPod.accountId}` : 'env box + discover'})`);
|
|
1874
2118
|
return;
|
|
1875
|
-
} catch (e) {
|
|
1876
|
-
if (realPod?.agent) {
|
|
1877
|
-
log(`cursor-backend: EnsureSandBox discover failed (${e.message}) — cached 1340 roster`);
|
|
1878
|
-
const payload = rewrittenBox();
|
|
1879
|
-
const reqCt = String(req.headers['content-type'] || '');
|
|
1880
|
-
if (/WatchSandBoxMigration/.test(full) || reqCt.includes('connect+proto')) {
|
|
1881
|
-
res.writeHead(200, {
|
|
1882
|
-
'content-type': 'application/connect+proto',
|
|
1883
|
-
'grpc-status': '0',
|
|
1884
|
-
...CORS,
|
|
1885
|
-
});
|
|
1886
|
-
const end = Buffer.from('{}');
|
|
1887
|
-
const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
|
|
1888
|
-
res.end(Buffer.concat([envelope(payload), h, end]));
|
|
1889
|
-
} else {
|
|
1890
|
-
res.writeHead(200, { 'content-type': 'application/proto', ...CORS });
|
|
1891
|
-
res.end(payload);
|
|
1892
|
-
}
|
|
1893
|
-
return;
|
|
1894
|
-
}
|
|
1895
|
-
log(`cursor-backend: EnsureSandBox discover failed (${e.message}) — env box`);
|
|
1896
2119
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
res.writeHead(200, {
|
|
1902
|
-
'content-type': 'application/connect+proto',
|
|
1903
|
-
'grpc-status': '0',
|
|
1904
|
-
...CORS,
|
|
1905
|
-
});
|
|
1906
|
-
const end = Buffer.from('{}');
|
|
1907
|
-
const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
|
|
1908
|
-
res.end(Buffer.concat([envelope(payload), h, end]));
|
|
1909
|
-
log('cursor-backend: -> WatchSandBoxMigration ready (hijack)');
|
|
2120
|
+
try {
|
|
2121
|
+
const pod = await ensureDiscover(req, body, log);
|
|
2122
|
+
replyEnsureBox(req, res, rewrittenBox({ confirmed: !!pod?.agent }), full);
|
|
2123
|
+
log(`cursor-backend: -> HIJACKED EnsureSandBox -> our box (roster from real 1340 account=${pod?.accountId || '?'})`);
|
|
1910
2124
|
return;
|
|
2125
|
+
} catch (e) {
|
|
2126
|
+
log(`cursor-backend: EnsureSandBox discover failed (${e.message}) — ${realPod?.agent && cacheFallbackOk ? 'cached 1340' : 'env box'}`);
|
|
1911
2127
|
}
|
|
1912
|
-
|
|
1913
|
-
log(`cursor-backend: -> HIJACKED EnsureSandBox -> our box`);
|
|
2128
|
+
replyEnsureBox(req, res, rewrittenBox({ confirmed: !!(realPod?.agent && cacheFallbackOk) }), full);
|
|
1914
2129
|
return;
|
|
1915
2130
|
}
|
|
1916
2131
|
// CAPTURE the chat inference request so its schema can be decoded from
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Bot tray/roster is per Cursor account, not per Mac.
|
|
3
|
+
*
|
|
4
|
+
* A second login on the same hijack was served ~/.openzoo/grokbot-agents.json
|
|
5
|
+
* (or an empty stub) because pod + roster were machine-global. Historical
|
|
6
|
+
* chats live on that account's cursorvm 1340 — never another account's cache.
|
|
7
|
+
*/
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
export function accountSlug(accountId) {
|
|
11
|
+
const s = String(accountId || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
|
|
12
|
+
return s || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function accountDir(home, accountId) {
|
|
16
|
+
const slug = accountSlug(accountId);
|
|
17
|
+
if (!slug) return null;
|
|
18
|
+
return path.join(home, '.openzoo', 'grokbot', slug);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function accountPodPath(home, accountId) {
|
|
22
|
+
const dir = accountDir(home, accountId);
|
|
23
|
+
return dir ? path.join(dir, 'pod.json') : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function accountAgentsPath(home, accountId) {
|
|
27
|
+
const dir = accountDir(home, accountId);
|
|
28
|
+
return dir ? path.join(dir, 'agents.json') : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Only serve a cached tray when it belongs to the live EnsureSandBox account. */
|
|
32
|
+
export function rosterForAccount({ liveAccountId, cachedAccountId, cached }) {
|
|
33
|
+
if (!liveAccountId || !cachedAccountId) return [];
|
|
34
|
+
if (liveAccountId !== cachedAccountId) return [];
|
|
35
|
+
return Array.isArray(cached) ? cached : [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function callerKeyFromAuth(authorization) {
|
|
39
|
+
const a = String(authorization || '').trim();
|
|
40
|
+
if (!a) return '';
|
|
41
|
+
// Not a secret store — a session-scoped equality key so a second oauth
|
|
42
|
+
// does not inherit the previous account's 1340 / tray cache.
|
|
43
|
+
let h = 2166136261;
|
|
44
|
+
for (let i = 0; i < a.length; i++) {
|
|
45
|
+
h ^= a.charCodeAt(i);
|
|
46
|
+
h = Math.imul(h, 16777619);
|
|
47
|
+
}
|
|
48
|
+
return (h >>> 0).toString(16);
|
|
49
|
+
}
|
package/lib/pay.js
CHANGED
|
@@ -9,6 +9,7 @@ import { buildEvmPayment, evmTokenBalance } from './evm.js';
|
|
|
9
9
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
10
10
|
import { withNamespace } from './namespace.js';
|
|
11
11
|
import { fetchHeaders } from './fetch.js';
|
|
12
|
+
import { withOnrampLink } from './stripeOnramp.js';
|
|
12
13
|
|
|
13
14
|
/** Drop any inherited bearer — see the call site: x402 is the only pay lane. */
|
|
14
15
|
function stripAuthorization(headers = {}) {
|
|
@@ -222,7 +223,14 @@ export class PayClient {
|
|
|
222
223
|
// to convert and no pool to walk: the wallet either holds the asset or it
|
|
223
224
|
// does not. This branch used to run resolvePool + poolState + a wrap
|
|
224
225
|
// (~4.5s) to mint a Token-2022 twin the gateway no longer accepts.
|
|
225
|
-
if (bal.raw < need)
|
|
226
|
+
if (bal.raw < need) {
|
|
227
|
+
const usd = Number(accept?.extra?.billedUsd);
|
|
228
|
+
const line = await withOnrampLink(
|
|
229
|
+
`Send USDC (or TOKEN/LEOS) on Solana to ${this.address}.`,
|
|
230
|
+
{ solana: this.address, usd },
|
|
231
|
+
);
|
|
232
|
+
throw new UnderfundedError(accept, bal.ui, this.address, { line });
|
|
233
|
+
}
|
|
226
234
|
const built = await buildPaymentOnline(this.connection, this.keypair, accept);
|
|
227
235
|
return { ...built, header: encodeEnvelope(paymentEnvelope(challenge, accept, built.payload)) };
|
|
228
236
|
}
|
|
@@ -244,7 +252,10 @@ export class PayClient {
|
|
|
244
252
|
const sym = accept?.extra?.symbol || 'the quoted asset';
|
|
245
253
|
const where = rail === 'robinhood' ? 'Robinhood Chain' : rail === 'base' ? 'Base' : accept.network;
|
|
246
254
|
throw new UnderfundedError(accept, null, owner, {
|
|
247
|
-
line:
|
|
255
|
+
line: await withOnrampLink(
|
|
256
|
+
`Send a few cents of ${sym} on ${where} to ${owner}.`,
|
|
257
|
+
{ solana: this.address, usd: Number(accept?.extra?.billedUsd) },
|
|
258
|
+
),
|
|
248
259
|
});
|
|
249
260
|
}
|
|
250
261
|
}
|
|
@@ -334,8 +345,11 @@ export class PayClient {
|
|
|
334
345
|
if (!fundErrs.length && tooHigh) throw tooHigh;
|
|
335
346
|
if (fundErrs.length === 1) throw fundErrs[0].err;
|
|
336
347
|
throw new UnderfundedError(candidates[0], null, this.address, {
|
|
337
|
-
line:
|
|
338
|
-
|
|
348
|
+
line: await withOnrampLink(
|
|
349
|
+
`No offered payment row is affordable from this wallet (tried ${fundErrs.length}):\n`
|
|
350
|
+
+ fundErrs.map(({ sym, err }) => ` · ${sym}: ${err.message.replace(/^openzoo wallet underfunded: /, '')}`).join('\n'),
|
|
351
|
+
{ solana: this.address, usd: Number(candidates[0]?.extra?.billedUsd) },
|
|
352
|
+
),
|
|
339
353
|
});
|
|
340
354
|
}
|
|
341
355
|
onStage?.('paying');
|
package/lib/proxy.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
config, FUNDING_ASSETS, EVM_FUNDING_ASSETS, evmRpcFor, fundingLine, liveRails, railFundingHint, railFundingAddresses, unfundableRails, RAIL_FUNDING,
|
|
9
9
|
} from './config.js';
|
|
10
10
|
import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
|
|
11
|
+
import { withOnrampLink } from './stripeOnramp.js';
|
|
11
12
|
import { tokenBalance } from './x402.js';
|
|
12
13
|
import { evmTokenBalance } from './evm.js';
|
|
13
14
|
import { autoContext } from './autobind.js';
|
|
@@ -760,15 +761,20 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
760
761
|
// need: the price, what the wallet holds, and where to send funds.
|
|
761
762
|
if (response.status === 402) {
|
|
762
763
|
let quoted = '';
|
|
764
|
+
let usd;
|
|
763
765
|
try {
|
|
764
766
|
const q = await response.clone().json();
|
|
765
|
-
|
|
767
|
+
usd = Number(q?.accepts?.[0]?.extra?.billedUsd);
|
|
766
768
|
if (Number.isFinite(usd)) quoted = ` This call needs ≈$${usd.toFixed(4)}.`;
|
|
767
769
|
} catch { /* body was not the quote after all */ }
|
|
768
|
-
|
|
770
|
+
const msg = await withOnrampLink(
|
|
769
771
|
`openzoo wallet underfunded — payment did not settle.${quoted} `
|
|
770
772
|
+ `Fund it and retry: send USDC (or TOKEN/LEOS for half price) to ${client.address} on Solana, `
|
|
771
|
-
+ `or USDC to ${client.evmAddress} on Base. Check with: openzoo balance
|
|
773
|
+
+ `or USDC to ${client.evmAddress} on Base. Check with: openzoo balance`,
|
|
774
|
+
{ solana: client.address, usd },
|
|
775
|
+
);
|
|
776
|
+
log(msg.includes('crypto.link.com') ? 'onramp: attached crypto.link.com' : 'onramp: no hosted URL');
|
|
777
|
+
jsonErr(res, 402, msg);
|
|
772
778
|
return;
|
|
773
779
|
}
|
|
774
780
|
await relay(res, response, meterStreamed);
|
|
@@ -777,8 +783,11 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
777
783
|
log(err.message);
|
|
778
784
|
jsonErr(res, 402, err.message, { quote: err.quote });
|
|
779
785
|
} else if (err instanceof UnderfundedError) {
|
|
780
|
-
|
|
781
|
-
|
|
786
|
+
const msg = await withOnrampLink(err.message, {
|
|
787
|
+
solana: client.address, usd: Number(err.accept?.extra?.billedUsd),
|
|
788
|
+
});
|
|
789
|
+
log(msg);
|
|
790
|
+
jsonErr(res, 402, msg);
|
|
782
791
|
} else {
|
|
783
792
|
// "fetch failed" alone is undiagnosable — undici hides the real
|
|
784
793
|
// network error in `cause`. Surface it or every transport hiccup looks
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe-hosted fiat→USDC onramp (crypto.link.com) for the x402 402 path.
|
|
3
|
+
*
|
|
4
|
+
* Secret is NEVER in the repo: STRIPE_SECRET_KEY, else STRIPE_SECRET_FILE,
|
|
5
|
+
* else ~/stripey.key. Onramp sessions lock the destination to the local
|
|
6
|
+
* Solana burner so a card checkout lands USDC where PayClient spends.
|
|
7
|
+
*
|
|
8
|
+
* Solana-only: Stripe 400s `wallet_addresses[base]` (parameter_unknown),
|
|
9
|
+
* which silently dropped the hosted URL from live Grok 402 replies.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
const STRIPE_VERSION = '2026-06-24.dahlia';
|
|
16
|
+
const TTL_MS = 10 * 60 * 1000;
|
|
17
|
+
const cache = new Map(); // key -> { url, at }
|
|
18
|
+
|
|
19
|
+
function stripeSecret() {
|
|
20
|
+
const env = String(process.env.STRIPE_SECRET_KEY || '').trim();
|
|
21
|
+
if (env) return env;
|
|
22
|
+
const file = process.env.STRIPE_SECRET_FILE || path.join(os.homedir(), 'stripey.key');
|
|
23
|
+
return fs.readFileSync(file, 'utf8').trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function dollars(usd) {
|
|
27
|
+
const n = Number(usd);
|
|
28
|
+
if (!Number.isFinite(n) || n <= 0) return 10;
|
|
29
|
+
return Math.max(5, Math.ceil(n));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Test seam. */
|
|
33
|
+
export function resetOnrampCache() { cache.clear(); }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mint (or reuse) a Stripe-hosted onramp URL that sends USDC to `solana`.
|
|
37
|
+
* Returns null if the key is missing or Stripe refuses — caller keeps the
|
|
38
|
+
* existing send-to-address copy. `evm` is ignored: Stripe does not accept
|
|
39
|
+
* `wallet_addresses[base]` on this API.
|
|
40
|
+
*/
|
|
41
|
+
export async function stripeUsdcOnrampLink({ solana, usd } = {}) {
|
|
42
|
+
const addr = String(solana || '').trim();
|
|
43
|
+
if (!addr) return null;
|
|
44
|
+
const amt = dollars(usd);
|
|
45
|
+
const key = `${addr}|${amt}`;
|
|
46
|
+
const hit = cache.get(key);
|
|
47
|
+
if (hit && Date.now() - hit.at < TTL_MS && hit.url) return hit.url;
|
|
48
|
+
let secret;
|
|
49
|
+
try { secret = stripeSecret(); } catch { return null; }
|
|
50
|
+
if (!secret) return null;
|
|
51
|
+
const body = new URLSearchParams();
|
|
52
|
+
body.set('destination_currency', 'usdc');
|
|
53
|
+
body.set('destination_network', 'solana');
|
|
54
|
+
body.append('destination_currencies[]', 'usdc');
|
|
55
|
+
body.append('destination_networks[]', 'solana');
|
|
56
|
+
body.set('source_currency', 'usd');
|
|
57
|
+
body.set('source_amount', String(amt));
|
|
58
|
+
body.set('lock_wallet_address', 'true');
|
|
59
|
+
body.set('wallet_addresses[solana]', addr);
|
|
60
|
+
try {
|
|
61
|
+
const r = await fetch('https://api.stripe.com/v1/crypto/onramp_sessions', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
authorization: `Bearer ${secret}`,
|
|
65
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
66
|
+
'Stripe-Version': STRIPE_VERSION,
|
|
67
|
+
},
|
|
68
|
+
body,
|
|
69
|
+
signal: AbortSignal.timeout(15000),
|
|
70
|
+
});
|
|
71
|
+
const data = await r.json();
|
|
72
|
+
const url = data?.redirect_url;
|
|
73
|
+
if (!r.ok || !url) {
|
|
74
|
+
const err = data?.error;
|
|
75
|
+
console.error(`openzoo onramp: stripe ${r.status} ${err?.code || err?.type || 'no-url'} ${(err?.message || '').slice(0, 160)}`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
cache.set(key, { url, at: Date.now() });
|
|
79
|
+
return url;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error(`openzoo onramp: ${e.message}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function withOnrampLink(text, dest) {
|
|
87
|
+
const url = await stripeUsdcOnrampLink(dest);
|
|
88
|
+
const body = String(text || '').trim();
|
|
89
|
+
if (!url) return body;
|
|
90
|
+
if (body.includes(url) || /crypto\.link\.com/i.test(body)) return body;
|
|
91
|
+
return `Buy USDC: ${url}\n\n${body}`;
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.27",
|
|
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",
|