openzoo 0.50.49 → 0.50.51

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.
@@ -41,7 +41,7 @@ import {
41
41
  accountPodPath, accountAgentsPath, rosterForAccount, rosterForEvent,
42
42
  readHouseRoster, houseAgentsPath, shapeAgent, agentBrief, briefFromName,
43
43
  readWakeups, writeWakeups, shapeWakeup, parseWakeupEvery, wantsWakeupCron,
44
- DEFAULT_WAKEUP_PROMPT,
44
+ DEFAULT_WAKEUP_PROMPT, addDeletedIds, filterDeleted,
45
45
  } from './grokbotAccount.js';
46
46
  import { formatSpendFooter, mergeTurnProof } from './spendProof.js';
47
47
  import { prefixVisitorRichText } from './grokbotweb.js';
@@ -363,7 +363,7 @@ function useHouseRoster() {
363
363
  return Boolean(process.env.OZ_HIJACK_POD) && !sniffOn();
364
364
  }
365
365
  function loadAgents() {
366
- const house = readHouseRoster(HOME, activeAccountId) || [];
366
+ const house = filterDeleted(readHouseRoster(HOME, activeAccountId) || [], HOME);
367
367
  const shaped = house.map(shapeAgent);
368
368
  const dirty = shaped.some((a, i) => a.brief && a.brief !== String(house[i]?.brief || house[i]?.description || ''));
369
369
  if (dirty && shaped.length) {
@@ -1264,6 +1264,10 @@ async function fireAgentWakeup(agentId) {
1264
1264
  wakeupLog(`cursor-backend: wakeup skip busy agent=${id}`);
1265
1265
  return;
1266
1266
  }
1267
+ if (focusedAgentId && String(focusedAgentId) === id) {
1268
+ wakeupLog(`cursor-backend: wakeup skip focused agent=${id}`);
1269
+ return;
1270
+ }
1267
1271
  const nonce = `oz-wakeup-${id}-${rec.lastAt}`;
1268
1272
  const prompt = rec.prompt || DEFAULT_WAKEUP_PROMPT;
1269
1273
  const turn = zooTurns.begin(id, nonce);
@@ -1299,7 +1303,9 @@ export function restoreAgentWakeups(log = () => {}) {
1299
1303
  const ids = Object.keys(map);
1300
1304
  const now = Date.now();
1301
1305
  ids.forEach((id, i) => {
1302
- map[id].nextAt = now + 5000 + i * 2000;
1306
+ // Do not fire during Grok Bot boot. 10 parallel zoo turns + CDP on the
1307
+ // spinner page left the window on a white disc. First tick is +90s.
1308
+ map[id].nextAt = now + 90_000 + i * 4000;
1303
1309
  });
1304
1310
  if (ids.length) persistWakeups(map);
1305
1311
  for (const id of ids) armWakeup(map[id]);
@@ -1534,7 +1540,7 @@ function mergeAgentLists(remote) {
1534
1540
  seen.add(a.id);
1535
1541
  out.push(stampActivity(a));
1536
1542
  }
1537
- return sortAgentsByActivity(out);
1543
+ return sortAgentsByActivity(filterDeleted(out, HOME));
1538
1544
  }
1539
1545
  function gatewayEntry(e) {
1540
1546
  const { seq, pulledRemote, promptRaw, ephemeral, ...rest } = e;
@@ -2906,8 +2912,12 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
2906
2912
  try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
2907
2913
  const ids = new Set([].concat(parsed.ids || parsed.id || []).map(String));
2908
2914
  const next = (cachedAgentList() || []).filter((a) => !ids.has(a.id));
2915
+ addDeletedIds(HOME, [...ids]);
2909
2916
  saveAgents(next);
2910
- for (const id of ids) transcripts.delete(id);
2917
+ for (const id of ids) {
2918
+ transcripts.delete(id);
2919
+ try { cancelAgentWakeup(id); } catch { /* */ }
2920
+ }
2911
2921
  jsonSend(res, { ok: true, deleted: [...ids] });
2912
2922
  log(`cursor-backend: deleteAgents n=${ids.size}`);
2913
2923
  return true;
@@ -244,6 +244,31 @@ export function readWakeups(home) {
244
244
  return out;
245
245
  }
246
246
 
247
+ export function deletedAgentsPath(home) {
248
+ return path.join(home, '.openzoo', 'grokbot-deleted.json');
249
+ }
250
+
251
+ export function readDeletedIds(home) {
252
+ const raw = readJsonFile(deletedAgentsPath(home));
253
+ const ids = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.ids) ? raw.ids : []);
254
+ return new Set(ids.map(String).filter(Boolean));
255
+ }
256
+
257
+ export function addDeletedIds(home, ids) {
258
+ const s = readDeletedIds(home);
259
+ for (const id of ids) if (id) s.add(String(id));
260
+ const dir = path.dirname(deletedAgentsPath(home));
261
+ fs.mkdirSync(dir, { recursive: true });
262
+ fs.writeFileSync(deletedAgentsPath(home), JSON.stringify([...s], null, 2));
263
+ return s;
264
+ }
265
+
266
+ export function filterDeleted(agents, home) {
267
+ const del = readDeletedIds(home);
268
+ if (!del.size || !Array.isArray(agents)) return agents || [];
269
+ return agents.filter((a) => a && !del.has(String(a.id)));
270
+ }
271
+
247
272
  export function writeWakeups(home, map) {
248
273
  const dir = path.dirname(wakeupsPath(home));
249
274
  fs.mkdirSync(dir, { recursive: true });
package/lib/grokcli.js CHANGED
@@ -110,7 +110,7 @@ const GROK_BOT_BIN = `${APP}/Contents/MacOS/Grok Bot`;
110
110
  /** pids whose command is the Grok Bot main binary (not grep itself). */
111
111
  export function grokBotPids(run = execSync) {
112
112
  try {
113
- return run(`pgrep -f ${JSON.stringify(GROK_BOT_BIN)}`, { encoding: 'utf8' })
113
+ return run(`pgrep -f ${JSON.stringify(GROK_BOT_BIN)}`, { encoding: 'utf8', shell: true })
114
114
  .trim().split('\n').map((s) => s.trim()).filter(Boolean);
115
115
  } catch {
116
116
  return [];
@@ -296,7 +296,7 @@ export async function runBot(argv = []) {
296
296
  await new Promise((r) => setTimeout(r, 1500));
297
297
  }
298
298
 
299
- if (plan.spawn) {
299
+ const launchGrokBot = () => {
300
300
  console.error('openzoo: launching Grok Bot');
301
301
  console.error(` CURSOR_API_BASE_URL=${url}`);
302
302
  spawn(bin, grokBotChromiumArgs(), {
@@ -315,14 +315,14 @@ export async function runBot(argv = []) {
315
315
  SAND_HOST_GATEWAY_NETWORK_TOKEN: 'openzoo',
316
316
  },
317
317
  }).unref();
318
- } else {
319
- console.error('openzoo: Grok Bot already hijacked — not spawning another copy');
320
- }
318
+ };
319
+ if (plan.spawn) launchGrokBot();
320
+ else console.error('openzoo: Grok Bot already hijacked — not spawning another copy');
321
321
 
322
322
  injectSpendChipInBackground({
323
323
  port: GROKBOT_CDP_PORT,
324
324
  log: (m) => console.error(m),
325
- delayMs: plan.spawn ? 500 : 200,
325
+ delayMs: plan.spawn ? 8000 : 2000,
326
326
  });
327
327
 
328
328
  console.error('openzoo: leave this running. ctrl-c stops the backend.');
@@ -7,9 +7,27 @@
7
7
  */
8
8
  import net from 'node:net';
9
9
  import crypto from 'node:crypto';
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ import { spendChipLabel } from './spendProof.js';
10
14
 
11
15
  export const GROKBOT_CDP_PORT = Number(process.env.OZ_GROKBOT_CDP_PORT || 9444);
12
16
 
17
+ /** Session totals for a floating pill when the open canvas has no footer. */
18
+ export function sessionSpendLabel(home = os.homedir()) {
19
+ try {
20
+ const s = JSON.parse(fs.readFileSync(path.join(home, '.openzoo', 'session.json'), 'utf8'));
21
+ const spent = Number(s.spentUsd || s.spendUsd || 0);
22
+ const would = Number(s.directUsd || 0);
23
+ const saved = Number(s.savedUsd != null ? s.savedUsd : Math.max(0, would - spent));
24
+ if (!(spent > 0.00005)) return '';
25
+ return spendChipLabel({ spent, would, saved, pct: would > 0 ? (100 * saved) / would : 0 });
26
+ } catch {
27
+ return '';
28
+ }
29
+ }
30
+
13
31
  export function grokBotChromiumArgs(port = GROKBOT_CDP_PORT) {
14
32
  return [
15
33
  '--ignore-certificate-errors',
@@ -133,9 +151,12 @@ export function spendOnlyText(t) {
133
151
  }
134
152
 
135
153
  function ozEnsureSpendCss() {
136
- if (document.getElementById('oz-spend-css')) return;
137
- const s = document.createElement('style');
138
- s.id = 'oz-spend-css';
154
+ let s = document.getElementById('oz-spend-css');
155
+ if (!s) {
156
+ s = document.createElement('style');
157
+ s.id = 'oz-spend-css';
158
+ (document.head || document.documentElement).appendChild(s);
159
+ }
139
160
  s.textContent = [
140
161
  '.oz-spend{margin:.55rem 0 0;font-size:12px;color:inherit;opacity:.82;max-width:36em}',
141
162
  '.oz-spend>summary{cursor:help;list-style:none;display:inline-flex;align-items:center;gap:.35rem;',
@@ -144,8 +165,9 @@ function ozEnsureSpendCss() {
144
165
  '.oz-spend>summary::-webkit-details-marker{display:none}',
145
166
  '.oz-spend-body{white-space:pre-wrap;margin:.55rem 0 0;font-size:11px;line-height:1.45;opacity:.88;overflow-wrap:anywhere}',
146
167
  '[data-oz-spend-hide]{display:none!important}',
168
+ 'button[aria-label="Start voice input"],button[aria-label*="voice input" i],',
169
+ 'button[aria-label*="steminvoer" i],button[aria-label^="Microphone"]{display:none!important}',
147
170
  ].join('');
148
- (document.head || document.documentElement).appendChild(s);
149
171
  }
150
172
 
151
173
  function ozSpendHost(el) {
@@ -292,27 +314,91 @@ function ozCollapseSpend() {
292
314
  ozAttachSpendChip(host, split);
293
315
  }
294
316
  ozHideSpendLeftovers();
317
+ ozEnsureFloatSpend();
318
+ }
319
+
320
+ function ozEnsureFloatSpend() {
321
+ const label = String(window.__OZ_SESSION_SPEND__ || '').trim();
322
+ const msgPills = document.querySelectorAll('.oz-spend:not(#oz-spend-float)').length;
323
+ let el = document.getElementById('oz-spend-float');
324
+ if (!label || msgPills) {
325
+ if (el) el.remove();
326
+ return;
327
+ }
328
+ ozEnsureSpendCss();
329
+ if (!el) {
330
+ el = document.createElement('details');
331
+ el.id = 'oz-spend-float';
332
+ el.className = 'oz-spend';
333
+ const sum = document.createElement('summary');
334
+ const body = document.createElement('div');
335
+ body.className = 'oz-spend-body';
336
+ body.textContent = 'session spend (this openzoo bot)';
337
+ el.appendChild(sum);
338
+ el.appendChild(body);
339
+ document.body.appendChild(el);
340
+ }
341
+ ozPlaceFloat(el);
342
+ const sum = el.querySelector('summary');
343
+ if (sum) sum.textContent = 'ⓘ ' + label;
344
+ }
345
+
346
+ function ozPlaceFloat(el) {
347
+ if (!el) return;
348
+ const ta = document.querySelector('textarea, [contenteditable="true"]');
349
+ const box = ta && ta.getBoundingClientRect();
350
+ el.style.position = 'fixed';
351
+ el.style.right = 'auto';
352
+ el.style.zIndex = '2147483646';
353
+ el.style.opacity = '0.95';
354
+ if (box && box.width > 80 && box.top > 48) {
355
+ el.style.left = Math.round(box.left) + 'px';
356
+ el.style.top = Math.round(Math.max(8, box.top - 56)) + 'px';
357
+ el.style.bottom = 'auto';
358
+ } else {
359
+ el.style.left = '96px';
360
+ el.style.bottom = '72px';
361
+ el.style.top = 'auto';
362
+ }
295
363
  }
296
364
 
297
365
  function ozWatchSpend() {
298
- const run = () => { try { ozCollapseSpend(); } catch { /* */ } };
366
+ let t = 0;
367
+ const run = () => {
368
+ const a = document.activeElement;
369
+ if (a && (a.tagName === 'TEXTAREA' || a.tagName === 'INPUT' || a.isContentEditable)) return;
370
+ try { ozCollapseSpend(); } catch (e) {}
371
+ };
372
+ const debounced = () => { clearTimeout(t); t = setTimeout(run, 600); };
299
373
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run);
300
374
  else run();
301
375
  try {
302
- const mo = new MutationObserver(run);
303
- const start = () => { if (document.body) mo.observe(document.body, { childList: true, subtree: true }); };
376
+ const mo = new MutationObserver(debounced);
377
+ const start = () => { if (document.body) mo.observe(document.body, { childList: true, subtree: false }); };
304
378
  if (document.body) start();
305
379
  else document.addEventListener('DOMContentLoaded', start);
306
- } catch { /* */ }
307
- setInterval(run, 1500);
380
+ } catch (e) {}
381
+ try { window.addEventListener('resize', debounced); } catch (e) {}
308
382
  }
309
383
 
310
384
  export function spendChipSource() {
311
385
  return [
312
386
  '(function ozSpendChip(){',
313
387
  "'use strict';",
314
- 'if (window.__OZ_SPEND_CHIP__ === 4) return;',
315
- 'window.__OZ_SPEND_CHIP__ = 4;',
388
+ 'if (window.__OZ_SPEND_CHIP__ === 11) {',
389
+ ' const el = document.getElementById("oz-spend-float");',
390
+ ' const ta = document.querySelector("textarea, [contenteditable=\\"true\\"]");',
391
+ ' const box = ta && ta.getBoundingClientRect();',
392
+ ' if (el && box && box.width > 80) {',
393
+ ' el.style.position = "fixed";',
394
+ ' el.style.left = Math.round(box.left) + "px";',
395
+ ' el.style.top = Math.round(Math.max(8, box.top - 56)) + "px";',
396
+ ' el.style.right = "auto";',
397
+ ' el.style.bottom = "auto";',
398
+ ' }',
399
+ ' return;',
400
+ '}',
401
+ 'window.__OZ_SPEND_CHIP__ = 11;',
316
402
  chipUsd.toString(),
317
403
  labelFromSpendBody.toString(),
318
404
  spendLinesOnly.toString(),
@@ -327,6 +413,8 @@ export function spendChipSource() {
327
413
  ozPreviousMessageCard.toString(),
328
414
  ozAttachSpendChip.toString(),
329
415
  ozCollapseSpend.toString(),
416
+ ozEnsureFloatSpend.toString(),
417
+ ozPlaceFloat.toString(),
330
418
  ozWatchSpend.toString(),
331
419
  'ozWatchSpend();',
332
420
  '})();',
@@ -511,12 +599,24 @@ export async function listCdpPages(port, fetchImpl = fetch) {
511
599
  return [];
512
600
  }
513
601
 
514
- async function injectTarget(target, source, connect) {
602
+ async function injectTarget(target, source, connect, { waitForUi = false } = {}) {
515
603
  const session = await connect(target.webSocketDebuggerUrl);
516
604
  try {
517
605
  await session.send('Page.enable').catch(() => {});
518
606
  await session.send('Runtime.enable').catch(() => {});
519
607
  await session.send('Page.addScriptToEvaluateOnNewDocument', { source }).catch(() => {});
608
+ if (waitForUi) {
609
+ for (let i = 0; i < 32; i += 1) {
610
+ try {
611
+ const r = await session.send('Runtime.evaluate', {
612
+ expression: '!!document.querySelector(\'[class*="sand-"], textarea, [contenteditable="true"]\')',
613
+ returnByValue: true,
614
+ });
615
+ if (r?.result?.value) break;
616
+ } catch { /* still booting */ }
617
+ await new Promise((ok) => setTimeout(ok, 250));
618
+ }
619
+ }
520
620
  await session.send('Runtime.evaluate', { expression: source, awaitPromise: false });
521
621
  } finally {
522
622
  session.close();
@@ -531,6 +631,7 @@ export async function injectSpendChip({
531
631
  connect = cdpSession,
532
632
  tries = 40,
533
633
  delayMs = 400,
634
+ waitForUi = false,
534
635
  } = {}) {
535
636
  let targets = [];
536
637
  for (let i = 0; i < Math.max(1, tries); i += 1) {
@@ -542,10 +643,12 @@ export async function injectSpendChip({
542
643
  log(`openzoo: spend chip CDP :${port} has no pages yet`);
543
644
  return { ok: false, injected: 0 };
544
645
  }
646
+ const label = sessionSpendLabel();
647
+ const src = `window.__OZ_SESSION_SPEND__=${JSON.stringify(label)};\n${source}`;
545
648
  let injected = 0;
546
649
  for (const t of targets) {
547
650
  try {
548
- await injectTarget(t, source, connect);
651
+ await injectTarget(t, src, connect, { waitForUi });
549
652
  injected += 1;
550
653
  } catch (e) {
551
654
  log(`openzoo: spend chip inject ${e.message}`);
@@ -555,11 +658,23 @@ export async function injectSpendChip({
555
658
  return { ok: injected > 0, injected };
556
659
  }
557
660
 
558
- /** Fire-and-forget wrapper used by `openzoo bot`. */
661
+ /** One CDP attach, then drop the debugger. A loop here pauses the renderer
662
+ * so the composer accepts a keystroke and then dies. Reloads pick the chip
663
+ * up via Page.addScriptToEvaluateOnNewDocument from that single inject. */
559
664
  export function injectSpendChipInBackground(opts = {}) {
560
665
  const log = opts.log || ((m) => console.error(m));
561
- injectSpendChip({ ...opts, log }).catch((e) => {
562
- log(`openzoo: spend chip inject failed ${e.message}`);
563
- });
666
+ const wait = Math.max(0, Number(opts.delayMs) || 400);
667
+ setTimeout(() => {
668
+ injectSpendChip({
669
+ tries: 24,
670
+ delayMs: 400,
671
+ waitForUi: true,
672
+ ...opts,
673
+ log,
674
+ connect: (ws) => cdpSession(ws, { timeoutMs: 2500 }),
675
+ }).catch((e) => {
676
+ log(`openzoo: spend chip inject failed ${e.message}`);
677
+ });
678
+ }, wait);
564
679
  }
565
680
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.49",
3
+ "version": "0.50.51",
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",