openzoo 0.50.77 → 0.50.78

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/botlog.js CHANGED
@@ -42,7 +42,7 @@ const MILESTONE = [
42
42
  /ERROR|FAIL|uncaught|rejection|timed out/,
43
43
  /wakeups restored|wakeup fire/,
44
44
  /ozRevive|ship_|ship:/,
45
- /sendPrompt done/,
45
+ /sendPrompt done|>> sendPrompt/,
46
46
  /create_agent tool|deleteAgents n=/,
47
47
  /x_compose|chrome reattach/,
48
48
  ];
@@ -57,7 +57,7 @@ export function isBotMilestone(line) {
57
57
  /**
58
58
  * Terminal gets milestones (or everything with verbose); the FULL stream is
59
59
  * always appended to `file` so quiet mode never destroys evidence.
60
- * Default file: ~/.openzoo/bot.log (truncated at start of each run).
60
+ * Default file: ~/.openzoo/bot.log (appended; one header line per run).
61
61
  */
62
62
  export function makeBotLogger({ verbose = false, write = (m) => console.error(m), file = defaultBotLogPath(), fsMod = null } = {}) {
63
63
  let fd = null;
@@ -65,8 +65,10 @@ export function makeBotLogger({ verbose = false, write = (m) => console.error(m)
65
65
  try {
66
66
  const fsx = fsMod || fsSync;
67
67
  fsx.mkdirSync(pathMod.dirname(file), { recursive: true });
68
- fd = fsx.openSync(file, 'w');
69
- fsx.writeSync(fd, `# openzoo bot full log ${new Date().toISOString()}\n`);
68
+ // APPEND, never truncate: the minutely revive cron restarts a dead
69
+ // hijack, and a truncating open erased the very turn being debugged.
70
+ fd = fsx.openSync(file, 'a');
71
+ fsx.writeSync(fd, `# openzoo bot run ${new Date().toISOString()} pid=${process.pid}\n`);
70
72
  } catch { fd = null; }
71
73
  }
72
74
  return (m) => {
@@ -45,7 +45,7 @@ import {
45
45
  DEFAULT_WAKEUP_PROMPT, addDeletedIds, filterDeleted,
46
46
  } from './grokbotAccount.js';
47
47
  import { formatSpendFooter, mergeTurnProof } from './spendProof.js';
48
- import { zooModelIds } from './models.js';
48
+ import { zooModelIds, zooModelRow, mediaKindOf } from './models.js';
49
49
  import { prefixVisitorRichText } from './grokbotweb.js';
50
50
  import {
51
51
  ingestUpload, lookupUpload, readUploadChunk, readUploadImage, readUploadText,
@@ -2393,9 +2393,24 @@ const LOCAL_TOOLS = [
2393
2393
 
2394
2394
  export const LOCAL_TOOL_NAMES = LOCAL_TOOLS.map((t) => t.function.name);
2395
2395
 
2396
+ /**
2397
+ * OpenAI-compatible upstreams cap `tools` at 128. MEASURED 2026-09-01:
2398
+ * surplusintelligence `400 Invalid 'tools': array too long … got 130` — every
2399
+ * bot turn with both browsers attached died at the door. Locals first, then
2400
+ * the browser MCPs (the bots' real work), then whatever fits.
2401
+ */
2402
+ export const MAX_TOOLS = 128;
2403
+ export function capTools(list, max = MAX_TOOLS) {
2404
+ if (!Array.isArray(list) || list.length <= max) return list;
2405
+ const name = (t) => String(t?.function?.name || '');
2406
+ const local = list.filter((t) => LOCAL_TOOL_NAMES.includes(name(t)));
2407
+ const browser = list.filter((t) => !LOCAL_TOOL_NAMES.includes(name(t)) && /^(chrome|brave)-devtools__/.test(name(t)));
2408
+ const rest = list.filter((t) => !local.includes(t) && !browser.includes(t));
2409
+ return [...local, ...browser, ...rest].slice(0, max);
2410
+ }
2396
2411
  function liveTools() {
2397
2412
  const extra = hostMcpTools();
2398
- return extra.length ? [...LOCAL_TOOLS, ...extra] : LOCAL_TOOLS;
2413
+ return capTools(extra.length ? [...LOCAL_TOOLS, ...extra] : LOCAL_TOOLS);
2399
2414
  }
2400
2415
 
2401
2416
  async function captureScreenshot(log) {
@@ -2744,6 +2759,58 @@ function zooTextFromMessage(msg, data) {
2744
2759
  return '';
2745
2760
  }
2746
2761
 
2762
+ let mediaClient = null;
2763
+ function receiptToX402(r) {
2764
+ if (!r) return {};
2765
+ return { billedUsd: r.billedUsd, directUsd: r.directUsd ?? r.billedUsd, rail: r.rail, tx: r.tx, line: r.line, paid: r.asset };
2766
+ }
2767
+ /** Pay the media endpoint, poll the job, paint the URL. Same PayClient path as zoo_video. */
2768
+ async function mediaTurn({ model, kind, prompt, log, onProgress, signal }) {
2769
+ const { PayClient } = await import('./pay.js');
2770
+ const { config } = await import('./config.js');
2771
+ const { withNamespace } = await import('./namespace.js');
2772
+ mediaClient ||= new PayClient();
2773
+ const say = (m) => { try { onProgress?.(m); } catch { /* */ } };
2774
+ say(`Rendering ${kind} with ${model}…`);
2775
+ log(`cursor-backend: media ${kind} POST model=${model} ${JSON.stringify(String(prompt || '').slice(0, 60))}`);
2776
+ let response; let receipt; let data = {};
2777
+ try {
2778
+ ({ response, receipt } = await mediaClient.fetch(`${config.apiBase}/v1/${kind}s/generations`, {
2779
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ model, prompt }),
2780
+ }));
2781
+ data = await response.json().catch(() => ({}));
2782
+ } catch (e) {
2783
+ return { text: `${kind} generation failed: ${e.message}`, data: {} };
2784
+ }
2785
+ if (!response.ok) return { text: `${kind} generation failed: HTTP ${response.status} ${JSON.stringify(data).slice(0, 240)}`, data: { x402: receiptToX402(receipt) } };
2786
+ if (kind === 'image') {
2787
+ const url = data?.data?.[0]?.url || data?.outputs?.image_url || data?.url || null;
2788
+ return { text: url ? `🖼️ ${url}` : JSON.stringify(data).slice(0, 400), data: { x402: receiptToX402(receipt) } };
2789
+ }
2790
+ const id = data?.id;
2791
+ let url = data?.outputs?.video_url || null;
2792
+ let status = data?.status || 'queued';
2793
+ let err = null;
2794
+ const t0 = Date.now();
2795
+ while (!url && id && Date.now() - t0 < 8 * 60_000) {
2796
+ if (signal?.aborted) throw new Error('superseded');
2797
+ await new Promise((ok) => setTimeout(ok, 5000));
2798
+ try {
2799
+ const r = await fetch(`${config.apiBase}/v1/videos/${encodeURIComponent(id)}`, { headers: withNamespace({}), signal: AbortSignal.timeout(15000) });
2800
+ const j = await r.json().catch(() => ({}));
2801
+ status = j?.status || status;
2802
+ url = j?.outputs?.video_url || null;
2803
+ if (status === 'failed') { err = j?.error?.message || JSON.stringify(j?.error || j).slice(0, 200); break; }
2804
+ } catch (e) { log(`cursor-backend: media poll ${e.message}`); }
2805
+ say(`Rendering video… ${status} ${Math.round((Date.now() - t0) / 1000)}s`);
2806
+ }
2807
+ const text = url
2808
+ ? `🎬 ${url}`
2809
+ : err ? `video failed: ${err}` : `video job ${id || '?'} still ${status} after 8 min — check later: ${config.apiBase}/v1/videos/${id || ''}`;
2810
+ log(`cursor-backend: media video done id=${id} status=${status} url=${url ? 'yes' : 'no'}`);
2811
+ return { text, data: { x402: receiptToX402(receipt) } };
2812
+ }
2813
+
2747
2814
  async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
2748
2815
  const model = currentModel(agentId);
2749
2816
  const helper = localExecSse.size > 0;
@@ -2757,6 +2824,14 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
2757
2824
  };
2758
2825
  await ensureTranscriptHydrated(agentId, log);
2759
2826
  try { await reattachChrome({ log }); } catch (e) { log(`cursor-backend: chrome reattach ${e.message}`); }
2827
+ // A bot pinned to a video/image model: the prompt IS the render request.
2828
+ // A chat POST to ByteDance/Seedance-2.5 came back 502 with an empty body
2829
+ // and painted "(empty zoo reply)" — measured 2026-09-01.
2830
+ if (!chatOnly) {
2831
+ let mediaKind = null;
2832
+ try { mediaKind = mediaKindOf(await zooModelRow(model)); } catch { mediaKind = null; }
2833
+ if (mediaKind) return await mediaTurn({ model, kind: mediaKind, prompt: spoken, log, onProgress: opts.onProgress, signal: opts.signal });
2834
+ }
2760
2835
  log(`cursor-backend: zoo POST :8402 model=${model} helper=${helper ? localExecSse.size : 0} hist=${historyMessages(agentId, spoken).length}${chatOnly ? ` visitor=${visitor.shortname} chat-only` : ''} ${JSON.stringify((spoken || '').slice(0, 60))}`);
2761
2836
  if (typeof opts.onProgress === 'function') opts.onProgress('Working on your Mac…');
2762
2837
 
package/lib/models.js CHANGED
@@ -541,3 +541,31 @@ export function wantsAnthropicModelList(headers = {}) {
541
541
  export function modelsListForRequest(payload, headers) {
542
542
  return wantsAnthropicModelList(headers) ? anthropicModelList(payload) : publishModelList(payload);
543
543
  }
544
+
545
+ /* ── media rows (video / image) ─────────────────────────────────────────── */
546
+ let rowsCache = { at: 0, rows: null, base: '' };
547
+ /** Raw catalog rows incl. media (`kind`, `endpoint`), cached like zooModelIds. */
548
+ export async function zooCatalogRows() {
549
+ if (rowsCache.rows && rowsCache.base === config.apiBase && Date.now() - rowsCache.at < CATALOG_TTL_MS) return rowsCache.rows;
550
+ const r = await fetchHeaders(`${config.apiBase}/v1/models`);
551
+ if (!r.ok) throw new Error(`model catalog fetch failed: HTTP ${r.status}`);
552
+ const d = await r.json();
553
+ const rows = Array.isArray(d?.data) ? d.data : [];
554
+ if (rows.length) rowsCache = { at: Date.now(), rows, base: config.apiBase };
555
+ return rows;
556
+ }
557
+ export async function zooModelRow(id) {
558
+ const want = String(id || '');
559
+ if (!want) return null;
560
+ const rows = await zooCatalogRows();
561
+ return rows.find((m) => m?.id === want) || rows.find((m) => String(m?.id || '').toLowerCase() === want.toLowerCase()) || null;
562
+ }
563
+ /** 'video' | 'image' | null — a chat POST to one of these is a guaranteed empty reply. */
564
+ export function mediaKindOf(row) {
565
+ const k = String(row?.kind || '').toLowerCase();
566
+ if (k === 'video' || k === 'image') return k;
567
+ const ep = String(row?.endpoint || '');
568
+ if (/\/videos\/generations/.test(ep)) return 'video';
569
+ if (/\/images\/generations/.test(ep)) return 'image';
570
+ return null;
571
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.77",
3
+ "version": "0.50.78",
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",