openzoo 0.50.39 → 0.50.41

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.
@@ -46,6 +46,9 @@ import { prefixVisitorRichText } from './grokbotweb.js';
46
46
  import {
47
47
  ingestUpload, lookupUpload, readUploadChunk, readUploadImage, readUploadText,
48
48
  } from './grokbotUploads.js';
49
+ import {
50
+ desktopAction, displayBounds, imageSize, noteShotMeta, resolveAppName,
51
+ } from './grokbotDesktop.js';
49
52
 
50
53
  const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
51
54
  const CURSOR_HOSTS = ['api2.cursor.sh', 'api3.cursor.sh', 'api4.cursor.sh', 'repo42.cursor.sh'];
@@ -1129,12 +1132,102 @@ function appendLine(agentId, role, text, extra = {}) {
1129
1132
  timestampMs: ts,
1130
1133
  requestId,
1131
1134
  ...(extra.author && typeof extra.author === 'object' ? { author: extra.author } : {}),
1135
+ ...(extra.ephemeral ? { ephemeral: true } : {}),
1132
1136
  };
1133
1137
  }
1134
1138
  t.entries.push(e);
1135
1139
  saveTranscripts();
1136
1140
  return e;
1137
1141
  }
1142
+
1143
+ /** One zooComplete per agent. A new sendPrompt (or Stop) aborts the previous
1144
+ * loop so "try again" does not stack 32-step exec storms with an empty canvas. */
1145
+ export function createZooTurnQueue() {
1146
+ const inflight = new Map();
1147
+ return {
1148
+ begin(agentId, nonce) {
1149
+ const id = String(agentId || '');
1150
+ const prev = inflight.get(id);
1151
+ if (prev && prev.nonce !== nonce) {
1152
+ try { prev.abort.abort(new Error('superseded')); } catch { /* */ }
1153
+ }
1154
+ const abort = new AbortController();
1155
+ inflight.set(id, { nonce, abort });
1156
+ return abort;
1157
+ },
1158
+ isCurrent(agentId, nonce) {
1159
+ return inflight.get(String(agentId || ''))?.nonce === nonce;
1160
+ },
1161
+ end(agentId, nonce) {
1162
+ const id = String(agentId || '');
1163
+ const cur = inflight.get(id);
1164
+ if (cur && cur.nonce === nonce) inflight.delete(id);
1165
+ },
1166
+ abort(agentId) {
1167
+ const id = String(agentId || '');
1168
+ const prev = inflight.get(id);
1169
+ if (!prev) return 0;
1170
+ try { prev.abort.abort(new Error('interrupted')); } catch { /* */ }
1171
+ inflight.delete(id);
1172
+ return 1;
1173
+ },
1174
+ };
1175
+ }
1176
+ const zooTurns = createZooTurnQueue();
1177
+
1178
+ export function formatZooProgress({ step, maxSteps, names, command } = {}) {
1179
+ const tools = Array.isArray(names) ? names.filter(Boolean).join(', ') : String(names || 'tools');
1180
+ const cmd = command ? ` ${JSON.stringify(String(command).slice(0, 80))}` : '';
1181
+ return `Working on your Mac (step ${Number(step) + 1}/${maxSteps || '?'}): ${tools}${cmd}`;
1182
+ }
1183
+
1184
+ /** One canvas line per tool. Asar ingest is append-only — mutating a working
1185
+ * bubble is still silence. Keep it short; the model history skips ephemeral. */
1186
+ export function formatZooToolLine({ name, args = {}, result } = {}) {
1187
+ const detail = args.command || args.path || args.name || args.window
1188
+ || args.query || args.url || args.key || args.app
1189
+ || (args.x != null && args.y != null ? `${args.x},${args.y}` : '')
1190
+ || (args.text != null ? String(args.text).slice(0, 80) : '');
1191
+ const head = detail
1192
+ ? `${name} ${JSON.stringify(String(detail).slice(0, 90))}`
1193
+ : String(name || 'tool');
1194
+ let tail = String(result || '').replace(/\s+/g, ' ').trim();
1195
+ if (tail.startsWith('{')) {
1196
+ try {
1197
+ const o = JSON.parse(String(result));
1198
+ if (o && typeof o === 'object') {
1199
+ tail = [o.path, o.id, o.name, o.bytes != null ? `${o.bytes}b` : '', o.ok === true ? 'ok' : '']
1200
+ .filter(Boolean).join(' ') || tail;
1201
+ }
1202
+ } catch { /* raw */ }
1203
+ }
1204
+ tail = tail.slice(0, 160);
1205
+ return `→ ${head}${tail ? `\n${tail}` : ''}`;
1206
+ }
1207
+
1208
+ export function combinedAbortSignal(a, b) {
1209
+ if (!a) return b;
1210
+ if (!b) return a;
1211
+ if (typeof AbortSignal.any === 'function') return AbortSignal.any([a, b]);
1212
+ const c = new AbortController();
1213
+ const on = () => { try { c.abort(); } catch { /* */ } };
1214
+ if (a.aborted || b.aborted) { on(); return c.signal; }
1215
+ a.addEventListener('abort', on, { once: true });
1216
+ b.addEventListener('abort', on, { once: true });
1217
+ return c.signal;
1218
+ }
1219
+
1220
+ export function isSupersededError(e) {
1221
+ const m = String(e?.message || e || '');
1222
+ return /superseded|interrupted/i.test(m);
1223
+ }
1224
+
1225
+ function paintChatUpdate(agentId, note, extra = {}) {
1226
+ const line = fanoutLine(agentId, 'assistant', note, { ...extra, ephemeral: true });
1227
+ ssePush('transcript', { ...gatewayEntry(line), agentId });
1228
+ bumpAgent(agentId, { preview: note, notify: false });
1229
+ return line;
1230
+ }
1138
1231
  function fanoutLine(primaryId, role, text, extra = {}) {
1139
1232
  // Only the addressed agent. Writing to every tailedAgents id mixed canvases
1140
1233
  // so a new/empty chat showed someone else's thread.
@@ -1254,7 +1347,7 @@ function mergeAgentLists(remote) {
1254
1347
  return sortAgentsByActivity(out);
1255
1348
  }
1256
1349
  function gatewayEntry(e) {
1257
- const { seq, pulledRemote, promptRaw, ...rest } = e;
1350
+ const { seq, pulledRemote, promptRaw, ephemeral, ...rest } = e;
1258
1351
  return rest;
1259
1352
  }
1260
1353
 
@@ -1286,6 +1379,7 @@ function historyMessages(agentId, currentPrompt) {
1286
1379
  if (!e || typeof e !== 'object') continue;
1287
1380
  let role = null;
1288
1381
  let text = '';
1382
+ if (e.ephemeral) continue;
1289
1383
  if (e.kind === 'send-message' || e.role === 'assistant' || e.kind === 'assistant') {
1290
1384
  text = entryPlainText(e.message ?? e.content ?? e.text);
1291
1385
  role = 'assistant';
@@ -1513,6 +1607,11 @@ const MODEL_ALIASES = {
1513
1607
  grok: 'x-ai/grok-4.6',
1514
1608
  'grok-4': 'x-ai/grok-4.6',
1515
1609
  'grok-4.6': 'x-ai/grok-4.6',
1610
+ glm: 'zai-org/glm-5.3-flash',
1611
+ 'glm-5': 'zai-org/glm-5.3-flash',
1612
+ 'glm-5.3': 'zai-org/glm-5.3-flash',
1613
+ 'glm-5.3-flash': 'zai-org/glm-5.3-flash',
1614
+ flash: 'zai-org/glm-5.3-flash',
1516
1615
  };
1517
1616
  function resolveModelId(raw) {
1518
1617
  const s = String(raw || '').trim();
@@ -1525,7 +1624,7 @@ function resolveModelId(raw) {
1525
1624
  function currentModel(agentId) {
1526
1625
  return agentModels.get(agentId)
1527
1626
  || process.env.OPENZOO_DEFAULT_MODEL
1528
- || 'x-ai/grok-4.6';
1627
+ || 'zai-org/glm-5.3-flash';
1529
1628
  }
1530
1629
 
1531
1630
  const execFileAsync = promisify(execFile);
@@ -1622,9 +1721,10 @@ async function execLocal(command, cwd, log) {
1622
1721
  return `${out}${err}`.trim() || `(exit ${got.exitCode ?? 0})`;
1623
1722
  }
1624
1723
  log(`cursor-backend: exec cwd=${dir} ${JSON.stringify(String(command).slice(0, 80))}`);
1724
+ const execMs = Math.min(180000, Math.max(15000, Number(process.env.OPENZOO_EXEC_TIMEOUT_MS || 90000)));
1625
1725
  const { stdout, stderr } = await execFileAsync('/bin/zsh', ['-lc', command], {
1626
1726
  cwd: dir,
1627
- timeout: 30000,
1727
+ timeout: execMs,
1628
1728
  maxBuffer: 2 * 1024 * 1024,
1629
1729
  env: process.env,
1630
1730
  });
@@ -1672,8 +1772,169 @@ const LOCAL_TOOLS = [
1672
1772
  parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
1673
1773
  },
1674
1774
  },
1775
+ {
1776
+ type: 'function',
1777
+ function: {
1778
+ name: 'screenshot',
1779
+ description: 'Capture the Mac display and attach the image. Returns screen.width/height in POINTS for click x,y. Screenshot first, then click/type, then screenshot again to confirm. Do not guess at a form you have not seen this turn.',
1780
+ parameters: {
1781
+ type: 'object',
1782
+ properties: {
1783
+ window: { type: 'string', description: 'Optional window title substring; default is the full display.' },
1784
+ },
1785
+ },
1786
+ },
1787
+ },
1788
+ {
1789
+ type: 'function',
1790
+ function: {
1791
+ name: 'click',
1792
+ description: 'Click the Mac UI. Prefer query (AX title/button text like "Submit" or "Complete form"). Or x,y in SCREEN POINTS from screenshot.screen (not image pixels). Use image_x/image_y if you measured the attached screenshot. This is how you fill and submit browser forms.',
1793
+ parameters: {
1794
+ type: 'object',
1795
+ properties: {
1796
+ x: { type: 'number' },
1797
+ y: { type: 'number' },
1798
+ image_x: { type: 'number', description: 'X in the attached screenshot pixels; mapped via last screenshot.' },
1799
+ image_y: { type: 'number' },
1800
+ query: { type: 'string', description: 'Visible name: Submit, Complete form, Compose, …' },
1801
+ app: { type: 'string', description: 'Brave Browser, Brave, Grok Bot, …' },
1802
+ button: { type: 'string', enum: ['left', 'right'] },
1803
+ double: { type: 'boolean' },
1804
+ },
1805
+ },
1806
+ },
1807
+ },
1808
+ {
1809
+ type: 'function',
1810
+ function: {
1811
+ name: 'type_text',
1812
+ description: 'Type into the focused field. Click the field first. Long text is pasted. Use this to fill form inputs.',
1813
+ parameters: {
1814
+ type: 'object',
1815
+ properties: {
1816
+ text: { type: 'string' },
1817
+ paste: { type: 'boolean' },
1818
+ app: { type: 'string' },
1819
+ },
1820
+ required: ['text'],
1821
+ },
1822
+ },
1823
+ },
1824
+ {
1825
+ type: 'function',
1826
+ function: {
1827
+ name: 'key',
1828
+ description: 'Press a key. enter/return submits, tab moves fields, escape cancels. cmd+l focuses the URL bar.',
1829
+ parameters: {
1830
+ type: 'object',
1831
+ properties: {
1832
+ key: { type: 'string', description: 'enter, tab, escape, space, a letter, or a named key' },
1833
+ cmd: { type: 'boolean' },
1834
+ alt: { type: 'boolean' },
1835
+ shift: { type: 'boolean' },
1836
+ ctrl: { type: 'boolean' },
1837
+ app: { type: 'string' },
1838
+ },
1839
+ required: ['key'],
1840
+ },
1841
+ },
1842
+ },
1843
+ {
1844
+ type: 'function',
1845
+ function: {
1846
+ name: 'ui_tree',
1847
+ description: 'Dump clickable AX controls of the front window (or app) with screen x,y. Use to find Submit/text fields when the screenshot is ambiguous.',
1848
+ parameters: {
1849
+ type: 'object',
1850
+ properties: {
1851
+ app: { type: 'string' },
1852
+ limit: { type: 'number' },
1853
+ },
1854
+ },
1855
+ },
1856
+ },
1857
+ {
1858
+ type: 'function',
1859
+ function: {
1860
+ name: 'focus_app',
1861
+ description: 'Bring an app to the front. Use "Brave Browser" before clicking Gmail/Stripe.',
1862
+ parameters: {
1863
+ type: 'object',
1864
+ properties: { app: { type: 'string' } },
1865
+ required: ['app'],
1866
+ },
1867
+ },
1868
+ },
1869
+ {
1870
+ type: 'function',
1871
+ function: {
1872
+ name: 'open_url',
1873
+ description: 'Open an http(s) URL in Brave (or app). Prefer this over guessing at the address bar.',
1874
+ parameters: {
1875
+ type: 'object',
1876
+ properties: {
1877
+ url: { type: 'string' },
1878
+ app: { type: 'string' },
1879
+ },
1880
+ required: ['url'],
1881
+ },
1882
+ },
1883
+ },
1884
+ {
1885
+ type: 'function',
1886
+ function: {
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.',
1889
+ parameters: {
1890
+ type: 'object',
1891
+ properties: {
1892
+ name: { type: 'string', description: 'Sidebar label for the new bot.' },
1893
+ select: { type: 'boolean', description: 'If true (default), switch the UI to the new bot.' },
1894
+ },
1895
+ required: ['name'],
1896
+ },
1897
+ },
1898
+ },
1675
1899
  ];
1676
1900
 
1901
+ export const LOCAL_TOOL_NAMES = LOCAL_TOOLS.map((t) => t.function.name);
1902
+
1903
+ async function captureScreenshot(log) {
1904
+ const dir = path.join(os.tmpdir(), 'openzoo-screens');
1905
+ fs.mkdirSync(dir, { recursive: true });
1906
+ const stamp = Date.now();
1907
+ const raw = path.join(dir, `oz-${stamp}.png`);
1908
+ const jpg = path.join(dir, `oz-${stamp}.jpg`);
1909
+ await execFileAsync('screencapture', ['-x', '-C', raw], { timeout: 15000 });
1910
+ if (!fs.existsSync(raw) || fs.statSync(raw).size < 80) {
1911
+ throw new Error('screencapture wrote nothing — Screen Recording permission may be off for Grok Bot / Terminal');
1912
+ }
1913
+ let screen = { width: 0, height: 0 };
1914
+ try { screen = await displayBounds(); } catch (e) {
1915
+ log(`cursor-backend: displayBounds failed: ${e.message}`);
1916
+ }
1917
+ let out = { path: raw, mime: 'image/png', buf: fs.readFileSync(raw) };
1918
+ try {
1919
+ await execFileAsync('sips', [
1920
+ '-Z', '1400', '-s', 'format', 'jpeg', '-s', 'formatOptions', '70',
1921
+ raw, '--out', jpg,
1922
+ ], { timeout: 15000 });
1923
+ if (fs.existsSync(jpg) && fs.statSync(jpg).size > 80) {
1924
+ out = { path: jpg, mime: 'image/jpeg', buf: fs.readFileSync(jpg) };
1925
+ }
1926
+ } catch (e) {
1927
+ log(`cursor-backend: sips screenshot resize failed: ${e.message}`);
1928
+ }
1929
+ let image = { width: 0, height: 0 };
1930
+ try { image = await imageSize(out.path); } catch (e) {
1931
+ log(`cursor-backend: imageSize failed: ${e.message}`);
1932
+ }
1933
+ const meta = { path: out.path, mime: out.mime, buf: out.buf, screen, image };
1934
+ noteShotMeta({ screen, image, path: out.path });
1935
+ return meta;
1936
+ }
1937
+
1677
1938
  async function runLocalTool(name, args, log) {
1678
1939
  try {
1679
1940
  if (name === 'read_file') {
@@ -1697,6 +1958,51 @@ async function runLocalTool(name, args, log) {
1697
1958
  if (name === 'exec') {
1698
1959
  return await execLocal(String(args.command || ''), args.cwd, log);
1699
1960
  }
1961
+ if (name === 'screenshot') {
1962
+ const shot = await captureScreenshot(log);
1963
+ return JSON.stringify({
1964
+ ok: true,
1965
+ path: shot.path,
1966
+ mime: shot.mime,
1967
+ bytes: shot.buf.length,
1968
+ screen: shot.screen,
1969
+ image: shot.image,
1970
+ click: 'x,y are SCREEN points (screenshot.screen). Or pass image_x,image_y from this image. Prefer click query="Submit".',
1971
+ dataUrl: `data:${shot.mime};base64,${shot.buf.toString('base64')}`,
1972
+ });
1973
+ }
1974
+ if (name === 'click') {
1975
+ const got = await desktopAction('click', args, log);
1976
+ return JSON.stringify(got);
1977
+ }
1978
+ if (name === 'type_text') {
1979
+ const got = await desktopAction('type', args, log);
1980
+ return JSON.stringify(got);
1981
+ }
1982
+ if (name === 'key') {
1983
+ const got = await desktopAction('key', args, log);
1984
+ return JSON.stringify(got);
1985
+ }
1986
+ if (name === 'ui_tree') {
1987
+ const got = await desktopAction('tree', args, log);
1988
+ return JSON.stringify(got);
1989
+ }
1990
+ if (name === 'focus_app') {
1991
+ const got = await desktopAction('focus', { app: resolveAppName(args.app) }, log);
1992
+ return JSON.stringify(got);
1993
+ }
1994
+ if (name === 'open_url') {
1995
+ const got = await desktopAction('open_url', args, log);
1996
+ return JSON.stringify(got);
1997
+ }
1998
+ if (name === 'create_agent') {
1999
+ const agent = mintLocalAgent({
2000
+ name: String(args.name || args.title || 'New Bot').slice(0, 80),
2001
+ });
2002
+ pushCreatedAgent(agent, { select: args.select !== false });
2003
+ 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 });
2005
+ }
1700
2006
  return `unknown tool ${name}`;
1701
2007
  } catch (e) {
1702
2008
  return `ERROR ${e.message}`;
@@ -1718,7 +2024,7 @@ function zooTextFromMessage(msg, data) {
1718
2024
  return '';
1719
2025
  }
1720
2026
 
1721
- async function zooComplete(prompt, log, agentId, parsed = {}) {
2027
+ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
1722
2028
  const model = currentModel(agentId);
1723
2029
  const helper = localExecSse.size > 0;
1724
2030
  const visitor = visitorFromSend(parsed);
@@ -1726,8 +2032,12 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1726
2032
  const spoken = visitor && !/^You are /.test(String(prompt || ''))
1727
2033
  ? labeledVisitorPrompt(visitor, prompt)
1728
2034
  : prompt;
2035
+ const throwIfAborted = () => {
2036
+ if (opts.signal?.aborted) throw new Error('superseded');
2037
+ };
1729
2038
  await ensureTranscriptHydrated(agentId, log);
1730
2039
  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))}`);
2040
+ if (typeof opts.onProgress === 'function') opts.onProgress('Working on your Mac…');
1731
2041
 
1732
2042
  const images = [];
1733
2043
  const textFiles = [];
@@ -1783,10 +2093,14 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1783
2093
  : [
1784
2094
  `You are ${model} served through openzoo inside Grok Bot.`,
1785
2095
  `You HAVE local tools on the user's computer via ${via}.`,
1786
- 'Tools: read_file, write_file, exec, list_dir. USE THEM. Write files to disk instead of pasting giant HTML into chat.',
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.',
2098
+ '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
+ '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
+ '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.',
1787
2101
  'Pasted images arrive as attachments — you can see them when present. Do not claim you cannot see images if they are in this turn.',
1788
2102
  'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
1789
- 'After using tools you MUST still write a normal chat reply: what you did, file paths written, and what to open. Empty content is a bug.',
2103
+ 'After using tools you MUST still write a normal chat reply: what you did, file paths written, and what to open. Empty content is a bug. A new user message cancels this turn — leave a visible reply before that happens.',
1790
2104
  'Do not stop mid-task. Do not write "Stopped on research", "nothing to open yet", "no app files written this turn", or "say go again". Keep using tools until the files the user asked for exist on disk THIS turn. Summarize only after those writes succeed.',
1791
2105
  'A spend footer is appended after your reply by the host — ignore it.',
1792
2106
  'Prior turns of THIS Grok Bot chat are in the messages below. Do not claim the thread starts blank or that earlier questions did not arrive.',
@@ -1823,12 +2137,13 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1823
2137
  const usedTools = [];
1824
2138
  const KEEP_GOING = 'Do not stop. Do not wait for another message. Write the files to disk now with write_file / exec. Keep going until the requested paths exist. A summary is only allowed after the files are written.';
1825
2139
  const zooPost = async (payload) => {
2140
+ throwIfAborted();
1826
2141
  const ms = Number(process.env.OPENZOO_ASK_TIMEOUT_MS || 10 * 60_000);
1827
2142
  const post = () => fetch('http://127.0.0.1:8402/v1/chat/completions', {
1828
2143
  method: 'POST',
1829
2144
  headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
1830
2145
  body: JSON.stringify(payload),
1831
- signal: AbortSignal.timeout(ms),
2146
+ signal: combinedAbortSignal(opts.signal, AbortSignal.timeout(ms)),
1832
2147
  });
1833
2148
  let r;
1834
2149
  let lastErr;
@@ -1872,7 +2187,9 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1872
2187
  return { r, data };
1873
2188
  };
1874
2189
  let keepGoingNudge = 0;
2190
+ const pendingVision = [];
1875
2191
  for (let step = 0; step < maxSteps; step++) {
2192
+ throwIfAborted();
1876
2193
  const payload = { model, messages, max_tokens: maxTok };
1877
2194
  if (!chatOnly) {
1878
2195
  payload.tools = LOCAL_TOOLS;
@@ -1896,25 +2213,49 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
1896
2213
  }
1897
2214
  continue;
1898
2215
  }
1899
- log(`cursor-backend: zoo tools step=${step} n=${calls.length} ${calls.map((c) => c.function?.name || c.name).join(',')}`);
2216
+ const names = calls.map((c) => c.function?.name || c.name);
2217
+ log(`cursor-backend: zoo tools step=${step} n=${calls.length} ${names.join(',')}`);
1900
2218
  messages.push(msg);
1901
2219
  for (const c of calls) {
2220
+ throwIfAborted();
1902
2221
  let args = {};
1903
2222
  try { args = JSON.parse(c.function?.arguments || c.arguments || '{}'); } catch { args = {}; }
1904
2223
  const name = c.function?.name || c.name || '';
1905
- const result = await runLocalTool(name, args, log);
2224
+ let result = await runLocalTool(name, args, log);
2225
+ if (name === 'screenshot') {
2226
+ try {
2227
+ const shot = JSON.parse(result);
2228
+ if (shot?.dataUrl) {
2229
+ pendingVision.push({ path: shot.path, dataUrl: shot.dataUrl });
2230
+ result = `screenshot ${shot.path} ${shot.bytes}b — image attached, look at it before acting`;
2231
+ }
2232
+ } catch { /* keep raw */ }
2233
+ }
1906
2234
  usedTools.push({
1907
2235
  name,
1908
2236
  path: args.path,
1909
2237
  command: args.command ? String(args.command).slice(0, 120) : undefined,
1910
2238
  note: String(result).slice(0, 200),
1911
2239
  });
2240
+ if (typeof opts.onProgress === 'function') {
2241
+ opts.onProgress(formatZooToolLine({ name, args, result }));
2242
+ }
1912
2243
  messages.push({
1913
2244
  role: 'tool',
1914
2245
  tool_call_id: c.id,
1915
2246
  content: String(result).slice(0, 120000),
1916
2247
  });
1917
2248
  }
2249
+ if (pendingVision.length) {
2250
+ messages.push({
2251
+ role: 'user',
2252
+ content: [
2253
+ ...pendingVision.map((img) => ({ type: 'image_url', image_url: { url: img.dataUrl } })),
2254
+ { type: 'text', text: pendingVision.map((img) => `screenshot: ${img.path}`).join('\n') },
2255
+ ],
2256
+ });
2257
+ pendingVision.length = 0;
2258
+ }
1918
2259
  continue;
1919
2260
  }
1920
2261
  text = zooTextFromMessage(msg, data);
@@ -2081,7 +2422,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
2081
2422
  'getBotTemplateExportPolicy', 'getTeachRecordingStatus', 'isGlobalSearchEnabled',
2082
2423
  'isEgressTunnelAvailable', 'listBoxMcpServers',
2083
2424
  'setWindowFocused', 'getAgentAutomations',
2084
- 'interruptAgentRun', 'requestDiskSaverAudit',
2425
+ 'requestDiskSaverAudit',
2085
2426
  'setAgentUnread', 'setAgentHiddenFromSidebar', 'setAgentNotificationsEnabled',
2086
2427
  'setAgentNotifyOnUpdates', 'setAgentAvatarBytes', 'getAgentAvatar',
2087
2428
  ]);
@@ -2100,6 +2441,15 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
2100
2441
  log(`cursor-backend: createAgent local id=${agent.id} name=${JSON.stringify(agent.name)}`);
2101
2442
  return true;
2102
2443
  }
2444
+ if (name === 'interruptAgentRun') {
2445
+ let parsed = {};
2446
+ try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
2447
+ const id = String(parsed.id || parsed.agentId || focusedAgentId || '');
2448
+ const n = id ? zooTurns.abort(id) : 0;
2449
+ jsonSend(res, { ok: true, interrupted: n, id });
2450
+ log(`cursor-backend: interruptAgentRun id=${id || '?'} n=${n}`);
2451
+ return true;
2452
+ }
2103
2453
  if (name === 'kickstartAgent') {
2104
2454
  let parsed = {};
2105
2455
  try { parsed = JSON.parse(String(body || '{}')); } catch { parsed = {}; }
@@ -2262,6 +2612,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
2262
2612
  log(`cursor-backend: >> sendPrompt agent=${agentId} keys=${Object.keys(parsed).join(',')} attachments=${attN}${visitor ? ` visitor=${visitor.shortname}` : ''} prompt=${JSON.stringify((prompt || '').slice(0, 80))}`);
2263
2613
  lastSendEchoId = String(nonce);
2264
2614
  jsonSend(res, { accepted: true });
2615
+ const turn = zooTurns.begin(agentId, nonce);
2265
2616
  const userLine = fanoutLine(agentId, 'user', uiText, {
2266
2617
  clientNonce: nonce,
2267
2618
  requestId: nonce,
@@ -2275,12 +2626,13 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
2275
2626
  setImmediate(async () => {
2276
2627
  let text = '';
2277
2628
  let grouped = false;
2629
+ const stillCurrent = () => zooTurns.isCurrent(agentId, nonce) && !turn.signal.aborted;
2278
2630
  try {
2279
2631
  if (modelCmd) {
2280
2632
  const want = modelCmd[1];
2281
2633
  if (!want) {
2282
2634
  const cur = currentModel(agentId);
2283
- text = `current model: ${cur}\nset with /model fable | opus | sonnet | grok | provider/id`;
2635
+ text = `current model: ${cur}\nset with /model fable | opus | sonnet | grok | glm | provider/id`;
2284
2636
  } else {
2285
2637
  const id = resolveModelId(want) || (want.includes('/') ? want : null);
2286
2638
  if (!id) {
@@ -2298,15 +2650,31 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
2298
2650
  grouped = true;
2299
2651
  await runGroupQueue({ agentId, humanPrompt: prompt, parsed, nonce, log });
2300
2652
  } else {
2301
- const z = await zooComplete(prompt, log, agentId, parsed);
2653
+ const z = await zooComplete(prompt, log, agentId, parsed, {
2654
+ signal: turn.signal,
2655
+ onProgress: (note) => {
2656
+ if (!stillCurrent()) return;
2657
+ paintChatUpdate(agentId, note, { clientNonce: nonce, requestId: nonce });
2658
+ },
2659
+ });
2302
2660
  text = z.text;
2303
2661
  }
2304
2662
  }
2305
2663
  } catch (e) {
2664
+ if (!stillCurrent() || isSupersededError(e)) {
2665
+ log(`cursor-backend: sendPrompt superseded agent=${agentId} nonce=${nonce} ${e.message}`);
2666
+ zooTurns.end(agentId, nonce);
2667
+ return;
2668
+ }
2306
2669
  grouped = false;
2307
2670
  text = `openzoo error: ${e.message}`;
2308
2671
  log(`cursor-backend: sendPrompt zoo failed: ${e.message}`);
2309
2672
  }
2673
+ if (!stillCurrent()) {
2674
+ zooTurns.end(agentId, nonce);
2675
+ log(`cursor-backend: sendPrompt dropped stale agent=${agentId} nonce=${nonce}`);
2676
+ return;
2677
+ }
2310
2678
  if (!grouped) {
2311
2679
  const line = fanoutLine(agentId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
2312
2680
  ssePush('transcript', { ...gatewayEntry(line), agentId });
@@ -2315,6 +2683,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
2315
2683
  } else {
2316
2684
  log(`cursor-backend: sendPrompt group done agent=${agentId}`);
2317
2685
  }
2686
+ zooTurns.end(agentId, nonce);
2318
2687
  });
2319
2688
  return true;
2320
2689
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Drive the Mac UI from Grok Bot tools: click, type, key, AX dump.
3
+ * JXA + CGEvent (no cliclick). Needs Accessibility for the node process
4
+ * that runs `openzoo bot` (Terminal / iTerm / Grok).
5
+ */
6
+ import { execFile } from 'node:child_process';
7
+ import { promisify } from 'node:util';
8
+ import { fileURLToPath } from 'node:url';
9
+ import path from 'node:path';
10
+ import fs from 'node:fs';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+ const JXA_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), 'grokbotDesktop.jxa');
14
+
15
+ const APP_ALIASES = {
16
+ brave: 'Brave Browser',
17
+ chrome: 'Google Chrome',
18
+ safari: 'Safari',
19
+ grok: 'Grok Bot',
20
+ finder: 'Finder',
21
+ };
22
+
23
+ export function resolveAppName(raw) {
24
+ const s = String(raw || '').trim();
25
+ if (!s) return '';
26
+ const hit = APP_ALIASES[s.toLowerCase()];
27
+ return hit || s;
28
+ }
29
+
30
+ export function mapImageClick(imageX, imageY, meta) {
31
+ const iw = Number(meta?.image?.width) || 0;
32
+ const ih = Number(meta?.image?.height) || 0;
33
+ const sw = Number(meta?.screen?.width) || 0;
34
+ const sh = Number(meta?.screen?.height) || 0;
35
+ if (!iw || !ih || !sw || !sh) return null;
36
+ return {
37
+ x: Math.round((Number(imageX) / iw) * sw),
38
+ y: Math.round((Number(imageY) / ih) * sh),
39
+ };
40
+ }
41
+
42
+ let lastShotMeta = null;
43
+ export function noteShotMeta(meta) {
44
+ lastShotMeta = meta && typeof meta === 'object' ? meta : null;
45
+ return lastShotMeta;
46
+ }
47
+ export function lastShot() {
48
+ return lastShotMeta;
49
+ }
50
+
51
+ export async function displayBounds() {
52
+ const { stdout } = await execFileAsync('osascript', [
53
+ '-e',
54
+ 'tell application "Finder" to get bounds of window of desktop',
55
+ ], { timeout: 8000 });
56
+ const n = String(stdout).match(/-?\d+/g)?.map(Number) || [];
57
+ const [x0, y0, x1, y1] = n;
58
+ if (![x0, y0, x1, y1].every(Number.isFinite)) {
59
+ throw new Error(`bad display bounds: ${stdout.trim()}`);
60
+ }
61
+ return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
62
+ }
63
+
64
+ export async function imageSize(p) {
65
+ const { stdout } = await execFileAsync('sips', ['-g', 'pixelWidth', '-g', 'pixelHeight', p], { timeout: 8000 });
66
+ const w = Number(String(stdout).match(/pixelWidth:\s*(\d+)/)?.[1]);
67
+ const h = Number(String(stdout).match(/pixelHeight:\s*(\d+)/)?.[1]);
68
+ if (!w || !h) throw new Error(`sips no size for ${p}`);
69
+ return { width: w, height: h };
70
+ }
71
+
72
+ export async function desktopAction(op, args = {}, log = () => {}) {
73
+ if (!fs.existsSync(JXA_PATH)) throw new Error(`missing ${JXA_PATH}`);
74
+ const payload = { op: String(op || ''), ...args };
75
+ if (payload.app) payload.app = resolveAppName(payload.app);
76
+ if (payload.image_x != null || payload.imageX != null) {
77
+ const mapped = mapImageClick(payload.image_x ?? payload.imageX, payload.image_y ?? payload.imageY, lastShotMeta);
78
+ if (mapped) {
79
+ payload.x = mapped.x;
80
+ payload.y = mapped.y;
81
+ payload.mappedFromImage = true;
82
+ }
83
+ }
84
+ log(`cursor-backend: desktop ${payload.op} ${JSON.stringify({
85
+ x: payload.x, y: payload.y, query: payload.query, key: payload.key, app: payload.app,
86
+ n: payload.text ? String(payload.text).length : undefined,
87
+ })}`);
88
+ const { stdout, stderr } = await execFileAsync('osascript', [
89
+ '-l', 'JavaScript', JXA_PATH, JSON.stringify(payload),
90
+ ], { timeout: 20000, maxBuffer: 2 * 1024 * 1024 });
91
+ const raw = String(stdout || '').trim() || String(stderr || '').trim();
92
+ try { return JSON.parse(raw); } catch {
93
+ return { ok: false, error: raw || 'empty jxa' };
94
+ }
95
+ }
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/osascript -l JavaScript
2
+ // Grok Bot desktop driver. argv[0] is a JSON payload {op, ...}.
3
+ ObjC.import('Cocoa');
4
+ ObjC.import('CoreGraphics');
5
+
6
+ function run(argv) {
7
+ var a = {};
8
+ try { a = JSON.parse(argv[0] || '{}'); } catch (e) {
9
+ return JSON.stringify({ ok: false, error: 'bad json: ' + e.message });
10
+ }
11
+ try {
12
+ return JSON.stringify(dispatch(a));
13
+ } catch (e) {
14
+ return JSON.stringify({ ok: false, error: String(e.message || e) });
15
+ }
16
+ }
17
+
18
+ function dispatch(a) {
19
+ var op = String(a.op || '');
20
+ if (op === 'bounds') return bounds();
21
+ if (op === 'focus') return focusApp(a.app);
22
+ if (op === 'click') return click(a);
23
+ if (op === 'type') return typeText(a);
24
+ if (op === 'key') return keyPress(a);
25
+ if (op === 'tree') return uiTree(a);
26
+ if (op === 'open_url') return openUrl(a);
27
+ throw new Error('unknown op ' + op);
28
+ }
29
+
30
+ function bounds() {
31
+ var f = $.NSScreen.mainScreen.frame;
32
+ // Cocoa origin is bottom-left; width/height are points (not retina pixels).
33
+ return {
34
+ ok: true,
35
+ width: f.size.width,
36
+ height: f.size.height,
37
+ x: f.origin.x,
38
+ y: f.origin.y,
39
+ };
40
+ }
41
+
42
+ function focusApp(name) {
43
+ name = String(name || '').trim();
44
+ if (!name) throw new Error('app required');
45
+ var app = Application(name);
46
+ app.activate();
47
+ delay(0.25);
48
+ return { ok: true, app: name };
49
+ }
50
+
51
+ function click(a) {
52
+ if (a.app) focusApp(a.app);
53
+ if (a.query) {
54
+ var hit = findQuery(a.query, a.app);
55
+ if (!hit) throw new Error('no AX match for ' + a.query);
56
+ a.x = hit.x + Math.floor((hit.w || 8) / 2);
57
+ a.y = hit.y + Math.floor((hit.h || 8) / 2);
58
+ a.matched = hit.title || hit.role;
59
+ }
60
+ var x = Number(a.x);
61
+ var y = Number(a.y);
62
+ if (!isFinite(x) || !isFinite(y)) throw new Error('click needs x,y or query');
63
+ // CGEvent uses top-left origin in global display points — same as AX position.
64
+ var pt = $.CGPointMake(x, y);
65
+ var move = $.CGEventCreateMouseEvent(null, $.kCGEventMouseMoved, pt, 0);
66
+ $.CGEventPost($.kCGHIDEventTap, move);
67
+ delay(0.04);
68
+ var btn = String(a.button || 'left') === 'right' ? $.kCGMouseButtonRight : $.kCGMouseButtonLeft;
69
+ var downType = btn === $.kCGMouseButtonRight ? $.kCGEventRightMouseDown : $.kCGEventLeftMouseDown;
70
+ var upType = btn === $.kCGMouseButtonRight ? $.kCGEventRightMouseUp : $.kCGEventLeftMouseUp;
71
+ var down = $.CGEventCreateMouseEvent(null, downType, pt, btn);
72
+ var up = $.CGEventCreateMouseEvent(null, upType, pt, btn);
73
+ $.CGEventPost($.kCGHIDEventTap, down);
74
+ delay(0.03);
75
+ $.CGEventPost($.kCGHIDEventTap, up);
76
+ if (a.double) {
77
+ delay(0.05);
78
+ $.CGEventPost($.kCGHIDEventTap, down);
79
+ delay(0.03);
80
+ $.CGEventPost($.kCGHIDEventTap, up);
81
+ }
82
+ return { ok: true, x: x, y: y, matched: a.matched || null, mappedFromImage: !!a.mappedFromImage };
83
+ }
84
+
85
+ function typeText(a) {
86
+ if (a.app) focusApp(a.app);
87
+ var text = String(a.text == null ? '' : a.text);
88
+ if (!text) throw new Error('text required');
89
+ var se = Application('System Events');
90
+ var paste = a.paste === true || (a.paste !== false && text.length > 20);
91
+ if (paste) {
92
+ var app = Application.currentApplication();
93
+ app.includeStandardAdditions = true;
94
+ app.setTheClipboardTo(text);
95
+ delay(0.05);
96
+ se.keystroke('v', { using: 'command down' });
97
+ return { ok: true, method: 'paste', n: text.length };
98
+ }
99
+ se.keystroke(text);
100
+ return { ok: true, method: 'keystroke', n: text.length };
101
+ }
102
+
103
+ var KEYCODES = {
104
+ enter: 36, return: 36, tab: 48, escape: 53, esc: 53, space: 49,
105
+ delete: 51, backspace: 51, up: 126, down: 125, left: 123, right: 124,
106
+ };
107
+
108
+ function keyPress(a) {
109
+ if (a.app) focusApp(a.app);
110
+ var key = String(a.key || '').toLowerCase();
111
+ if (!key) throw new Error('key required');
112
+ var using = [];
113
+ if (a.cmd || a.command || a.meta) using.push('command down');
114
+ if (a.alt || a.option) using.push('option down');
115
+ if (a.shift) using.push('shift down');
116
+ if (a.ctrl || a.control) using.push('control down');
117
+ var se = Application('System Events');
118
+ var opts = using.length ? { using: using } : {};
119
+ var code = KEYCODES[key];
120
+ if (code != null) se.keyCode(code, opts);
121
+ else se.keystroke(key.length === 1 ? key : key.charAt(0), opts);
122
+ return { ok: true, key: key, mods: using };
123
+ }
124
+
125
+ function openUrl(a) {
126
+ var url = String(a.url || a.href || '').trim();
127
+ if (!/^https?:\/\//i.test(url)) throw new Error('url must be http(s)');
128
+ var appName = resolveBrave(a.app);
129
+ var app = Application(appName);
130
+ app.activate();
131
+ delay(0.2);
132
+ try {
133
+ app.windows[0].activeTab.url = url;
134
+ } catch (e) {
135
+ app.openLocation(url);
136
+ }
137
+ return { ok: true, app: appName, url: url };
138
+ }
139
+
140
+ function resolveBrave(name) {
141
+ name = String(name || 'Brave Browser');
142
+ if (/brave/i.test(name)) return 'Brave Browser';
143
+ return name;
144
+ }
145
+
146
+ function uiTree(a) {
147
+ var se = Application('System Events');
148
+ var proc;
149
+ if (a.app) {
150
+ proc = se.processes.byName(String(a.app));
151
+ } else {
152
+ proc = se.processes.whose({ frontmost: true })[0];
153
+ }
154
+ var limit = Math.min(120, Math.max(10, Number(a.limit) || 60));
155
+ var rows = [];
156
+ var win;
157
+ try { win = proc.windows[0]; } catch (e) { throw new Error('no window for ' + (a.app || 'front app')); }
158
+ function walk(el, depth) {
159
+ if (rows.length >= limit || depth > 10) return;
160
+ var role = '', title = '', val = '', pos = null, size = null;
161
+ try { role = String(el.role()); } catch (e) {}
162
+ try { title = String(el.title() || el.description() || el.name() || ''); } catch (e) {}
163
+ try {
164
+ var v = el.value();
165
+ val = v == null ? '' : String(v);
166
+ } catch (e) {}
167
+ try { pos = el.position(); } catch (e) {}
168
+ try { size = el.size(); } catch (e) {}
169
+ var interesting = /button|text|field|link|check|pop|menu|tab|combo|scroll|heading|web/i.test(role + ' ' + title);
170
+ if (interesting && (title || val || /button|field|link|check/i.test(role))) {
171
+ rows.push({
172
+ role: role,
173
+ title: title.slice(0, 80),
174
+ value: val.slice(0, 80),
175
+ x: pos ? pos[0] : null,
176
+ y: pos ? pos[1] : null,
177
+ w: size ? size[0] : null,
178
+ h: size ? size[1] : null,
179
+ });
180
+ }
181
+ var kids = [];
182
+ try { kids = el.uiElements(); } catch (e) { return; }
183
+ for (var i = 0; i < kids.length; i++) walk(kids[i], depth + 1);
184
+ }
185
+ walk(win, 0);
186
+ var title = '';
187
+ try { title = String(win.title()); } catch (e) {}
188
+ var pname = '';
189
+ try { pname = String(proc.name()); } catch (e) {}
190
+ return { ok: true, app: pname, window: title, n: rows.length, rows: rows };
191
+ }
192
+
193
+ function findQuery(q, app) {
194
+ var tree = uiTree({ app: app, limit: 120 });
195
+ var want = String(q || '').toLowerCase();
196
+ if (!want) return null;
197
+ var rows = tree.rows || [];
198
+ for (var i = 0; i < rows.length; i++) {
199
+ var r = rows[i];
200
+ var blob = ((r.title || '') + ' ' + (r.value || '') + ' ' + (r.role || '')).toLowerCase();
201
+ if (blob.indexOf(want) >= 0 && r.x != null && r.y != null) return r;
202
+ }
203
+ return null;
204
+ }
@@ -143,7 +143,7 @@
143
143
  autoUpdateWhenIdleOptIn: false,
144
144
  autoUpdateWhenIdleGateEnabled: false,
145
145
  };
146
- const DEFAULT_MODEL = { modelId: 'x-ai/grok-4.6', maxMode: true, parameters: [] };
146
+ const DEFAULT_MODEL = { modelId: 'zai-org/glm-5.3-flash', maxMode: true, parameters: [] };
147
147
  let defaultModel = DEFAULT_MODEL;
148
148
  try {
149
149
  const raw = localStorage.getItem('sand.p.default-model');
@@ -300,7 +300,7 @@
300
300
  setComputerUseModel: async () => null,
301
301
  getAvailableModels: async () => ({
302
302
  models: [
303
- { modelId: 'x-ai/grok-4.6', maxMode: true, parameters: [] },
303
+ { modelId: 'zai-org/glm-5.3-flash', maxMode: true, parameters: [] },
304
304
  { modelId: 'x-ai/grok-4.5', maxMode: true, parameters: [] },
305
305
  ],
306
306
  }),
package/lib/xb ADDED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.39",
3
+ "version": "0.50.41",
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",