openzoo 0.50.58 → 0.50.60
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 +3 -2
- package/lib/mcpbridge.js +71 -4
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
import {
|
|
54
54
|
desktopAction, displayBounds, imageSize, noteShotMeta, resolveAppName,
|
|
55
55
|
} from './grokbotDesktop.js';
|
|
56
|
-
import { startHostMcps, hostMcpTools, hostMcpHas, callHostMcp, hostMcpServers, chromeStatus } from './mcpbridge.js';
|
|
56
|
+
import { startHostMcps, hostMcpTools, hostMcpHas, callHostMcp, hostMcpServers, chromeStatus, reattachChrome } from './mcpbridge.js';
|
|
57
57
|
import * as ship from './ship.js';
|
|
58
58
|
|
|
59
59
|
const TLS_DIR = path.join(os.homedir(), '.openzoo', 'cursor-tls');
|
|
@@ -2572,6 +2572,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
2572
2572
|
if (opts.signal?.aborted) throw new Error('superseded');
|
|
2573
2573
|
};
|
|
2574
2574
|
await ensureTranscriptHydrated(agentId, log);
|
|
2575
|
+
try { await reattachChrome({ log }); } catch (e) { log(`cursor-backend: chrome reattach ${e.message}`); }
|
|
2575
2576
|
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))}`);
|
|
2576
2577
|
if (typeof opts.onProgress === 'function') opts.onProgress('Working on your Mac…');
|
|
2577
2578
|
|
|
@@ -2649,7 +2650,7 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
2649
2650
|
return [
|
|
2650
2651
|
`Host MCP tools from the user's local Claude/Grok config are attached (${servers.join(', ') || 'mcp'}): ${names.slice(0, 36).join(', ')}${names.length > 36 ? '…' : ''}.`,
|
|
2651
2652
|
chrome.length
|
|
2652
|
-
? `For any web page or HTML form, use chrome-devtools tools (navigate_page / take_snapshot / fill / click). Do NOT use osascript, Quartz, Python Foundation, or AppleScript to read Brave. screenshot/click/type_text are for native Mac UI only. Browser mode: ${chromeStatus().mode}.${chromeStatus().hint ? ` If the human
|
|
2653
|
+
? `For any web page or HTML form, use chrome-devtools tools (navigate_page / take_snapshot / fill / click). Do NOT use osascript, Quartz, Python Foundation, or AppleScript to read Brave. screenshot/click/type_text are for native Mac UI only. On heavy pages (x.com, gmail, big SPAs) take_snapshot can hang: prefer evaluate_script that returns only the text you need, or take_screenshot; a tool that times out says so — do not repeat it. Do not re-run schedule_wakeup or list_agents every turn; once is enough. Browser mode: ${chromeStatus().mode}.${chromeStatus().hint ? ` This Chrome is NOT the human's browser: a separate profile with no logins. If the human asks for their logged-in Chrome / Google / X / "the same chrome", do NOT open pages in this one and do NOT spawn anything — reply with exactly this and stop: ${chromeStatus().hint}` : ' This is the human\'s real logged-in browser; do not log out or change its settings.'}`
|
|
2653
2654
|
: 'Use matching MCP tools instead of inventing shell one-liners.',
|
|
2654
2655
|
].join(' ');
|
|
2655
2656
|
})(),
|
package/lib/mcpbridge.js
CHANGED
|
@@ -20,6 +20,8 @@ const registry = new Map();
|
|
|
20
20
|
let openaiTools = [];
|
|
21
21
|
let started = null;
|
|
22
22
|
let serversUp = [];
|
|
23
|
+
let chromeCfg = null;
|
|
24
|
+
let lastReattachCheck = 0;
|
|
23
25
|
|
|
24
26
|
export function hostMcpTools() {
|
|
25
27
|
return openaiTools;
|
|
@@ -323,7 +325,7 @@ async function connectOne(cfg, log) {
|
|
|
323
325
|
try {
|
|
324
326
|
transport.stderr?.on?.('data', (buf) => {
|
|
325
327
|
const line = String(buf).trim().split('\n')[0];
|
|
326
|
-
if (line) log?.(`cursor-backend: mcp ${cfg.name} ${line.slice(0, 160)}`);
|
|
328
|
+
if (line && !/No handler registered for issue code/.test(line)) log?.(`cursor-backend: mcp ${cfg.name} ${line.slice(0, 160)}`);
|
|
327
329
|
});
|
|
328
330
|
} catch { /* */ }
|
|
329
331
|
}
|
|
@@ -337,6 +339,48 @@ async function connectOne(cfg, log) {
|
|
|
337
339
|
return { client, tools };
|
|
338
340
|
}
|
|
339
341
|
|
|
342
|
+
/**
|
|
343
|
+
* The human flipped chrome://inspect/#remote-debugging AFTER boot: Chrome
|
|
344
|
+
* writes DevToolsActivePort immediately. Swap the blank-profile
|
|
345
|
+
* chrome-devtools for one attached to their real browser, no restart.
|
|
346
|
+
* Cheap (two stats) — called at the top of every zoo turn, throttled 5s.
|
|
347
|
+
*/
|
|
348
|
+
export async function reattachChrome({ log = () => {}, home = os.homedir() } = {}) {
|
|
349
|
+
if (!chromeCfg || !started) return false;
|
|
350
|
+
if (/^(real-chrome|real-brave|explicit)$/.test(chromeMode.mode)) return false;
|
|
351
|
+
const now = Date.now();
|
|
352
|
+
if (now - lastReattachCheck < 5000) return false;
|
|
353
|
+
lastReattachCheck = now;
|
|
354
|
+
const chromeDir = path.join(home, 'Library', 'Application Support', 'Google', 'Chrome');
|
|
355
|
+
const braveDir = path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser');
|
|
356
|
+
const chromePort = readActivePort(chromeDir);
|
|
357
|
+
const bravePort = readActivePort(braveDir);
|
|
358
|
+
const port = chromePort || bravePort;
|
|
359
|
+
if (!port || !(await portOpen(port))) return false;
|
|
360
|
+
log(`cursor-backend: mcp chrome-devtools real browser appeared on ${port} — re-attaching`);
|
|
361
|
+
const oldClients = new Set();
|
|
362
|
+
for (const [name, rec] of [...registry]) {
|
|
363
|
+
if (rec.server === chromeCfg.name) { oldClients.add(rec.client); registry.delete(name); }
|
|
364
|
+
}
|
|
365
|
+
for (const c of oldClients) { try { await c.close?.(); } catch { /* */ } }
|
|
366
|
+
serversUp = serversUp.filter((n) => n !== chromeCfg.name);
|
|
367
|
+
try {
|
|
368
|
+
const got = await connectOne(chromeCfg, log);
|
|
369
|
+
serversUp.push(chromeCfg.name);
|
|
370
|
+
for (const tool of got.tools) {
|
|
371
|
+
registry.set(toolOpenaiName(chromeCfg.name, tool.name), {
|
|
372
|
+
client: got.client, server: chromeCfg.name, tool: tool.name, description: tool.description || tool.name, schema: tool.inputSchema,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
rebuildOpenai();
|
|
376
|
+
log(`cursor-backend: mcp chrome-devtools re-attached mode=${chromeMode.mode} tools=${got.tools.length}`);
|
|
377
|
+
return true;
|
|
378
|
+
} catch (e) {
|
|
379
|
+
log(`cursor-backend: mcp chrome-devtools re-attach FAIL ${e.message}`);
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
340
384
|
function rebuildOpenai() {
|
|
341
385
|
openaiTools = [];
|
|
342
386
|
for (const [name, rec] of registry) {
|
|
@@ -345,11 +389,33 @@ function rebuildOpenai() {
|
|
|
345
389
|
}
|
|
346
390
|
}
|
|
347
391
|
|
|
348
|
-
|
|
392
|
+
/**
|
|
393
|
+
* One MCP call, with a hard ceiling. Measured 2026-09-01: take_snapshot on
|
|
394
|
+
* x.com never returned and the whole zoo turn froze behind it. A tool that
|
|
395
|
+
* hangs must come back as an error string the model can route around.
|
|
396
|
+
*/
|
|
397
|
+
export const MCP_CALL_TIMEOUT_MS = Math.max(10_000, Number(process.env.OZ_MCP_CALL_TIMEOUT_MS || 75_000));
|
|
398
|
+
export async function callHostMcp(name, args, { timeoutMs = MCP_CALL_TIMEOUT_MS } = {}) {
|
|
349
399
|
const rec = registry.get(String(name || ''));
|
|
350
400
|
if (!rec) throw new Error(`no mcp tool ${name}`);
|
|
351
|
-
|
|
352
|
-
|
|
401
|
+
let timer;
|
|
402
|
+
const ceiling = new Promise((_, rej) => {
|
|
403
|
+
timer = setTimeout(() => rej(new Error(`MCP tool ${name} timed out after ${Math.round(timeoutMs / 1000)}s. The page may be too heavy for that action. Use a narrower one: evaluate_script to pull the text you need, take_screenshot, or navigate_page to a lighter URL. Do not retry the same call.`)), timeoutMs);
|
|
404
|
+
timer.unref?.();
|
|
405
|
+
});
|
|
406
|
+
try {
|
|
407
|
+
const r = await Promise.race([
|
|
408
|
+
rec.client.callTool({ name: rec.tool, arguments: args && typeof args === 'object' ? args : {} }),
|
|
409
|
+
ceiling,
|
|
410
|
+
]);
|
|
411
|
+
return flattenMcpResult(r);
|
|
412
|
+
} finally {
|
|
413
|
+
clearTimeout(timer);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function registerHostMcpForTests(name, rec) {
|
|
418
|
+
registry.set(name, rec);
|
|
353
419
|
}
|
|
354
420
|
|
|
355
421
|
export function resetHostMcpForTests() {
|
|
@@ -367,6 +433,7 @@ export async function startHostMcps({ log = () => {}, home = os.homedir() } = {}
|
|
|
367
433
|
}
|
|
368
434
|
started = (async () => {
|
|
369
435
|
const configs = loadHostMcpConfigs(home);
|
|
436
|
+
chromeCfg = configs.find((c) => c.name === 'chrome-devtools') || configs.find((c) => /chrome|devtools|browser/i.test(c.name)) || null;
|
|
370
437
|
log(`cursor-backend: mcp loading n=${configs.length} ${configs.map((c) => c.name).join(',')}`);
|
|
371
438
|
const results = await Promise.allSettled(configs.map((cfg) => connectOne(cfg, log)));
|
|
372
439
|
for (let i = 0; i < results.length; i++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.60",
|
|
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",
|