openzoo 0.50.21 → 0.50.22

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',
@@ -764,10 +812,45 @@ function appendLine(agentId, role, text, extra = {}) {
764
812
  return e;
765
813
  }
766
814
  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;
815
+ // Only the addressed agent. Writing to every tailedAgents id mixed canvases
816
+ // so a new/empty chat showed someone else's thread.
817
+ return appendLine(primaryId, role, text, extra);
818
+ }
819
+ function mintLocalAgent(parsed = {}) {
820
+ const id = String(parsed.id || randomUUID());
821
+ const name = String(parsed.name || parsed.title || 'new chat');
822
+ const agent = {
823
+ id,
824
+ name,
825
+ description: String(parsed.description || ''),
826
+ title: String(parsed.title || name),
827
+ origin: parsed.origin || 'user',
828
+ createdAt: Date.now(),
829
+ updatedAt: Date.now(),
830
+ avatarShape: parsed.avatarShape || null,
831
+ avatarColor: parsed.avatarColor || null,
832
+ path: `/local/${id}`,
833
+ };
834
+ const list = cachedAgentList() || [];
835
+ if (!list.some((a) => a.id === id)) list.unshift(agent);
836
+ else {
837
+ const i = list.findIndex((a) => a.id === id);
838
+ list[i] = { ...list[i], ...agent };
839
+ }
840
+ saveAgents(list);
841
+ agentTranscript(id);
842
+ return agent;
843
+ }
844
+ function mergeAgentLists(remote) {
845
+ const local = cachedAgentList() || [];
846
+ const seen = new Set();
847
+ const out = [];
848
+ for (const a of [...local, ...(Array.isArray(remote) ? remote : [])]) {
849
+ if (!a?.id || seen.has(a.id)) continue;
850
+ seen.add(a.id);
851
+ out.push(a);
852
+ }
853
+ return out;
771
854
  }
772
855
  function gatewayEntry(e) {
773
856
  const { seq, ...rest } = e;
@@ -1223,12 +1306,56 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
1223
1306
  'getBotTemplateExportPolicy', 'getTeachRecordingStatus', 'isGlobalSearchEnabled',
1224
1307
  'isEgressTunnelAvailable', 'listBoxMcpServers', 'getHostStatus',
1225
1308
  'setWindowFocused', 'getAgentAutomations',
1226
- 'createAgent', 'createAgentFromTemplate', 'createGroup', 'setGroupMembers',
1227
- 'updateAgent', 'deleteAgents', 'duplicateAgent', 'kickstartAgent',
1309
+ 'createGroup', 'setGroupMembers',
1228
1310
  'interruptAgentRun', 'requestDiskSaverAudit', 'broadcastToAgents',
1229
1311
  'setAgentUnread', 'setAgentHiddenFromSidebar', 'setAgentNotificationsEnabled',
1230
1312
  'setAgentNotifyOnUpdates', 'setAgentAvatarBytes', 'getAgentAvatar',
1231
1313
  ]);
1314
+ // create/delete stay LOCAL — 1340 createAgent 401s on a stale cached token
1315
+ // and the UI then never grows a sidebar row or clears the canvas.
1316
+ if (name === 'createAgent' || name === 'createAgentFromTemplate' || name === 'duplicateAgent') {
1317
+ let parsed = {};
1318
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1319
+ if (name === 'duplicateAgent' && parsed.id) {
1320
+ const src = (cachedAgentList() || []).find((a) => a.id === parsed.id) || {};
1321
+ parsed = { ...src, id: undefined, name: `${src.name || 'chat'} copy` };
1322
+ }
1323
+ const agent = mintLocalAgent(parsed);
1324
+ jsonSend(res, { agent, id: agent.id, ...agent });
1325
+ ssePush('agents', { action: 'created', agent });
1326
+ log(`cursor-backend: createAgent local id=${agent.id} name=${JSON.stringify(agent.name)}`);
1327
+ return true;
1328
+ }
1329
+ if (name === 'updateAgent') {
1330
+ let parsed = {};
1331
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1332
+ const agent = mintLocalAgent(parsed);
1333
+ jsonSend(res, { agent, ...agent });
1334
+ log(`cursor-backend: updateAgent local id=${agent.id}`);
1335
+ return true;
1336
+ }
1337
+ if (name === 'deleteAgents') {
1338
+ let parsed = {};
1339
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1340
+ const ids = new Set([].concat(parsed.ids || parsed.id || []).map(String));
1341
+ const next = (cachedAgentList() || []).filter((a) => !ids.has(a.id));
1342
+ saveAgents(next);
1343
+ for (const id of ids) transcripts.delete(id);
1344
+ jsonSend(res, { ok: true, deleted: [...ids] });
1345
+ log(`cursor-backend: deleteAgents n=${ids.size}`);
1346
+ return true;
1347
+ }
1348
+ if (name === 'listAgents') {
1349
+ if (!sniffOn() && realPod?.agent) {
1350
+ const proxied = await proxyPodHttp(req, res, full, body, log);
1351
+ if (proxied) return true;
1352
+ }
1353
+ const list = mergeAgentLists([]);
1354
+ jsonSend(res, list);
1355
+ log(`cursor-backend: listAgents local n=${list.length}`);
1356
+ return true;
1357
+ }
1358
+
1232
1359
  if (!sniffOn() && realPod?.agent && roster.has(name)) {
1233
1360
  return proxyPodHttp(req, res, full, body, log);
1234
1361
  }
@@ -1289,14 +1416,9 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
1289
1416
  if (name === 'getAgentTranscriptTail' || name === 'getAgentTranscriptWindow' || name === 'openAgentTail') {
1290
1417
  let parsed = {};
1291
1418
  try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
1292
- let id = String(parsed.id || parsed.agentId || 'openzoo');
1419
+ const id = String(parsed.id || parsed.agentId || 'openzoo');
1293
1420
  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
- }
1299
- }
1421
+ const t = agentTranscript(id);
1300
1422
  const limit = Math.min(Number(parsed.limit) || 50, 200);
1301
1423
  const before = parsed.beforeSeq != null ? Number(parsed.beforeSeq) : Infinity;
1302
1424
  const sliced = t.entries.filter((e) => e.seq < before).slice(-limit);
@@ -1334,7 +1456,8 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
1334
1456
  getHostSettings: { settings: {} },
1335
1457
  setHostSettings: { ok: true },
1336
1458
  setBoxSecrets: { ok: true },
1337
- listAgents: [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: id, status: 'ready' })),
1459
+ listAgents: cachedAgentList()
1460
+ || [...new Set([...transcripts.keys(), ...tailedAgents])].map((id) => ({ id, name: id, status: 'ready' })),
1338
1461
  getAgentTranscriptTail: { tail: '', lines: [], dropped: false, ok: true },
1339
1462
  getTeachRecordingStatus: { recording: false },
1340
1463
  getTrays: { trays: [] },
@@ -1551,25 +1674,31 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
1551
1674
  // with OUR box so Grok Bot's UI wires to our sandbox; everything else
1552
1675
  // still passes through so the app loads normally.
1553
1676
  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
1677
  try {
1568
1678
  process.env.OZ_SNIFF_SELF = process.env.OZ_SNIFF_SELF || 'https://127.0.0.1:8443';
1569
1679
  await sniffEnsureSandBox(req, res, body, host, full, log);
1570
1680
  log('cursor-backend: -> HIJACKED EnsureSandBox -> our box (roster from real 1340)');
1571
1681
  return;
1572
1682
  } catch (e) {
1683
+ if (realPod?.agent) {
1684
+ log(`cursor-backend: EnsureSandBox discover failed (${e.message}) — cached 1340 roster`);
1685
+ const payload = rewrittenBox();
1686
+ const reqCt = String(req.headers['content-type'] || '');
1687
+ if (/WatchSandBoxMigration/.test(full) || reqCt.includes('connect+proto')) {
1688
+ res.writeHead(200, {
1689
+ 'content-type': 'application/connect+proto',
1690
+ 'grpc-status': '0',
1691
+ ...CORS,
1692
+ });
1693
+ const end = Buffer.from('{}');
1694
+ const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1);
1695
+ res.end(Buffer.concat([envelope(payload), h, end]));
1696
+ } else {
1697
+ res.writeHead(200, { 'content-type': 'application/proto', ...CORS });
1698
+ res.end(payload);
1699
+ }
1700
+ return;
1701
+ }
1573
1702
  log(`cursor-backend: EnsureSandBox discover failed (${e.message}) — env box`);
1574
1703
  }
1575
1704
  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.22",
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",