openzoo 0.50.40 → 0.50.42
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 +154 -7
- package/lib/grokbotDesktop.js +95 -0
- package/lib/grokbotDesktop.jxa +204 -0
- package/lib/grokbotweb-shim.js +2 -2
- package/lib/ozSpendChip.js +6 -5
- package/lib/spendProof.js +55 -8
- 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'];
|
|
@@ -1181,7 +1184,10 @@ export function formatZooProgress({ step, maxSteps, names, command } = {}) {
|
|
|
1181
1184
|
/** One canvas line per tool. Asar ingest is append-only — mutating a working
|
|
1182
1185
|
* bubble is still silence. Keep it short; the model history skips ephemeral. */
|
|
1183
1186
|
export function formatZooToolLine({ name, args = {}, result } = {}) {
|
|
1184
|
-
const detail = args.command || args.path || args.name || args.window
|
|
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) : '');
|
|
1185
1191
|
const head = detail
|
|
1186
1192
|
? `${name} ${JSON.stringify(String(detail).slice(0, 90))}`
|
|
1187
1193
|
: String(name || 'tool');
|
|
@@ -1601,6 +1607,11 @@ const MODEL_ALIASES = {
|
|
|
1601
1607
|
grok: 'x-ai/grok-4.6',
|
|
1602
1608
|
'grok-4': 'x-ai/grok-4.6',
|
|
1603
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',
|
|
1604
1615
|
};
|
|
1605
1616
|
function resolveModelId(raw) {
|
|
1606
1617
|
const s = String(raw || '').trim();
|
|
@@ -1613,7 +1624,7 @@ function resolveModelId(raw) {
|
|
|
1613
1624
|
function currentModel(agentId) {
|
|
1614
1625
|
return agentModels.get(agentId)
|
|
1615
1626
|
|| process.env.OPENZOO_DEFAULT_MODEL
|
|
1616
|
-
|| '
|
|
1627
|
+
|| 'zai-org/glm-5.3-flash';
|
|
1617
1628
|
}
|
|
1618
1629
|
|
|
1619
1630
|
const execFileAsync = promisify(execFile);
|
|
@@ -1765,7 +1776,7 @@ const LOCAL_TOOLS = [
|
|
|
1765
1776
|
type: 'function',
|
|
1766
1777
|
function: {
|
|
1767
1778
|
name: 'screenshot',
|
|
1768
|
-
description: 'Capture the Mac
|
|
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.',
|
|
1769
1780
|
parameters: {
|
|
1770
1781
|
type: 'object',
|
|
1771
1782
|
properties: {
|
|
@@ -1774,6 +1785,102 @@ const LOCAL_TOOLS = [
|
|
|
1774
1785
|
},
|
|
1775
1786
|
},
|
|
1776
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
|
+
},
|
|
1777
1884
|
{
|
|
1778
1885
|
type: 'function',
|
|
1779
1886
|
function: {
|
|
@@ -1803,18 +1910,29 @@ async function captureScreenshot(log) {
|
|
|
1803
1910
|
if (!fs.existsSync(raw) || fs.statSync(raw).size < 80) {
|
|
1804
1911
|
throw new Error('screencapture wrote nothing — Screen Recording permission may be off for Grok Bot / Terminal');
|
|
1805
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) };
|
|
1806
1918
|
try {
|
|
1807
1919
|
await execFileAsync('sips', [
|
|
1808
1920
|
'-Z', '1400', '-s', 'format', 'jpeg', '-s', 'formatOptions', '70',
|
|
1809
1921
|
raw, '--out', jpg,
|
|
1810
1922
|
], { timeout: 15000 });
|
|
1811
1923
|
if (fs.existsSync(jpg) && fs.statSync(jpg).size > 80) {
|
|
1812
|
-
|
|
1924
|
+
out = { path: jpg, mime: 'image/jpeg', buf: fs.readFileSync(jpg) };
|
|
1813
1925
|
}
|
|
1814
1926
|
} catch (e) {
|
|
1815
1927
|
log(`cursor-backend: sips screenshot resize failed: ${e.message}`);
|
|
1816
1928
|
}
|
|
1817
|
-
|
|
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;
|
|
1818
1936
|
}
|
|
1819
1937
|
|
|
1820
1938
|
async function runLocalTool(name, args, log) {
|
|
@@ -1847,9 +1965,36 @@ async function runLocalTool(name, args, log) {
|
|
|
1847
1965
|
path: shot.path,
|
|
1848
1966
|
mime: shot.mime,
|
|
1849
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".',
|
|
1850
1971
|
dataUrl: `data:${shot.mime};base64,${shot.buf.toString('base64')}`,
|
|
1851
1972
|
});
|
|
1852
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
|
+
}
|
|
1853
1998
|
if (name === 'create_agent') {
|
|
1854
1999
|
const agent = mintLocalAgent({
|
|
1855
2000
|
name: String(args.name || args.title || 'New Bot').slice(0, 80),
|
|
@@ -1948,8 +2093,10 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
1948
2093
|
: [
|
|
1949
2094
|
`You are ${model} served through openzoo inside Grok Bot.`,
|
|
1950
2095
|
`You HAVE local tools on the user's computer via ${via}.`,
|
|
1951
|
-
'Tools: read_file, write_file, exec, list_dir, screenshot,
|
|
2096
|
+
'Tools: read_file, write_file, exec, list_dir, screenshot, click, type_text, key, ui_tree, focus_app, open_url, create_agent.',
|
|
1952
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".',
|
|
1953
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.',
|
|
1954
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.',
|
|
1955
2102
|
'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
|
|
@@ -2485,7 +2632,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
|
|
|
2485
2632
|
const want = modelCmd[1];
|
|
2486
2633
|
if (!want) {
|
|
2487
2634
|
const cur = currentModel(agentId);
|
|
2488
|
-
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`;
|
|
2489
2636
|
} else {
|
|
2490
2637
|
const id = resolveModelId(want) || (want.includes('/') ? want : null);
|
|
2491
2638
|
if (!id) {
|
|
@@ -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/ozSpendChip.js
CHANGED
|
@@ -21,11 +21,11 @@ export function grokBotChromiumArgs(port = GROKBOT_CDP_PORT) {
|
|
|
21
21
|
/** Pure. Same split the renderer IIFE uses. */
|
|
22
22
|
export function splitSpendText(text) {
|
|
23
23
|
const s = String(text || '');
|
|
24
|
-
let m = s.match(/::oz-spend::(
|
|
25
|
-
if (m && (m[2] || '').length > 8) {
|
|
24
|
+
let m = s.match(/::oz-spend::([^\n]+)\n?([\s\S]*)$/);
|
|
25
|
+
if (m && ((m[2] || '').length > 8 || /\$[0-9.]/.test(m[1] || ''))) {
|
|
26
26
|
let summary = (m[1] || '').trim();
|
|
27
|
-
const body = m[2].trim();
|
|
28
|
-
if (
|
|
27
|
+
const body = (m[2] || '').trim();
|
|
28
|
+
if (!summary || summary === '$0.0000' || summary === 'spend') {
|
|
29
29
|
const spent = body.match(/spent \$([0-9.]+)/i);
|
|
30
30
|
const call = body.match(/this call \$([0-9.]+)/i);
|
|
31
31
|
const n = Number((call && Number(call[1]) > 0.00005 ? call[1] : null) || (spent && spent[1]) || 0);
|
|
@@ -69,7 +69,8 @@ function ozEnsureSpendCss() {
|
|
|
69
69
|
s.textContent = [
|
|
70
70
|
'.oz-spend{margin:.55rem 0 0;font-size:12px;color:inherit;opacity:.82;max-width:36em}',
|
|
71
71
|
'.oz-spend>summary{cursor:help;list-style:none;display:inline-flex;align-items:center;gap:.35rem;',
|
|
72
|
-
'padding:3px 9px;border-radius:999px;border:1px solid rgba(255,255,255,.18)
|
|
72
|
+
'padding:3px 9px;border-radius:999px;border:1px solid rgba(255,255,255,.18);white-space:nowrap;',
|
|
73
|
+
'max-width:100%;overflow:hidden;text-overflow:ellipsis}',
|
|
73
74
|
'.oz-spend>summary::-webkit-details-marker{display:none}',
|
|
74
75
|
'.oz-spend-body{white-space:pre-wrap;margin:.55rem 0 0;font-size:11px;line-height:1.45;opacity:.88;overflow-wrap:anywhere}',
|
|
75
76
|
'[data-oz-spend-hide]{display:none!important}',
|
package/lib/spendProof.js
CHANGED
|
@@ -250,9 +250,53 @@ export function mergeTurnProof(prev, data) {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
function chipUsd(n) {
|
|
254
|
+
const x = Number(n) || 0;
|
|
255
|
+
if (Math.abs(x) >= 0.01) return `$${x.toFixed(2)}`;
|
|
256
|
+
return `$${x.toFixed(4)}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function chipMult(would, spent) {
|
|
260
|
+
if (!(spent > 0) || !(would > 0)) return null;
|
|
261
|
+
const m = would / spent;
|
|
262
|
+
if (!Number.isFinite(m) || m < 1) return null;
|
|
263
|
+
if (m >= 100) return `${Math.round(m)}×`;
|
|
264
|
+
if (m >= 10) return `${m.toFixed(1)}×`;
|
|
265
|
+
return `${m.toFixed(2)}×`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Collapsed chip: spent · saved $ · saved % / multiplier. Visible before click. */
|
|
269
|
+
export function spendChipLabel({
|
|
270
|
+
billedUsd,
|
|
271
|
+
directUsd,
|
|
272
|
+
spent = 0,
|
|
273
|
+
would = 0,
|
|
274
|
+
saved = 0,
|
|
275
|
+
pct = 0,
|
|
276
|
+
} = {}) {
|
|
277
|
+
let spentN = Number(spent);
|
|
278
|
+
let wouldN = Number(would);
|
|
279
|
+
let savedN = Number(saved);
|
|
280
|
+
if (!(spentN > 0.00005) && Number(billedUsd) > 0.00005) {
|
|
281
|
+
spentN = Number(billedUsd);
|
|
282
|
+
wouldN = Number(directUsd) > 0 ? Number(directUsd) : wouldN;
|
|
283
|
+
savedN = Math.max(0, wouldN - spentN);
|
|
284
|
+
}
|
|
285
|
+
if (!(savedN > 0) && wouldN > spentN) savedN = wouldN - spentN;
|
|
286
|
+
let pctN = Number(pct);
|
|
287
|
+
if (wouldN > 0) pctN = 100 * Math.max(0, savedN) / wouldN;
|
|
288
|
+
const bits = [chipUsd(spentN), `saved ${chipUsd(savedN)}`];
|
|
289
|
+
const sav = [];
|
|
290
|
+
if (Number.isFinite(pctN)) sav.push(`${Math.round(pctN)}%`);
|
|
291
|
+
const mult = chipMult(wouldN, spentN);
|
|
292
|
+
if (mult) sav.push(mult);
|
|
293
|
+
if (sav.length) bits.push(sav.join('/'));
|
|
294
|
+
return bits.join(' · ');
|
|
295
|
+
}
|
|
296
|
+
|
|
253
297
|
/**
|
|
254
298
|
* Two leading newlines, then the existing spend lines, then optional
|
|
255
|
-
* tx / memo / proves. Never throws.
|
|
299
|
+
* tx / memo / proves. Never throws. Collapsed chip reads the ::oz-spend:: line.
|
|
256
300
|
*/
|
|
257
301
|
export function formatSpendFooter({
|
|
258
302
|
billedUsd,
|
|
@@ -280,15 +324,18 @@ export function formatSpendFooter({
|
|
|
280
324
|
const savedN = Number.isFinite(Number(saved)) ? Number(saved) : 0;
|
|
281
325
|
const pctN = Number.isFinite(Number(pct)) ? Number(pct) : 0;
|
|
282
326
|
const bal = balance != null && Number.isFinite(Number(balance)) ? Number(balance) : null;
|
|
283
|
-
const balTxt = bal != null ? ` · balance $${bal.toFixed(2)}` : '';
|
|
327
|
+
const balTxt = bal != null && bal > 0.004 ? ` · balance $${bal.toFixed(2)}` : '';
|
|
284
328
|
lines.push(`spent $${spentN.toFixed(4)}${balTxt} · OpenRouter would $${wouldN.toFixed(4)} · saved $${savedN.toFixed(4)} (${pctN.toFixed(0)}%)`);
|
|
285
329
|
|
|
286
330
|
const sigs = collectTxs({ tx: tx ?? x.tx, txs: txs ?? x.txs });
|
|
287
331
|
const r = rail || x.rail;
|
|
288
332
|
const net = network || x.network;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
333
|
+
const last = sigs[sigs.length - 1];
|
|
334
|
+
if (last) {
|
|
335
|
+
const url = explorerUrl(last, { rail: r, network: net });
|
|
336
|
+
if (url) {
|
|
337
|
+
lines.push(sigs.length > 1 ? `tx ${url} (+${sigs.length - 1} earlier)` : `tx ${url}`);
|
|
338
|
+
}
|
|
292
339
|
}
|
|
293
340
|
const mem = memo ?? x.memo;
|
|
294
341
|
if (mem != null && String(mem).length) {
|
|
@@ -297,9 +344,9 @@ export function formatSpendFooter({
|
|
|
297
344
|
if (d.proves) lines.push(`proves ${d.proves}`);
|
|
298
345
|
}
|
|
299
346
|
const body = lines.filter((l) => l !== '').join('\n');
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
347
|
+
const tag = spendChipLabel({
|
|
348
|
+
billedUsd, directUsd, spent: spentN, would: wouldN, saved: savedN, pct: pctN,
|
|
349
|
+
});
|
|
303
350
|
return `\n\n::oz-spend::${tag}\n${body}`;
|
|
304
351
|
} catch {
|
|
305
352
|
return '\n\n';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.42",
|
|
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",
|