openzoo 0.50.21 → 0.50.23

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.
@@ -289,7 +289,34 @@ const RESP_DROP = new Set([
289
289
  ]);
290
290
 
291
291
  const SNIFF_FILE = path.join(os.homedir(), '.openzoo', 'grokbot-sniff.jsonl');
292
- let realPod = null; // { agent, vnc, token, p1340, p6081, region, accountId, podId }
292
+ const POD_FILE = path.join(os.homedir(), '.openzoo', 'grokbot-pod.json');
293
+ function loadPod() {
294
+ try { return JSON.parse(fs.readFileSync(POD_FILE, 'utf8')); } catch { return null; }
295
+ }
296
+ function savePod(p) {
297
+ if (!p?.agent) return;
298
+ try {
299
+ fs.mkdirSync(path.dirname(POD_FILE), { recursive: true });
300
+ fs.writeFileSync(POD_FILE, JSON.stringify(p));
301
+ } catch { /* */ }
302
+ }
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
+ function loadAgents() {
306
+ try {
307
+ const a = JSON.parse(fs.readFileSync(AGENTS_FILE, 'utf8'));
308
+ return Array.isArray(a) ? a : null;
309
+ } catch { return null; }
310
+ }
311
+ function saveAgents(a) {
312
+ if (!Array.isArray(a) || !a.length) return;
313
+ try { fs.writeFileSync(AGENTS_FILE, JSON.stringify(a)); } catch { /* */ }
314
+ }
315
+ function cachedAgentList() {
316
+ const a = loadAgents();
317
+ if (a?.length) return a;
318
+ return null;
319
+ }
293
320
 
294
321
  function sniffOn() { return process.env.OPENZOO_SNIFF === '1'; }
295
322
  function sniffSelf() { return process.env.OZ_SNIFF_SELF || 'https://127.0.0.1:8443'; }
@@ -446,6 +473,7 @@ function rememberPod(fields, log) {
446
473
  podId: String(fields[3] || ''),
447
474
  };
448
475
  sniffDump({ kind: 'pod', fields, realPod });
476
+ savePod(realPod);
449
477
  log(`cursor-backend: SNIFF real pod ${realPod.agent}`);
450
478
  return realPod;
451
479
  }
@@ -470,7 +498,7 @@ async function sniffEnsureSandBox(req, res, body, host, full, log) {
470
498
  const upstream = cursorUpstream(host);
471
499
  const headers = copyReqHeaders(req, upstream);
472
500
  const cap = await upstreamUnary({
473
- host: upstream, path: full, method: req.method, headers, body, timeoutMs: 30000,
501
+ host: upstream, path: full, method: req.method, headers, body, timeoutMs: 60000,
474
502
  });
475
503
  const raw = inflateBody(cap.buf, cap.respHeaders);
476
504
  const proto = unwrapConnect(raw);
@@ -564,6 +592,26 @@ async function proxyPodHttp(req, res, full, body, log) {
564
592
  body,
565
593
  timeoutMs: path0 === '/health' ? 8000 : 120000,
566
594
  });
595
+ if (path0 === '/api/listAgents' && cap.status === 200) {
596
+ try {
597
+ const parsed = JSON.parse(String(inflateBody(cap.buf, cap.respHeaders)));
598
+ if (Array.isArray(parsed)) {
599
+ const merged = mergeAgentLists(parsed);
600
+ saveAgents(merged);
601
+ jsonSend(res, merged);
602
+ log(`cursor-backend: listAgents 200 merged n=${merged.length}`);
603
+ return true;
604
+ }
605
+ } catch { /* */ }
606
+ }
607
+ if (cap.status === 401 && path0 === '/api/listAgents') {
608
+ const cached = mergeAgentLists([]);
609
+ if (cached.length) {
610
+ jsonSend(res, cached);
611
+ log(`cursor-backend: listAgents 401 — cached ${cached.length} named agents`);
612
+ return true;
613
+ }
614
+ }
567
615
  writeCaptured(res, cap.status, cap.respHeaders, cap.buf);
568
616
  const rec = {
569
617
  kind: 'pod-http',
@@ -723,9 +771,33 @@ function expandUserPath(p) {
723
771
  const transcripts = new Map();
724
772
  const tailedAgents = new Set();
725
773
  let lastSendEchoId = `oz-${Date.now()}`;
774
+ const TX_FILE = path.join(os.homedir(), '.openzoo', 'grokbot-transcripts.json');
775
+ function loadTranscripts() {
776
+ try {
777
+ const o = JSON.parse(fs.readFileSync(TX_FILE, 'utf8'));
778
+ for (const [id, t] of Object.entries(o || {})) {
779
+ transcripts.set(id, {
780
+ seq: Number(t.seq) || (t.entries || []).length,
781
+ entries: Array.isArray(t.entries) ? t.entries : [],
782
+ pulledRemote: false,
783
+ });
784
+ }
785
+ } catch { /* first run */ }
786
+ }
787
+ function saveTranscripts() {
788
+ try {
789
+ const o = {};
790
+ for (const [id, t] of transcripts) {
791
+ o[id] = { seq: t.seq, entries: t.entries.slice(-200) };
792
+ }
793
+ fs.mkdirSync(path.dirname(TX_FILE), { recursive: true });
794
+ fs.writeFileSync(TX_FILE, JSON.stringify(o));
795
+ } catch { /* */ }
796
+ }
797
+ loadTranscripts();
726
798
  function agentTranscript(id) {
727
799
  let t = transcripts.get(id);
728
- if (!t) { t = { seq: 0, entries: [] }; transcripts.set(id, t); }
800
+ if (!t) { t = { seq: 0, entries: [], pulledRemote: false }; transcripts.set(id, t); }
729
801
  return t;
730
802
  }
731
803
  function appendLine(agentId, role, text, extra = {}) {
@@ -761,18 +833,108 @@ function appendLine(agentId, role, text, extra = {}) {
761
833
  };
762
834
  }
763
835
  t.entries.push(e);
836
+ saveTranscripts();
764
837
  return e;
765
838
  }
766
839
  function fanoutLine(primaryId, role, text, extra = {}) {
767
- const ids = new Set([primaryId, ...tailedAgents]);
768
- let last = null;
769
- for (const id of ids) last = appendLine(id, role, text, extra);
770
- return last;
840
+ // Only the addressed agent. Writing to every tailedAgents id mixed canvases
841
+ // so a new/empty chat showed someone else's thread.
842
+ return appendLine(primaryId, role, text, extra);
843
+ }
844
+ function mintLocalAgent(parsed = {}) {
845
+ const id = String(parsed.id || randomUUID());
846
+ const name = String(parsed.name || parsed.title || 'new chat');
847
+ const agent = {
848
+ id,
849
+ name,
850
+ description: String(parsed.description || ''),
851
+ title: String(parsed.title || name),
852
+ origin: parsed.origin || 'user',
853
+ createdAt: Date.now(),
854
+ updatedAt: Date.now(),
855
+ avatarShape: parsed.avatarShape || null,
856
+ avatarColor: parsed.avatarColor || null,
857
+ path: `/local/${id}`,
858
+ };
859
+ const list = cachedAgentList() || [];
860
+ if (!list.some((a) => a.id === id)) list.unshift(agent);
861
+ else {
862
+ const i = list.findIndex((a) => a.id === id);
863
+ list[i] = { ...list[i], ...agent };
864
+ }
865
+ saveAgents(list);
866
+ agentTranscript(id);
867
+ return agent;
868
+ }
869
+ function mergeAgentLists(remote) {
870
+ const local = cachedAgentList() || [];
871
+ const seen = new Set();
872
+ const out = [];
873
+ for (const a of [...local, ...(Array.isArray(remote) ? remote : [])]) {
874
+ if (!a?.id || seen.has(a.id)) continue;
875
+ seen.add(a.id);
876
+ out.push(a);
877
+ }
878
+ return out;
771
879
  }
772
880
  function gatewayEntry(e) {
773
- const { seq, ...rest } = e;
881
+ const { seq, pulledRemote, ...rest } = e;
774
882
  return rest;
775
883
  }
884
+ async function podJson(path0, bodyObj, log) {
885
+ if (!realPod?.agent) return null;
886
+ let agent;
887
+ try { agent = new URL(realPod.agent); } catch { return null; }
888
+ const headers = {
889
+ 'content-type': 'application/json',
890
+ accept: 'application/json',
891
+ authorization: `Bearer ${realPod.accessToken || realPod.token || ''}`,
892
+ 'x-anyrun-network-token': realPod.token || '',
893
+ 'x-sand-slim-avatars': '1',
894
+ };
895
+ try {
896
+ const cap = await upstreamUnary({
897
+ host: agent.hostname,
898
+ path: path0,
899
+ method: 'POST',
900
+ headers,
901
+ body: Buffer.from(JSON.stringify(bodyObj || {})),
902
+ timeoutMs: 20000,
903
+ });
904
+ if (cap.status !== 200) {
905
+ log(`cursor-backend: podJson ${path0} ${cap.status}`);
906
+ return null;
907
+ }
908
+ const raw = inflateBody(cap.buf, cap.respHeaders);
909
+ return JSON.parse(String(raw));
910
+ } catch (e) {
911
+ log(`cursor-backend: podJson ${path0} ${e.message}`);
912
+ return null;
913
+ }
914
+ }
915
+ function ingestRemoteEntries(agentId, entries) {
916
+ if (!Array.isArray(entries) || !entries.length) return 0;
917
+ const t = agentTranscript(agentId);
918
+ const have = new Set(t.entries.map((e) => e.id || e.clientNonce || '').filter(Boolean));
919
+ const incoming = [];
920
+ for (const e of entries) {
921
+ if (!e || typeof e !== 'object') continue;
922
+ const k = e.id || e.clientNonce || '';
923
+ if (k && have.has(k)) continue;
924
+ incoming.push(e);
925
+ }
926
+ if (!incoming.length) return 0;
927
+ const local = t.entries;
928
+ t.entries = [];
929
+ t.seq = 0;
930
+ for (const e of [...incoming, ...local]) {
931
+ t.seq += 1;
932
+ const { seq: _s, ...rest } = e;
933
+ t.entries.push({ ...rest, seq: t.seq });
934
+ }
935
+ saveTranscripts();
936
+ return incoming.length;
937
+ }
776
938
 
777
939
  function promptFromSendBody(raw) {
778
940
  let obj = raw;
@@ -1223,12 +1385,56 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
1223
1385
  'getBotTemplateExportPolicy', 'getTeachRecordingStatus', 'isGlobalSearchEnabled',
1224
1386
  'isEgressTunnelAvailable', 'listBoxMcpServers', 'getHostStatus',
1225
1387
  'setWindowFocused', 'getAgentAutomations',
1226
- 'createAgent', 'createAgentFromTemplate', 'createGroup', 'setGroupMembers',
1227
- 'updateAgent', 'deleteAgents', 'duplicateAgent', 'kickstartAgent',
1388
+ 'createGroup', 'setGroupMembers',
1228
1389
  'interruptAgentRun', 'requestDiskSaverAudit', 'broadcastToAgents',
1229
1390
  'setAgentUnread', 'setAgentHiddenFromSidebar', 'setAgentNotificationsEnabled',
1230
1391
  'setAgentNotifyOnUpdates', 'setAgentAvatarBytes', 'getAgentAvatar',
1231
1392
  ]);
1393
+ // create/delete stay LOCAL — 1340 createAgent 401s on a stale cached token
1394
+ // and the UI then never grows a sidebar row or clears the canvas.
1395
+ if (name === 'createAgent' || name === 'createAgentFromTemplate' || name === 'duplicateAgent') {
1396
+ let parsed = {};
1397
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1398
+ if (name === 'duplicateAgent' && parsed.id) {
1399
+ const src = (cachedAgentList() || []).find((a) => a.id === parsed.id) || {};
1400
+ parsed = { ...src, id: undefined, name: `${src.name || 'chat'} copy` };
1401
+ }
1402
+ const agent = mintLocalAgent(parsed);
1403
+ jsonSend(res, { agent, id: agent.id, ...agent });
1404
+ ssePush('agents', { action: 'created', agent });
1405
+ log(`cursor-backend: createAgent local id=${agent.id} name=${JSON.stringify(agent.name)}`);
1406
+ return true;
1407
+ }
1408
+ if (name === 'updateAgent') {
1409
+ let parsed = {};
1410
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1411
+ const agent = mintLocalAgent(parsed);
1412
+ jsonSend(res, { agent, ...agent });
1413
+ log(`cursor-backend: updateAgent local id=${agent.id}`);
1414
+ return true;
1415
+ }
1416
+ if (name === 'deleteAgents') {
1417
+ let parsed = {};
1418
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1419
+ const ids = new Set([].concat(parsed.ids || parsed.id || []).map(String));
1420
+ const next = (cachedAgentList() || []).filter((a) => !ids.has(a.id));
1421
+ saveAgents(next);
1422
+ for (const id of ids) transcripts.delete(id);
1423
+ jsonSend(res, { ok: true, deleted: [...ids] });
1424
+ log(`cursor-backend: deleteAgents n=${ids.size}`);
1425
+ return true;
1426
+ }
1427
+ if (name === 'listAgents') {
1428
+ if (!sniffOn() && realPod?.agent) {
1429
+ const proxied = await proxyPodHttp(req, res, full, body, log);
1430
+ if (proxied) return true;
1431
+ }
1432
+ const list = mergeAgentLists([]);
1433
+ jsonSend(res, list);
1434
+ log(`cursor-backend: listAgents local n=${list.length}`);
1435
+ return true;
1436
+ }
1437
+
1232
1438
  if (!sniffOn() && realPod?.agent && roster.has(name)) {
1233
1439
  return proxyPodHttp(req, res, full, body, log);
1234
1440
  }
@@ -1289,24 +1495,25 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
1289
1495
  if (name === 'getAgentTranscriptTail' || name === 'getAgentTranscriptWindow' || name === 'openAgentTail') {
1290
1496
  let parsed = {};
1291
1497
  try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1292
- let id = String(parsed.id || parsed.agentId || 'openzoo');
1498
+ const id = String(parsed.id || parsed.agentId || 'openzoo');
1293
1499
  tailedAgents.add(id);
1294
- let t = agentTranscript(id);
1295
- if (!t.entries.length) {
1296
- for (const [other, ot] of transcripts) {
1297
- if (ot.entries.length) { id = other; t = ot; break; }
1298
- }
1500
+ const t = agentTranscript(id);
1501
+ if (!t.pulledRemote && realPod?.agent) {
1502
+ const remote = await podJson('/api/getAgentTranscriptTail', {
1503
+ id, agentId: id, limit: 200, beforeSeq: parsed.beforeSeq,
1504
+ }, log);
1505
+ const n = ingestRemoteEntries(id, remote?.entries);
1506
+ t.pulledRemote = true;
1507
+ if (n) log(`cursor-backend: hydrated ${id} +${n} from 1340`);
1299
1508
  }
1509
+ const t2 = agentTranscript(id);
1300
1510
  const limit = Math.min(Number(parsed.limit) || 50, 200);
1301
1511
  const before = parsed.beforeSeq != null ? Number(parsed.beforeSeq) : Infinity;
1302
- const sliced = t.entries.filter((e) => e.seq < before).slice(-limit);
1512
+ const sliced = t2.entries.filter((e) => e.seq < before).slice(-limit);
1303
1513
  const page = { entries: sliced.map(gatewayEntry) };
1304
- if (t.entries.length > sliced.length && sliced.length) page.nextBeforeSeq = sliced[0].seq;
1305
- // Live pod returns RAW {entries, nextBeforeSeq} — no CVr envelope
1306
- // (measured 1340 getAgentTranscriptTail 2026-08-29). Wrapping {status,value}
1307
- // made transcript-page validation fail and the canvas stayed empty.
1514
+ if (t2.entries.length > sliced.length && sliced.length) page.nextBeforeSeq = sliced[0].seq;
1308
1515
  jsonSend(res, page);
1309
- log(`cursor-backend: -> transcript ${id} n=${sliced.length}/${t.seq}`);
1516
+ log(`cursor-backend: -> transcript ${id} n=${sliced.length}/${t2.seq}`);
1310
1517
  return true;
1311
1518
  }
1312
1519
  if (name === 'promptAcceptanceStatus') {
@@ -1334,7 +1541,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
1334
1541
  getHostSettings: { settings: {} },
1335
1542
  setHostSettings: { ok: true },
1336
1543
  setBoxSecrets: { ok: true },
1337
- listAgents: [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: id, status: 'ready' })),
1544
+ listAgents: cachedAgentList()
1545
+ || [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: id, status: 'ready' })),
1338
1546
  getAgentTranscriptTail: { tail: '', lines: [], dropped: false, ok: true },
1339
1547
  getTeachRecordingStatus: { recording: false },
1340
1548
  getTrays: { trays: [] },
@@ -1551,25 +1759,31 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
1551
1759
  // with OUR box so Grok Bot's UI wires to our sandbox; everything else
1552
1760
  // still passes through so the app loads normally.
1553
1761
  if (/GrokBotService\/(EnsureSandBox|WatchSandBoxMigration)/.test(full) && process.env.OZ_HIJACK_POD) {
1554
- if (/WatchSandBoxMigration/.test(full) && realPod) {
1555
- const payload = rewrittenBox();
1556
- res.writeHead(200, {
1557
- 'content-type': 'application/connect+proto',
1558
- 'grpc-status': '0',
1559
- ...CORS,
1560
- });
1561
- const end = Buffer.from('{}');
1562
- const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
1563
- res.end(Buffer.concat([envelope(payload), h, end]));
1564
- log('cursor-backend: -> WatchSandBoxMigration ready (hijack, real roster)');
1565
- return;
1566
- }
1567
1762
  try {
1568
1763
  process.env.OZ_SNIFF_SELF = process.env.OZ_SNIFF_SELF || 'https://127.0.0.1:8443';
1569
1764
  await sniffEnsureSandBox(req, res, body, host, full, log);
1570
1765
  log('cursor-backend: -> HIJACKED EnsureSandBox -> our box (roster from real 1340)');
1571
1766
  return;
1572
1767
  } catch (e) {
1768
+ if (realPod?.agent) {
1769
+ log(`cursor-backend: EnsureSandBox discover failed (${e.message}) — cached 1340 roster`);
1770
+ const payload = rewrittenBox();
1771
+ const reqCt = String(req.headers['content-type'] || '');
1772
+ if (/WatchSandBoxMigration/.test(full) || reqCt.includes('connect+proto')) {
1773
+ res.writeHead(200, {
1774
+ 'content-type': 'application/connect+proto',
1775
+ 'grpc-status': '0',
1776
+ ...CORS,
1777
+ });
1778
+ const end = Buffer.from('{}');
1779
+ const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
1780
+ res.end(Buffer.concat([envelope(payload), h, end]));
1781
+ } else {
1782
+ res.writeHead(200, { 'content-type': 'application/proto', ...CORS });
1783
+ res.end(payload);
1784
+ }
1785
+ return;
1786
+ }
1573
1787
  log(`cursor-backend: EnsureSandBox discover failed (${e.message}) — env box`);
1574
1788
  }
1575
1789
  let pod;
package/lib/grokcli.js CHANGED
@@ -177,8 +177,10 @@ export async function runBot(argv = []) {
177
177
  console.error(' EnsureSandBox HIJACKED here; StreamUnifiedChat -> :8402 (x402)');
178
178
  }
179
179
 
180
- try { execSync('osascript -e \'tell application "Grok Bot" to quit\'', { stdio: 'ignore' }); } catch { /* ok */ }
181
- await new Promise((r) => setTimeout(r, 1500));
180
+ if (!argv.includes('--no-quit') && process.env.OZ_NO_QUIT !== '1') {
181
+ try { execSync('osascript -e \'tell application "Grok Bot" to quit\'', { stdio: 'ignore' }); } catch { /* ok */ }
182
+ await new Promise((r) => setTimeout(r, 1500));
183
+ }
182
184
 
183
185
  console.error('openzoo: launching Grok Bot');
184
186
  console.error(` CURSOR_API_BASE_URL=${url}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.21",
3
+ "version": "0.50.23",
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",