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.
- package/lib/cursorbackend.js +381 -12
- package/lib/grokbotDesktop.js +95 -0
- package/lib/grokbotDesktop.jxa +204 -0
- package/lib/grokbotweb-shim.js +2 -2
- package/lib/xb +0 -0
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -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
|
-
|| '
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
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
|
+
}
|
package/lib/grokbotweb-shim.js
CHANGED
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
autoUpdateWhenIdleOptIn: false,
|
|
144
144
|
autoUpdateWhenIdleGateEnabled: false,
|
|
145
145
|
};
|
|
146
|
-
const DEFAULT_MODEL = { modelId: '
|
|
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: '
|
|
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.
|
|
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",
|