openzoo 0.50.66 → 0.50.68
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 +79 -9
- package/lib/mcpbridge.js +23 -12
- package/lib/xclaims.js +15 -7
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -2332,6 +2332,22 @@ const LOCAL_TOOLS = [
|
|
|
2332
2332
|
},
|
|
2333
2333
|
},
|
|
2334
2334
|
},
|
|
2335
|
+
{
|
|
2336
|
+
type: 'function',
|
|
2337
|
+
function: {
|
|
2338
|
+
name: 'x_open',
|
|
2339
|
+
description: 'Open YOUR OWN tab in the given browser (chrome|brave) at a URL and pin it to you: every later chrome-devtools__/brave-devtools__ call you make is routed to that tab automatically, so other bots cannot stomp on it. Call this first, before evaluate_script/x_compose. Returns the page text length.',
|
|
2340
|
+
parameters: { type: 'object', properties: { browser: { type: 'string', enum: ['chrome', 'brave'] }, url: { type: 'string' } }, required: ['browser', 'url'] },
|
|
2341
|
+
},
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
type: 'function',
|
|
2345
|
+
function: {
|
|
2346
|
+
name: 'x_close',
|
|
2347
|
+
description: 'Close the tab x_open pinned to you (do this at the end of every turn).',
|
|
2348
|
+
parameters: { type: 'object', properties: { browser: { type: 'string', enum: ['chrome', 'brave'] } }, required: ['browser'] },
|
|
2349
|
+
},
|
|
2350
|
+
},
|
|
2335
2351
|
];
|
|
2336
2352
|
|
|
2337
2353
|
export const LOCAL_TOOL_NAMES = LOCAL_TOOLS.map((t) => t.function.name);
|
|
@@ -2419,6 +2435,35 @@ async function deliverAgentMessage({ fromId, to, text, parsed = {}, log }) {
|
|
|
2419
2435
|
}
|
|
2420
2436
|
}
|
|
2421
2437
|
|
|
2438
|
+
|
|
2439
|
+
/** agentId -> { 'chrome-devtools': pageId, 'brave-devtools': pageId }. A bot's
|
|
2440
|
+
* browser calls are preceded by select_page on its pinned tab (under the
|
|
2441
|
+
* per-browser lock) so 13 bots on one Chrome do not act on each other's tab. */
|
|
2442
|
+
const agentPages = new Map();
|
|
2443
|
+
function pinnedPage(agentId, server) { return agentPages.get(String(agentId || ''))?.[server]; }
|
|
2444
|
+
function pinPage(agentId, server, pageId) {
|
|
2445
|
+
const m = agentPages.get(String(agentId || '')) || {};
|
|
2446
|
+
if (pageId == null) delete m[server]; else m[server] = pageId;
|
|
2447
|
+
agentPages.set(String(agentId || ''), m);
|
|
2448
|
+
}
|
|
2449
|
+
/** chrome-devtools-mcp prints "## Pages\n1: <url> [selected]\n2: …". */
|
|
2450
|
+
export function selectedPageId(text) {
|
|
2451
|
+
const m = String(text || '').match(/(\d+):\s[^\n]*\[selected\]/);
|
|
2452
|
+
return m ? Number(m[1]) : null;
|
|
2453
|
+
}
|
|
2454
|
+
async function callBrowserForAgent(server, tool, args, agentId) {
|
|
2455
|
+
const pinned = pinnedPage(agentId, server);
|
|
2456
|
+
if (pinned != null && tool === 'select_page' && Number(args?.pageId) !== Number(pinned)) {
|
|
2457
|
+
// A pinned bot stays on its own tab. Answer with its tab, do not switch.
|
|
2458
|
+
return callHostMcp(`${server}__select_page`, { pageId: pinned });
|
|
2459
|
+
}
|
|
2460
|
+
if (pinned != null && !/^(new_page|list_pages|select_page|close_page)$/.test(tool)) {
|
|
2461
|
+
try { await callHostMcp(`${server}__select_page`, { pageId: pinned }); } catch { /* page gone; fall through */ }
|
|
2462
|
+
}
|
|
2463
|
+
const out = await callHostMcp(`${server}__${tool}`, args);
|
|
2464
|
+
if (tool === 'new_page') { const pid = selectedPageId(out); if (pid != null) pinPage(agentId, server, pid); }
|
|
2465
|
+
return out;
|
|
2466
|
+
}
|
|
2422
2467
|
async function runLocalTool(name, args, log, ctx = {}) {
|
|
2423
2468
|
try {
|
|
2424
2469
|
if (name === 'read_file') {
|
|
@@ -2537,21 +2582,45 @@ async function runLocalTool(name, args, log, ctx = {}) {
|
|
|
2537
2582
|
if (name === 'x_done') return JSON.stringify(xclaims.markDone({ tweet: args.tweet || args.id, by: me.id, name: me.name, url: args.url, lane: args.lane }));
|
|
2538
2583
|
if (name === 'x_release') return JSON.stringify(xclaims.releaseTweet({ tweet: args.tweet || args.id, by: me.id }));
|
|
2539
2584
|
if (name === 'x_claims') return JSON.stringify(xclaims.listClaims());
|
|
2585
|
+
if (name === 'x_open') {
|
|
2586
|
+
const server = String(args.browser || 'chrome').toLowerCase() === 'brave' ? 'brave-devtools' : 'chrome-devtools';
|
|
2587
|
+
if (!hostMcpHas(`${server}__new_page`)) return JSON.stringify({ ok: false, error: `${server} not attached` });
|
|
2588
|
+
const out = String(await callHostMcp(`${server}__new_page`, { url: String(args.url || 'https://x.com') }));
|
|
2589
|
+
const pid = selectedPageId(out);
|
|
2590
|
+
if (pid == null) return JSON.stringify({ ok: false, error: 'could not read the new page id', raw: out.slice(0, 300) });
|
|
2591
|
+
pinPage(ctx.agentId, server, pid);
|
|
2592
|
+
return JSON.stringify({ ok: true, browser: server, pageId: pid, note: 'pinned: your browser calls now go to this tab' });
|
|
2593
|
+
}
|
|
2594
|
+
if (name === 'x_close') {
|
|
2595
|
+
const server = String(args.browser || 'chrome').toLowerCase() === 'brave' ? 'brave-devtools' : 'chrome-devtools';
|
|
2596
|
+
const pid = pinnedPage(ctx.agentId, server);
|
|
2597
|
+
if (pid == null) return JSON.stringify({ ok: true, closed: false });
|
|
2598
|
+
try { await callHostMcp(`${server}__close_page`, { pageId: pid }); } catch (e) { pinPage(ctx.agentId, server, null); return JSON.stringify({ ok: false, error: e.message }); }
|
|
2599
|
+
pinPage(ctx.agentId, server, null);
|
|
2600
|
+
return JSON.stringify({ ok: true, closed: true, pageId: pid });
|
|
2601
|
+
}
|
|
2540
2602
|
if (name === 'x_compose') {
|
|
2541
2603
|
const server = String(args.browser || 'chrome').toLowerCase() === 'brave' ? 'brave-devtools' : 'chrome-devtools';
|
|
2542
|
-
const
|
|
2604
|
+
for (const t of ['evaluate_script', 'click_at', 'type_text', 'press_key']) {
|
|
2605
|
+
if (!hostMcpHas(`${server}__${t}`)) return JSON.stringify({ ok: false, error: `${server}__${t} not attached` });
|
|
2606
|
+
}
|
|
2607
|
+
const probeRaw = String(await callBrowserForAgent(server, 'evaluate_script', { function: xclaims.composeProbeScript() }, ctx.agentId));
|
|
2608
|
+
const probe = (() => { try { return JSON.parse(probeRaw.match(/\{[\s\S]*\}/)?.[0] || '{}'); } catch { return {}; } })();
|
|
2609
|
+
if (!probe.ok) return JSON.stringify({ ok: false, error: probe.error || `composer probe failed: ${probeRaw.slice(0, 200)}` });
|
|
2610
|
+
const plan = xclaims.composePlan(args.text, probe);
|
|
2543
2611
|
let last = '';
|
|
2544
|
-
for (const op of plan.ops)
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2612
|
+
for (const op of plan.ops) last = String(await callBrowserForAgent(server, op.tool, op.args, ctx.agentId));
|
|
2613
|
+
const back = (() => { try { return JSON.parse(last.match(/\{[\s\S]*\}/)?.[0] || '{}'); } catch { return {}; } })();
|
|
2614
|
+
log(`cursor-backend: x_compose ${server} lines=${plan.lines} back=${back.lines} replyEnabled=${back.replyEnabled}`);
|
|
2615
|
+
if (!back.replyEnabled) {
|
|
2616
|
+
return JSON.stringify({ ok: false, browser: server, error: 'typed, but X did not register it (Reply still disabled). Click the reply box yourself with a real click (find [data-testid="tweetTextarea_0"] and use click_at on its centre), then call x_compose again.', composer: back });
|
|
2549
2617
|
}
|
|
2550
|
-
|
|
2551
|
-
return JSON.stringify({ ok: true, browser: server, linesTyped: plan.lines, composer: last.slice(0, 700), next: 'verify the composer text above shows separate lines, then click the Reply button' });
|
|
2618
|
+
return JSON.stringify({ ok: true, browser: server, linesTyped: plan.lines, composer: back, next: 'lines are in and Reply is enabled: click the Reply button ([data-testid="tweetButtonInline"]) now' });
|
|
2552
2619
|
}
|
|
2553
2620
|
}
|
|
2554
2621
|
if (hostMcpHas(name)) {
|
|
2622
|
+
const m = name.match(/^(chrome-devtools|brave-devtools)__(.+)$/);
|
|
2623
|
+
if (m) return await callBrowserForAgent(m[1], m[2], args, ctx.agentId);
|
|
2555
2624
|
return await callHostMcp(name, args);
|
|
2556
2625
|
}
|
|
2557
2626
|
return `unknown tool ${name}`;
|
|
@@ -2710,7 +2779,8 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
2710
2779
|
return brief ? `${who} Standing brief (persisted): ${brief}` : who;
|
|
2711
2780
|
})(),
|
|
2712
2781
|
`You HAVE local tools on the user's computer via ${via}.`,
|
|
2713
|
-
'Tools: read_file, write_file, exec, list_dir, screenshot, click, type_text, key, ui_tree, focus_app, open_url, create_agent, set_brief, list_agents, message_agent, schedule_wakeup, cancel_wakeup, ship_crew, ship_forge, ship_launch_worker, ship_status, ship_review, ship_open_pr, x_claim, x_done, x_release, x_claims, x_compose.',
|
|
2782
|
+
'Tools: read_file, write_file, exec, list_dir, screenshot, click, type_text, key, ui_tree, focus_app, open_url, create_agent, set_brief, list_agents, message_agent, schedule_wakeup, cancel_wakeup, ship_crew, ship_forge, ship_launch_worker, ship_status, ship_review, ship_open_pr, x_claim, x_done, x_release, x_claims, x_compose, x_open, x_close.',
|
|
2783
|
+
'Tabs: x_open {browser,url} FIRST — it pins a tab to you and every browser call you make is routed there. x_close at the end. Do not select_page other tabs.',
|
|
2714
2784
|
'Posting on X: NEVER fill the reply box (newlines get flattened). Open the tweet, click the reply box, then x_compose {browser, text} which types line by line with Enter, then click Reply.',
|
|
2715
2785
|
'X reply bots: x_claims first (skip those ids), x_claim the tweet BEFORE drafting (ok:false -> pick another), x_done with our reply URL after posting, x_release if you back out. Never reply to a tweet you did not claim.',
|
|
2716
2786
|
'If no sidebar bot is named Firstmate and the human brings code or repo work, ask ONE question: which repo path to set up Grok Ship for. Then call ship_crew with that cwd. Do not start coding outside the factory.',
|
package/lib/mcpbridge.js
CHANGED
|
@@ -213,12 +213,12 @@ export function loadHostMcpConfigs(home = os.homedir()) {
|
|
|
213
213
|
// remote debugging it writes DevToolsActivePort like Chrome does; a second
|
|
214
214
|
// chrome-devtools-mcp attaches to it by --browserUrl and its tools land as
|
|
215
215
|
// brave-devtools__*. Half the X bots drive Brave, half drive Chrome.
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
216
|
+
const brave = readActive(path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser'));
|
|
217
|
+
if (brave.port && !seen.has('brave-devtools')) {
|
|
218
218
|
out.push({
|
|
219
219
|
name: 'brave-devtools',
|
|
220
220
|
command: 'npx',
|
|
221
|
-
args: ['-y', 'chrome-devtools-mcp@latest',
|
|
221
|
+
args: ['-y', 'chrome-devtools-mcp@latest', ...braveAttachArgs(brave)],
|
|
222
222
|
url: '',
|
|
223
223
|
});
|
|
224
224
|
}
|
|
@@ -277,22 +277,32 @@ export function chromeStatus() {
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
function readActivePort(dir) {
|
|
280
|
+
return readActive(dir).port;
|
|
281
|
+
}
|
|
282
|
+
/** DevToolsActivePort = "<port>\n<ws path>". Chrome 144+ "Allow remote debugging"
|
|
283
|
+
* serves ONLY that websocket path (no /json/version), so a second browser must
|
|
284
|
+
* be attached by --wsEndpoint, not --browserUrl. */
|
|
285
|
+
export function readActive(dir) {
|
|
280
286
|
try {
|
|
281
|
-
const first = fs.readFileSync(path.join(dir, 'DevToolsActivePort'), 'utf8').split('\n')
|
|
282
|
-
const
|
|
283
|
-
|
|
287
|
+
const [first, second = ''] = fs.readFileSync(path.join(dir, 'DevToolsActivePort'), 'utf8').split('\n').map((l) => l.trim());
|
|
288
|
+
const port = Number(first);
|
|
289
|
+
if (!Number.isFinite(port) || port <= 0) return { port: 0, ws: '' };
|
|
290
|
+
return { port, ws: second.startsWith('/') ? `ws://127.0.0.1:${port}${second}` : '' };
|
|
284
291
|
} catch {
|
|
285
|
-
return 0;
|
|
292
|
+
return { port: 0, ws: '' };
|
|
286
293
|
}
|
|
287
294
|
}
|
|
295
|
+
export function braveAttachArgs({ port, ws }) {
|
|
296
|
+
return ws ? ['--wsEndpoint', ws] : ['--browserUrl', `http://127.0.0.1:${port}`];
|
|
297
|
+
}
|
|
288
298
|
|
|
289
299
|
/** Pure: decide the chrome-devtools-mcp argv from what is observable. */
|
|
290
|
-
export function chromeArgsFor(baseArgs, { chromePort = 0, bravePort = 0, openPorts = [], hasOwnBraveServer = false } = {}) {
|
|
300
|
+
export function chromeArgsFor(baseArgs, { chromePort = 0, bravePort = 0, braveWs = '', openPorts = [], hasOwnBraveServer = false } = {}) {
|
|
291
301
|
const args = [...(baseArgs || ['-y', 'chrome-devtools-mcp@latest'])];
|
|
292
302
|
const has = (flag) => args.some((a) => String(a) === flag || String(a).startsWith(`${flag}=`));
|
|
293
303
|
if (has('--browserUrl') || has('--autoConnect') || has('--wsEndpoint')) return { args, mode: 'explicit', detail: '' };
|
|
294
304
|
if (chromePort) { args.push('--autoConnect'); return { args, mode: 'real-chrome', detail: `Chrome DevToolsActivePort ${chromePort}` }; }
|
|
295
|
-
if (bravePort && !hasOwnBraveServer) { args.push(
|
|
305
|
+
if (bravePort && !hasOwnBraveServer) { args.push(...braveAttachArgs({ port: bravePort, ws: braveWs })); return { args, mode: 'real-brave', detail: `Brave DevToolsActivePort ${bravePort}` }; }
|
|
296
306
|
const port = openPorts.find((n) => n === 9222 || n === 9333);
|
|
297
307
|
if (port) { args.push('--browserUrl', `http://127.0.0.1:${port}`); return { args, mode: `attached:${port}`, detail: `browser listening on ${port}` }; }
|
|
298
308
|
return { args, mode: 'own-profile', detail: '~/.cache/chrome-devtools-mcp/chrome-profile' };
|
|
@@ -361,10 +371,11 @@ async function connectOne(cfg, log) {
|
|
|
361
371
|
*/
|
|
362
372
|
async function ensureBraveServer(log, home) {
|
|
363
373
|
if (!started || serversUp.includes('brave-devtools')) return false;
|
|
364
|
-
const
|
|
374
|
+
const brave = readActive(path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser'));
|
|
375
|
+
const bravePort = brave.port;
|
|
365
376
|
if (!bravePort || !(await portOpen(bravePort))) return false;
|
|
366
|
-
const cfg = { name: 'brave-devtools', command: 'npx', args: ['-y', 'chrome-devtools-mcp@latest',
|
|
367
|
-
log(`cursor-backend: mcp brave-devtools Brave appeared on ${bravePort} — attaching`);
|
|
377
|
+
const cfg = { name: 'brave-devtools', command: 'npx', args: ['-y', 'chrome-devtools-mcp@latest', ...braveAttachArgs(brave)], url: '' };
|
|
378
|
+
log(`cursor-backend: mcp brave-devtools Brave appeared on ${bravePort} — attaching (${brave.ws ? 'wsEndpoint' : 'browserUrl'})`);
|
|
368
379
|
try {
|
|
369
380
|
const got = await connectOne(cfg, log);
|
|
370
381
|
serversUp.push(cfg.name);
|
package/lib/xclaims.js
CHANGED
|
@@ -106,19 +106,27 @@ export function listClaims({ home = os.homedir(), now = Date.now() } = {}) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* X's composer is a Draft-style contenteditable
|
|
110
|
-
* newline (
|
|
111
|
-
* "corex402")
|
|
112
|
-
*
|
|
109
|
+
* X's composer is a Draft-style contenteditable. Two things measured 2026-09-01:
|
|
110
|
+
* 1. one `fill` flattens every newline ("openzoo.fun/core" + "x402 · PAID…"
|
|
111
|
+
* fused into "corex402");
|
|
112
|
+
* 2. `el.focus()` + keyboard typing puts text in the DOM but NOT in React's
|
|
113
|
+
* state — the Reply button stays disabled and the draft is wiped on the
|
|
114
|
+
* next render. Only a REAL mouse click on the box initialises Draft.
|
|
115
|
+
* So: probe the box's rect, click_at its centre, type line / Enter / line,
|
|
116
|
+
* then read back both the text and whether Reply is enabled.
|
|
113
117
|
*/
|
|
114
118
|
export const X_COMPOSER_SELECTOR = '[data-testid="tweetTextarea_0"], [data-testid="tweetTextarea_0RichTextInputContainer"] [contenteditable="true"], div[role="textbox"][contenteditable="true"]';
|
|
115
|
-
export
|
|
119
|
+
export const X_REPLY_BUTTON_SELECTOR = '[data-testid="tweetButtonInline"], [data-testid="tweetButton"]';
|
|
120
|
+
export const composeProbeScript = () => `() => { const el = document.querySelector(${JSON.stringify(X_COMPOSER_SELECTOR)}); if (!el) return { ok: false, error: 'no composer on this page — open the tweet and click Reply first' }; el.scrollIntoView({ block: 'center' }); const r = el.getBoundingClientRect(); return { ok: true, x: Math.round(r.left + Math.min(r.width / 2, 120)), y: Math.round(r.top + Math.min(r.height / 2, 20)), w: Math.round(r.width), h: Math.round(r.height) }; }`;
|
|
121
|
+
export const composeReadbackScript = () => `() => { const el = document.querySelector(${JSON.stringify(X_COMPOSER_SELECTOR)}); const b = document.querySelector(${JSON.stringify(X_REPLY_BUTTON_SELECTOR)}); const t = el ? (el.innerText || el.textContent || '') : ''; const dis = b ? (b.disabled || b.getAttribute('aria-disabled') === 'true') : true; return { ok: !!el, lines: t.split('\\n').filter(Boolean).length, replyEnabled: !dis, text: t.slice(0, 600) }; }`;
|
|
122
|
+
export function composePlan(text, rect) {
|
|
116
123
|
const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
|
|
117
|
-
const ops = [
|
|
124
|
+
const ops = [];
|
|
125
|
+
if (rect && Number.isFinite(rect.x) && Number.isFinite(rect.y)) ops.push({ tool: 'click_at', args: { x: rect.x, y: rect.y } });
|
|
118
126
|
lines.forEach((line, i) => {
|
|
119
127
|
if (i) ops.push({ tool: 'press_key', args: { key: 'Enter' } });
|
|
120
128
|
if (line) ops.push({ tool: 'type_text', args: { text: line } });
|
|
121
129
|
});
|
|
122
|
-
ops.push({ tool: 'evaluate_script', args: { function:
|
|
130
|
+
ops.push({ tool: 'evaluate_script', args: { function: composeReadbackScript() } });
|
|
123
131
|
return { lines: lines.length, ops };
|
|
124
132
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.68",
|
|
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",
|