openzoo 0.50.42 → 0.50.43

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.
@@ -39,7 +39,7 @@ import { randomUUID } from 'node:crypto';
39
39
  import { encodeAvailableModels, encodeForMethod, encodeEnsureSandBox, encodeGetGrokBotSendStatus, decodeProtoFields, unwrapConnect } from './cursorapi.js';
40
40
  import {
41
41
  accountPodPath, accountAgentsPath, rosterForAccount, rosterForEvent,
42
- readHouseRoster, houseAgentsPath, shapeAgent,
42
+ readHouseRoster, houseAgentsPath, shapeAgent, agentBrief,
43
43
  } from './grokbotAccount.js';
44
44
  import { formatSpendFooter, mergeTurnProof } from './spendProof.js';
45
45
  import { prefixVisitorRichText } from './grokbotweb.js';
@@ -1885,17 +1885,56 @@ const LOCAL_TOOLS = [
1885
1885
  type: 'function',
1886
1886
  function: {
1887
1887
  name: 'create_agent',
1888
- description: 'Mint another Grok Bot in the sidebar. Use this when asked to spawn bots talking about spawning is not enough.',
1888
+ description: 'Mint another Grok Bot in the sidebar. Pass brief so they keep their job across restarts.',
1889
1889
  parameters: {
1890
1890
  type: 'object',
1891
1891
  properties: {
1892
1892
  name: { type: 'string', description: 'Sidebar label for the new bot.' },
1893
+ brief: { type: 'string', description: 'Standing job. Persisted. Injected as their system brief on every turn.' },
1893
1894
  select: { type: 'boolean', description: 'If true (default), switch the UI to the new bot.' },
1894
1895
  },
1895
1896
  required: ['name'],
1896
1897
  },
1897
1898
  },
1898
1899
  },
1900
+ {
1901
+ type: 'function',
1902
+ function: {
1903
+ name: 'set_brief',
1904
+ description: 'Save a standing brief on a sidebar bot (this one if agent omitted). Survives hijack restart.',
1905
+ parameters: {
1906
+ type: 'object',
1907
+ properties: {
1908
+ brief: { type: 'string' },
1909
+ agent: { type: 'string', description: 'Name or id. Default: the current bot.' },
1910
+ },
1911
+ required: ['brief'],
1912
+ },
1913
+ },
1914
+ },
1915
+ {
1916
+ type: 'function',
1917
+ function: {
1918
+ name: 'list_agents',
1919
+ description: 'List sidebar bots with id, name, brief. Use before message_agent.',
1920
+ parameters: { type: 'object', properties: {} },
1921
+ },
1922
+ },
1923
+ {
1924
+ type: 'function',
1925
+ function: {
1926
+ name: 'message_agent',
1927
+ description: 'Send text to another sidebar bot by name or id, wait for their reply, paint it on their canvas. One hop — do not chain.',
1928
+ parameters: {
1929
+ type: 'object',
1930
+ properties: {
1931
+ to: { type: 'string', description: 'Name or id of the other bot.' },
1932
+ text: { type: 'string' },
1933
+ },
1934
+ required: ['to', 'text'],
1935
+ },
1936
+ },
1937
+ },
1899
1938
  ];
1900
1939
 
1901
1940
  export const LOCAL_TOOL_NAMES = LOCAL_TOOLS.map((t) => t.function.name);
@@ -1935,7 +1974,50 @@ async function captureScreenshot(log) {
1935
1974
  return meta;
1936
1975
  }
1937
1976
 
1938
- async function runLocalTool(name, args, log) {
1977
+ function findAgent(q) {
1978
+ const list = cachedAgentList() || [];
1979
+ const s = String(q || '').trim();
1980
+ if (!s) return null;
1981
+ const lower = s.toLowerCase();
1982
+ return list.find((a) => a.id === s)
1983
+ || list.find((a) => String(a.name || '').toLowerCase() === lower)
1984
+ || list.find((a) => String(a.name || '').toLowerCase().includes(lower))
1985
+ || null;
1986
+ }
1987
+
1988
+ let messageAgentDepth = 0;
1989
+ async function deliverAgentMessage({ fromId, to, text, parsed = {}, log }) {
1990
+ const dest = findAgent(to);
1991
+ if (!dest) return `ERROR no bot named ${JSON.stringify(to)}. list_agents first.`;
1992
+ if (fromId && dest.id === String(fromId)) return 'ERROR cannot message_agent yourself';
1993
+ const msg = String(text || '').trim();
1994
+ if (!msg) return 'ERROR empty message';
1995
+ if (messageAgentDepth >= 1) return 'ERROR message_agent is one hop — reply in chat instead of chaining';
1996
+ const from = findAgent(fromId);
1997
+ const fromName = from?.name || 'another bot';
1998
+ const nonce = `oz-msg-${Date.now()}`;
1999
+ const prompt = `[message from ${fromName}]\n${msg}`;
2000
+ messageAgentDepth += 1;
2001
+ try {
2002
+ const userLine = fanoutLine(dest.id, 'user', prompt, { clientNonce: nonce, requestId: nonce });
2003
+ ssePush('transcript', { ...gatewayEntry(userLine), agentId: dest.id });
2004
+ bumpAgent(dest.id, { preview: msg, notify: true });
2005
+ const z = await zooComplete(prompt, log, dest.id, { ...parsed, visitor: undefined });
2006
+ const reply = String(z.text || '');
2007
+ const line = fanoutLine(dest.id, 'assistant', reply, { clientNonce: nonce, requestId: nonce });
2008
+ ssePush('transcript', { ...gatewayEntry(line), agentId: dest.id });
2009
+ bumpAgent(dest.id, { preview: reply, notify: true });
2010
+ return JSON.stringify({
2011
+ ok: true,
2012
+ to: { id: dest.id, name: dest.name },
2013
+ reply: stripSpendFooter(reply).slice(0, 4000),
2014
+ });
2015
+ } finally {
2016
+ messageAgentDepth -= 1;
2017
+ }
2018
+ }
2019
+
2020
+ async function runLocalTool(name, args, log, ctx = {}) {
1939
2021
  try {
1940
2022
  if (name === 'read_file') {
1941
2023
  const buf = await readLocalBytes(expandUserPath(args.path), log);
@@ -1998,10 +2080,34 @@ async function runLocalTool(name, args, log) {
1998
2080
  if (name === 'create_agent') {
1999
2081
  const agent = mintLocalAgent({
2000
2082
  name: String(args.name || args.title || 'New Bot').slice(0, 80),
2083
+ brief: String(args.brief || args.instructions || ''),
2001
2084
  });
2002
2085
  pushCreatedAgent(agent, { select: args.select !== false });
2003
2086
  log(`cursor-backend: create_agent tool id=${agent.id} name=${JSON.stringify(agent.name)}`);
2004
- return JSON.stringify({ ok: true, id: agent.id, name: agent.name });
2087
+ return JSON.stringify({ ok: true, id: agent.id, name: agent.name, brief: agent.brief || '' });
2088
+ }
2089
+ if (name === 'set_brief') {
2090
+ const who = findAgent(args.agent || args.id || args.name) || findAgent(ctx.agentId);
2091
+ if (!who) return 'ERROR no such agent';
2092
+ const agent = mintLocalAgent({ id: who.id, name: who.name, brief: String(args.brief || '') });
2093
+ return JSON.stringify({ ok: true, id: agent.id, name: agent.name, brief: agent.brief || '' });
2094
+ }
2095
+ if (name === 'list_agents') {
2096
+ const list = cachedAgentList() || [];
2097
+ return JSON.stringify(list.map((a) => ({
2098
+ id: a.id,
2099
+ name: a.name,
2100
+ brief: String(a.brief || '').slice(0, 120),
2101
+ })));
2102
+ }
2103
+ if (name === 'message_agent') {
2104
+ return await deliverAgentMessage({
2105
+ fromId: ctx.agentId,
2106
+ to: args.to || args.agent,
2107
+ text: args.text || args.message || args.prompt,
2108
+ parsed: ctx.parsed,
2109
+ log,
2110
+ });
2005
2111
  }
2006
2112
  return `unknown tool ${name}`;
2007
2113
  } catch (e) {
@@ -2092,9 +2198,15 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
2092
2198
  ].join(' ')
2093
2199
  : [
2094
2200
  `You are ${model} served through openzoo inside Grok Bot.`,
2201
+ (() => {
2202
+ const me = (cachedAgentList() || []).find((a) => a.id === agentId);
2203
+ const brief = me ? agentBrief(me) : '';
2204
+ const who = me?.name ? `You are "${me.name}" in this Grok Bot sidebar.` : '';
2205
+ return brief ? `${who} Standing brief (persisted): ${brief}` : who;
2206
+ })(),
2095
2207
  `You HAVE local tools on the user's computer via ${via}.`,
2096
- 'Tools: read_file, write_file, exec, list_dir, screenshot, click, type_text, key, ui_tree, focus_app, open_url, create_agent.',
2097
- 'create_agent mints a new Grok Bot in the sidebar. When asked to spawn bots, call it do not only describe spawning.',
2208
+ 'Tools: read_file, write_file, exec, list_dir, screenshot, click, type_text, key, ui_tree, focus_app, open_url, create_agent, set_brief, list_agents, message_agent.',
2209
+ 'create_agent mints a sidebar bot. ALWAYS pass brief so they keep the job across restart. set_brief updates it. list_agents + message_agent talk to other bots (one hop). Do not tell the human to copy-paste between canvases.',
2098
2210
  'You CAN click the Mac and fill/submit browser forms. That is required when the user asks. Do not write a markdown briefing instead of clicking. Do not tell the user to click.',
2099
2211
  'Form loop: focus_app Brave Browser → screenshot → click the field (query or x,y) → type_text → screenshot to confirm → click Submit / key enter. screenshot.screen is click coordinate space. Prefer click query="Submit".',
2100
2212
  'screenshot captures the display and attaches the image. For any on-screen form, dashboard, or click target, screenshot first this turn. Do not guess at UI you have not seen.',
@@ -2221,7 +2333,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
2221
2333
  let args = {};
2222
2334
  try { args = JSON.parse(c.function?.arguments || c.arguments || '{}'); } catch { args = {}; }
2223
2335
  const name = c.function?.name || c.name || '';
2224
- let result = await runLocalTool(name, args, log);
2336
+ let result = await runLocalTool(name, args, log, { agentId, parsed });
2225
2337
  if (name === 'screenshot') {
2226
2338
  try {
2227
2339
  const shot = JSON.parse(result);
@@ -111,6 +111,17 @@ function activityTs(agent, activity) {
111
111
  * nulls, then clears the whole persisted tray. That is how a group vanished
112
112
  * after the first send: bumpAgent wrote a partial row, restore returned null.
113
113
  */
114
+ /** Standing job text. shapeAgent used to drop this, so every restart was amnesia. */
115
+ export function agentBrief(raw = {}) {
116
+ const a = raw && typeof raw === 'object' ? raw : {};
117
+ for (const k of ['brief', 'instructions', 'customInstructions', 'systemPrompt']) {
118
+ const v = a[k];
119
+ if (typeof v === 'string' && v.trim()) return v.trim().slice(0, 8000);
120
+ }
121
+ const d = String(a.description || '').trim();
122
+ return d.slice(0, 8000);
123
+ }
124
+
114
125
  export function shapeAgent(raw = {}) {
115
126
  const a = raw && typeof raw === 'object' ? raw : {};
116
127
  const id = String(a.id || '');
@@ -119,10 +130,12 @@ export function shapeAgent(raw = {}) {
119
130
  : (Array.isArray(a.memberAgentIds) ? a.memberAgentIds.map((x) => String(x)).filter(Boolean) : []);
120
131
  const isGroup = a.isGroup === true || memberIds.length > 0;
121
132
  const name = String(a.name || a.title || (isGroup ? 'group' : 'chat'));
133
+ const brief = agentBrief(a);
122
134
  return {
123
135
  id,
124
136
  name,
125
- description: String(a.description || ''),
137
+ brief,
138
+ description: String(a.description || brief || ''),
126
139
  title: String(a.title || name),
127
140
  origin: String(a.origin || 'user'),
128
141
  path: String(a.path || (id ? `/local/${id}` : '/local')),
package/lib/grokcli.js CHANGED
@@ -22,6 +22,7 @@ import { dirname, join } from 'node:path';
22
22
  import { fileURLToPath } from 'node:url';
23
23
 
24
24
  import { config } from './config.js';
25
+ import { killListen } from './proxy.js';
25
26
  import {
26
27
  GROKBOT_CDP_PORT,
27
28
  grokBotChromiumArgs,
@@ -251,10 +252,13 @@ export async function runBot(argv = []) {
251
252
  'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-3.5-turbo',
252
253
  ].map((n) => ({ name: n, label: n }));
253
254
  const log = (m) => console.error(' backend:', m);
255
+ const stale = killListen(port);
256
+ if (stale.length) console.error(`openzoo: killed stale hijack on :${port} pids=${stale.join(',')}`);
254
257
  try {
255
258
  startCursorBackend({ port, models, log });
256
259
  } catch (e) {
257
- console.error('openzoo: 8443 already bound reusing it (', e.message, ')');
260
+ console.error('openzoo: 8443 bind failed after kill (', e.message, ')');
261
+ throw e;
258
262
  }
259
263
  await new Promise((r) => setTimeout(r, 400));
260
264
  let ver = '?';
@@ -443,6 +447,7 @@ export async function setupGrokBot(argv = []) {
443
447
  // persists, nothing else on the machine is affected, no password needed.
444
448
  const { startCursorBackend } = await import('./cursorbackend.js');
445
449
  const port = 8443;
450
+ killListen(port);
446
451
  process.env.OPENZOO_BYOK = '1';
447
452
  // LOUD BY DEFAULT. The previous run failed silently — "listening" and then
448
453
  // nothing — and silence looked identical to success. Every arriving method
@@ -18,30 +18,97 @@ export function grokBotChromiumArgs(port = GROKBOT_CDP_PORT) {
18
18
  ];
19
19
  }
20
20
 
21
- /** Pure. Same split the renderer IIFE uses. */
21
+ function chipUsd(n) {
22
+ const x = Number(n) || 0;
23
+ if (Math.abs(x) >= 0.01) return `$${x.toFixed(2)}`;
24
+ return `$${x.toFixed(4)}`;
25
+ }
26
+
27
+ /** Rebuild pill from spent/saved even if the tag is only `$0.0010`. */
28
+ export function labelFromSpendBody(body, fallback) {
29
+ const s = String(body || '');
30
+ const spent = Number(s.match(/spent \$([0-9.]+)/i)?.[1]);
31
+ const saved = Number(s.match(/saved \$([0-9.]+)/i)?.[1]);
32
+ const would = Number(s.match(/OpenRouter would \$([0-9.]+)/i)?.[1]);
33
+ if (!Number.isFinite(spent) || !(spent > 0)) return (fallback || 'spend').trim();
34
+ const savedN = Number.isFinite(saved) ? saved : (Number.isFinite(would) ? Math.max(0, would - spent) : 0);
35
+ const wouldN = Number.isFinite(would) && would > 0 ? would : 0;
36
+ const pct = wouldN > 0 ? Math.round((100 * savedN) / wouldN) : 0;
37
+ const bits = [chipUsd(spent)];
38
+ if (savedN > 0.00005) bits.push(`saved ${chipUsd(savedN)}`);
39
+ const sav = [];
40
+ if (pct >= 1) sav.push(`${pct}%`);
41
+ if (wouldN > 0 && spent > 0) {
42
+ const m = wouldN / spent;
43
+ if (m >= 1.05) sav.push(m >= 10 ? `${m.toFixed(1)}×` : `${m.toFixed(2)}×`);
44
+ }
45
+ if (sav.length) bits.push(sav.join('/'));
46
+ return bits.join(' · ');
47
+ }
48
+
49
+ /** Drop proves-wall / other-bot transcript that leaked into vis text. */
50
+ export function spendLinesOnly(body) {
51
+ const raw = String(body || '').replace(/::oz-spend::[^\n]*/gi, ' ');
52
+ const parts = raw.split(/\n|(?=this call \$)|(?=spent \$)|(?=tx )|(?=memo )|(?=proves )/i);
53
+ const keep = [];
54
+ let txs = 0;
55
+ for (let i = 0; i < parts.length; i += 1) {
56
+ const line = String(parts[i] || '').trim();
57
+ if (!line) continue;
58
+ if (/^this call \$/i.test(line) || /^spent \$/i.test(line)) { keep.push(line); continue; }
59
+ if (/^tx /i.test(line) || /^https?:\/\/(?:solscan|basescan)/i.test(line)) {
60
+ txs += 1;
61
+ if (txs === 1) keep.push(/^tx /i.test(line) ? line : `tx ${line}`);
62
+ continue;
63
+ }
64
+ if (/^memo /i.test(line)) { keep.push(line.slice(0, 160)); continue; }
65
+ if (/^proves /i.test(line)) continue;
66
+ if (keep.length) break;
67
+ }
68
+ if (txs > 1 && keep.length) {
69
+ const last = keep.length - 1;
70
+ if (/^tx /i.test(keep[last])) keep[last] += ` (+${txs - 1} earlier)`;
71
+ }
72
+ return keep.join('\n');
73
+ }
74
+
75
+ /** Pure. Same split the renderer IIFE uses.
76
+ * vis text from TreeWalker has NO newlines between React text nodes, so
77
+ * `[^\n]+` used to swallow the whole footer and skip the pill (body < 12). */
22
78
  export function splitSpendText(text) {
23
79
  const s = String(text || '');
24
- let m = s.match(/::oz-spend::([^\n]+)\n?([\s\S]*)$/);
25
- if (m && ((m[2] || '').length > 8 || /\$[0-9.]/.test(m[1] || ''))) {
26
- let summary = (m[1] || '').trim();
27
- const body = (m[2] || '').trim();
28
- if (!summary || summary === '$0.0000' || summary === 'spend') {
29
- const spent = body.match(/spent \$([0-9.]+)/i);
30
- const call = body.match(/this call \$([0-9.]+)/i);
31
- const n = Number((call && Number(call[1]) > 0.00005 ? call[1] : null) || (spent && spent[1]) || 0);
32
- if (n > 0) summary = `$${n.toFixed(4)}`;
33
- }
34
- return { head: s.slice(0, m.index), summary: summary || 'spend', body };
80
+ const mark = s.search(/::oz-spend::/i);
81
+ let head = '';
82
+ let raw = '';
83
+ if (mark >= 0) {
84
+ head = s.slice(0, mark);
85
+ raw = s.slice(mark + '::oz-spend::'.length);
86
+ } else {
87
+ const m = s.match(/(?:this call \$|spent \$)[\s\S]*$/i);
88
+ if (!m) return null;
89
+ head = s.slice(0, m.index);
90
+ raw = m[0];
35
91
  }
36
- m = s.match(/(?:this call \$|spent \$)[\s\S]*$/i);
37
- if (m) {
38
- const body = m[0].trim();
39
- const call = body.match(/this call \$([0-9.]+)/i);
40
- const spent = body.match(/spent \$([0-9.]+)/i);
41
- const n = Number((call && Number(call[1]) > 0.00005 ? call[1] : null) || (spent && spent[1]) || 0);
42
- return { head: s.slice(0, m.index), summary: n > 0 ? `$${n.toFixed(4)}` : 'spend', body };
92
+ const bodyAt = raw.search(/(?:this call \$)|(?:spent \$)/i);
93
+ let summary = '';
94
+ let body = raw;
95
+ if (bodyAt > 0) {
96
+ summary = raw.slice(0, bodyAt).replace(/^\s+|\s+$/g, '');
97
+ body = raw.slice(bodyAt);
98
+ } else if (bodyAt === 0) {
99
+ summary = '';
100
+ body = raw;
101
+ } else {
102
+ const nl = raw.indexOf('\n');
103
+ if (nl >= 0) {
104
+ summary = raw.slice(0, nl).trim();
105
+ body = raw.slice(nl + 1);
106
+ }
43
107
  }
44
- return null;
108
+ body = spendLinesOnly(body);
109
+ if (!body || body.length < 8) return null;
110
+ summary = labelFromSpendBody(body, summary);
111
+ return { head, summary: summary || 'spend', body };
45
112
  }
46
113
 
47
114
  export function spendOnlyText(t) {
@@ -56,6 +123,9 @@ export function spendOnlyText(t) {
56
123
  .replace(/saved \$[0-9.]+/gi, ' ')
57
124
  .replace(/balance \$[0-9.]+/gi, ' ')
58
125
  .replace(/\(\s*\d+%\s*\)/g, ' ')
126
+ .replace(/\d+%\s*\/\s*[\d.]+×/g, ' ')
127
+ .replace(/\d+%/g, ' ')
128
+ .replace(/[×xX]/g, ' ')
59
129
  .replace(/\b(?:tx|memo|proves)\b[^\n]*/gi, ' ')
60
130
  .replace(/https?:\/\/\S+/gi, ' ')
61
131
  .replace(/[·•.,;:/$%\d\s()[\]+\-]/g, '');
@@ -166,7 +236,14 @@ function ozPreviousMessageCard(el) {
166
236
 
167
237
  function ozAttachSpendChip(host, split) {
168
238
  if (!host || !split) return;
169
- if (host.querySelector && host.querySelector('.oz-spend')) return;
239
+ const existing = host.querySelector && host.querySelector('.oz-spend');
240
+ if (existing) {
241
+ const sum = existing.querySelector('summary');
242
+ const body = existing.querySelector('.oz-spend-body');
243
+ if (sum) sum.textContent = 'ⓘ ' + split.summary;
244
+ if (body) body.textContent = split.body;
245
+ return;
246
+ }
170
247
  const d = document.createElement('details');
171
248
  d.className = 'oz-spend';
172
249
  const sum = document.createElement('summary');
@@ -233,8 +310,11 @@ export function spendChipSource() {
233
310
  return [
234
311
  '(function ozSpendChip(){',
235
312
  "'use strict';",
236
- 'if (window.__OZ_SPEND_CHIP__) return;',
237
- 'window.__OZ_SPEND_CHIP__ = 1;',
313
+ 'if (window.__OZ_SPEND_CHIP__ === 3) return;',
314
+ 'window.__OZ_SPEND_CHIP__ = 3;',
315
+ chipUsd.toString(),
316
+ labelFromSpendBody.toString(),
317
+ spendLinesOnly.toString(),
238
318
  splitSpendText.toString(),
239
319
  spendOnlyText.toString(),
240
320
  ozEnsureSpendCss.toString(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.42",
3
+ "version": "0.50.43",
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",