openzoo 0.50.34 → 0.50.36
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/bin/openzoo.js +4 -0
- package/lib/cursorbackend.js +31 -7
- package/lib/grokcli.js +104 -21
- package/package.json +1 -1
package/bin/openzoo.js
CHANGED
|
@@ -93,6 +93,10 @@ usage:
|
|
|
93
93
|
:8443, then launches the app with
|
|
94
94
|
CURSOR_API_BASE_URL=https://127.0.0.1:8443
|
|
95
95
|
(no sudo, no /etc/hosts). Leave it running.
|
|
96
|
+
After a reboot the login item comes back without
|
|
97
|
+
that env — bot bounces it so send works. Already-
|
|
98
|
+
hijacked sessions are left alone.
|
|
99
|
+
--quit force bounce · --no-quit never bounce
|
|
96
100
|
npx openzoo cursor [dir] start proxy+tunnel, write MCP config + every model
|
|
97
101
|
into the picker, and LAUNCH Cursor on [dir]
|
|
98
102
|
(defaults to the current directory; ~ works;
|
package/lib/cursorbackend.js
CHANGED
|
@@ -1549,6 +1549,11 @@ async function runLocalTool(name, args, log) {
|
|
|
1549
1549
|
}
|
|
1550
1550
|
}
|
|
1551
1551
|
|
|
1552
|
+
/** Model parked instead of writing files. Host keeps the tool loop going. */
|
|
1553
|
+
export function looksStoppedReply(s) {
|
|
1554
|
+
return /stopped on research|no (?:new )?app files written|nothing (?:extra )?to open yet|say go again|next message i['’]?ll write|stopped — no new/i.test(String(s || ''));
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1552
1557
|
function zooTextFromMessage(msg, data) {
|
|
1553
1558
|
let c = msg?.content;
|
|
1554
1559
|
if (Array.isArray(c)) {
|
|
@@ -1597,6 +1602,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1597
1602
|
'Pasted images arrive as attachments — you can see them when present. Do not claim you cannot see images if they are in this turn.',
|
|
1598
1603
|
'Never claim you lack filesystem access or local-exec. If a tool errors, report the error.',
|
|
1599
1604
|
'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.',
|
|
1605
|
+
'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.',
|
|
1600
1606
|
'A spend footer is appended after your reply by the host — ignore it.',
|
|
1601
1607
|
'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.',
|
|
1602
1608
|
].join(' '),
|
|
@@ -1624,10 +1630,12 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1624
1630
|
: userContent,
|
|
1625
1631
|
});
|
|
1626
1632
|
|
|
1627
|
-
const maxTok = Number(process.env.OPENZOO_ASK_MAX_TOKENS ||
|
|
1633
|
+
const maxTok = Number(process.env.OPENZOO_ASK_MAX_TOKENS || 8192);
|
|
1634
|
+
const maxSteps = Math.min(64, Math.max(8, Number(process.env.OPENZOO_ASK_TOOL_STEPS || 32)));
|
|
1628
1635
|
let lastData = {};
|
|
1629
1636
|
let text = '';
|
|
1630
1637
|
const usedTools = [];
|
|
1638
|
+
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.';
|
|
1631
1639
|
const zooPost = async (payload) => {
|
|
1632
1640
|
const ms = Number(process.env.OPENZOO_ASK_TIMEOUT_MS || 10 * 60_000);
|
|
1633
1641
|
const post = () => fetch('http://127.0.0.1:8402/v1/chat/completions', {
|
|
@@ -1638,7 +1646,8 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1638
1646
|
});
|
|
1639
1647
|
let r;
|
|
1640
1648
|
let lastErr;
|
|
1641
|
-
|
|
1649
|
+
const tries = 5;
|
|
1650
|
+
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
1642
1651
|
try {
|
|
1643
1652
|
r = await post();
|
|
1644
1653
|
lastErr = null;
|
|
@@ -1646,8 +1655,9 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1646
1655
|
} catch (e) {
|
|
1647
1656
|
lastErr = e;
|
|
1648
1657
|
log(`cursor-backend: zoo POST attempt=${attempt} ${e.message}`);
|
|
1649
|
-
if (attempt ===
|
|
1650
|
-
|
|
1658
|
+
if (attempt === tries) throw e;
|
|
1659
|
+
const abortish = /abort|timeout/i.test(String(e?.name || '') + String(e?.message || ''));
|
|
1660
|
+
await new Promise((ok) => setTimeout(ok, (abortish ? 2000 : 800) * attempt));
|
|
1651
1661
|
}
|
|
1652
1662
|
}
|
|
1653
1663
|
if (!r) throw lastErr || new Error('zoo POST failed');
|
|
@@ -1675,7 +1685,8 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1675
1685
|
}
|
|
1676
1686
|
return { r, data };
|
|
1677
1687
|
};
|
|
1678
|
-
|
|
1688
|
+
let keepGoingNudge = 0;
|
|
1689
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
1679
1690
|
const { r, data } = await zooPost({
|
|
1680
1691
|
model,
|
|
1681
1692
|
messages,
|
|
@@ -1709,13 +1720,26 @@ async function zooComplete(prompt, log, agentId, parsed = {}) {
|
|
|
1709
1720
|
continue;
|
|
1710
1721
|
}
|
|
1711
1722
|
text = zooTextFromMessage(msg, data);
|
|
1712
|
-
|
|
1723
|
+
const finish = data.choices?.[0]?.finish_reason || '?';
|
|
1724
|
+
log(`cursor-backend: << zoo ${r.status} ${text.length}c model=${data.model || model} finish=${finish}`);
|
|
1725
|
+
// Model likes to park after research ("Stopped on research", "say go again")
|
|
1726
|
+
// instead of writing files. Measured 2026-08-30 on volume track00r.
|
|
1727
|
+
if ((finish === 'length' || looksStoppedReply(text)) && keepGoingNudge < 6) {
|
|
1728
|
+
keepGoingNudge += 1;
|
|
1729
|
+
log(`cursor-backend: keep-going nudge=${keepGoingNudge} finish=${finish}`);
|
|
1730
|
+
messages.push(msg);
|
|
1731
|
+
messages.push({ role: 'user', content: KEEP_GOING });
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1713
1734
|
break;
|
|
1714
1735
|
}
|
|
1715
1736
|
if (usedTools.length && !String(text || '').trim()) {
|
|
1737
|
+
const wrote = usedTools.some((t) => t.name === 'write_file');
|
|
1716
1738
|
messages.push({
|
|
1717
1739
|
role: 'user',
|
|
1718
|
-
content:
|
|
1740
|
+
content: wrote
|
|
1741
|
+
? 'Write a short chat reply: files written (full paths), commands that mattered, and what the user should open. No empty message.'
|
|
1742
|
+
: KEEP_GOING,
|
|
1719
1743
|
});
|
|
1720
1744
|
const { r, data } = await zooPost({
|
|
1721
1745
|
model,
|
package/lib/grokcli.js
CHANGED
|
@@ -99,6 +99,74 @@ ${body}
|
|
|
99
99
|
`;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const GROK_BOT_BIN = `${APP}/Contents/MacOS/Grok Bot`;
|
|
103
|
+
|
|
104
|
+
/** pids whose command is the Grok Bot main binary (not grep itself). */
|
|
105
|
+
export function grokBotPids(run = execSync) {
|
|
106
|
+
try {
|
|
107
|
+
return run(`pgrep -f ${JSON.stringify(GROK_BOT_BIN)}`, { encoding: 'utf8' })
|
|
108
|
+
.trim().split('\n').map((s) => s.trim()).filter(Boolean);
|
|
109
|
+
} catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function pidHasNeedle(pid, needle, run = execSync) {
|
|
115
|
+
const n = Number(pid);
|
|
116
|
+
if (!Number.isInteger(n) || n <= 0) return false;
|
|
117
|
+
for (const cmd of [`ps -Eww -p ${n} -o command=`, `ps eww -p ${n}`]) {
|
|
118
|
+
try {
|
|
119
|
+
if (run(cmd, { encoding: 'utf8', timeout: 2000 }).includes(needle)) return true;
|
|
120
|
+
} catch { /* SIP / dead pid */ }
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function somethingTalksToPort(port, run = execSync) {
|
|
126
|
+
try {
|
|
127
|
+
const out = run(`lsof -nP -iTCP:${Number(port)} -sTCP:ESTABLISHED`, {
|
|
128
|
+
encoding: 'utf8', timeout: 2000,
|
|
129
|
+
});
|
|
130
|
+
return /Grok|Electron|Helper/i.test(out);
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Is the running Grok Bot already pointed at our aiserver? */
|
|
137
|
+
export function inspectGrokBotHijack(url, port = 8443, run = execSync) {
|
|
138
|
+
const pids = grokBotPids(run);
|
|
139
|
+
const needle = `CURSOR_API_BASE_URL=${url}`;
|
|
140
|
+
const envHit = pids.some((pid) => pidHasNeedle(pid, needle, run));
|
|
141
|
+
const tcpHit = somethingTalksToPort(port, run);
|
|
142
|
+
return { running: pids.length > 0, pids, hijacked: envHit || tcpHit };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Decide whether to osascript-quit / spawn Grok Bot.
|
|
147
|
+
* forceQuit = --quit / OZ_NO_QUIT=0
|
|
148
|
+
* neverQuit = --no-quit / OZ_NO_QUIT=1
|
|
149
|
+
*/
|
|
150
|
+
export function grokBotQuitPlan({ forceQuit = false, neverQuit = false, running = false, hijacked = false } = {}) {
|
|
151
|
+
if (forceQuit) return { quit: true, spawn: true, reason: 'forced --quit' };
|
|
152
|
+
if (neverQuit) {
|
|
153
|
+
return {
|
|
154
|
+
quit: false,
|
|
155
|
+
spawn: !hijacked,
|
|
156
|
+
reason: hijacked
|
|
157
|
+
? 'explicit --no-quit (already hijacked)'
|
|
158
|
+
: 'explicit --no-quit (Grok Bot may be a login-item without hijack env)',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (!running) return { quit: false, spawn: true, reason: 'Grok Bot not running' };
|
|
162
|
+
if (hijacked) return { quit: false, spawn: false, reason: 'already hijacked — leaving the session' };
|
|
163
|
+
return {
|
|
164
|
+
quit: true,
|
|
165
|
+
spawn: true,
|
|
166
|
+
reason: 'running without hijack env (reboot/login item) — bouncing so CURSOR_API_BASE_URL sticks',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
102
170
|
/**
|
|
103
171
|
* `openzoo bot` — Grok Bot.app on the zoo, no sudo, no /etc/hosts.
|
|
104
172
|
*
|
|
@@ -185,31 +253,46 @@ export async function runBot(argv = []) {
|
|
|
185
253
|
console.error(' EnsureSandBox HIJACKED here; StreamUnifiedChat -> :8402 (x402)');
|
|
186
254
|
}
|
|
187
255
|
|
|
188
|
-
// Default:
|
|
189
|
-
//
|
|
190
|
-
|
|
256
|
+
// Default: leave a Grok Bot that is ALREADY hijacked. After a reboot the
|
|
257
|
+
// login item comes back WITHOUT CURSOR_API_BASE_URL; Electron's single-
|
|
258
|
+
// instance lock then swallows our env'd spawn and the overlay stays
|
|
259
|
+
// "Couldn't send your message". Bounce only that unhijacked case.
|
|
260
|
+
// --quit / OZ_NO_QUIT=0 force a bounce. --no-quit / OZ_NO_QUIT=1 never.
|
|
261
|
+
const state = inspectGrokBotHijack(url, port);
|
|
262
|
+
const plan = grokBotQuitPlan({
|
|
263
|
+
forceQuit: argv.includes('--quit') || process.env.OZ_NO_QUIT === '0',
|
|
264
|
+
neverQuit: argv.includes('--no-quit') || process.env.OZ_NO_QUIT === '1',
|
|
265
|
+
running: state.running,
|
|
266
|
+
hijacked: state.hijacked,
|
|
267
|
+
});
|
|
268
|
+
console.error(`openzoo: grokbot ${plan.reason}`);
|
|
269
|
+
if (plan.quit) {
|
|
191
270
|
try { execSync('osascript -e \'tell application "Grok Bot" to quit\'', { stdio: 'ignore' }); } catch { /* ok */ }
|
|
192
271
|
await new Promise((r) => setTimeout(r, 1500));
|
|
193
272
|
}
|
|
194
273
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
274
|
+
if (plan.spawn) {
|
|
275
|
+
console.error('openzoo: launching Grok Bot');
|
|
276
|
+
console.error(` CURSOR_API_BASE_URL=${url}`);
|
|
277
|
+
spawn(bin, ['--ignore-certificate-errors'], {
|
|
278
|
+
stdio: 'ignore',
|
|
279
|
+
detached: true,
|
|
280
|
+
env: {
|
|
281
|
+
...process.env,
|
|
282
|
+
NODE_TLS_REJECT_UNAUTHORIZED: '0',
|
|
283
|
+
CURSOR_API_BASE_URL: url,
|
|
284
|
+
SAND_BACKEND_URL: url,
|
|
285
|
+
// Helper daemon (local-exec-daemon/main.cjs) uses Node fetch, not Chromium.
|
|
286
|
+
// Without these it dials the cached cursorvm 1337 URL / dies on our
|
|
287
|
+
// self-signed cert and GET /local-exec/requests never arrives.
|
|
288
|
+
SAND_HOST_GATEWAY_URL: url,
|
|
289
|
+
SAND_HOST_GATEWAY_TOKEN: 'openzoo',
|
|
290
|
+
SAND_HOST_GATEWAY_NETWORK_TOKEN: 'openzoo',
|
|
291
|
+
},
|
|
292
|
+
}).unref();
|
|
293
|
+
} else {
|
|
294
|
+
console.error('openzoo: Grok Bot already hijacked — not spawning another copy');
|
|
295
|
+
}
|
|
213
296
|
|
|
214
297
|
console.error('openzoo: leave this running. ctrl-c stops the backend.');
|
|
215
298
|
if (argv.includes('--once')) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.36",
|
|
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",
|