openzoo 0.50.65 → 0.50.67

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.
@@ -2320,6 +2320,34 @@ const LOCAL_TOOLS = [
2320
2320
  parameters: { type: 'object', properties: {} },
2321
2321
  },
2322
2322
  },
2323
+ {
2324
+ type: 'function',
2325
+ function: {
2326
+ name: 'x_compose',
2327
+ description: 'Type reply text into the X composer on the current page of the given browser, LINE BY LINE with real Enter keys so newlines survive (fill flattens them). Click/focus the reply box first (or open the tweet). Then click the Reply button yourself. browser: chrome or brave.',
2328
+ parameters: {
2329
+ type: 'object',
2330
+ properties: { browser: { type: 'string', enum: ['chrome', 'brave'] }, text: { type: 'string' } },
2331
+ required: ['browser', 'text'],
2332
+ },
2333
+ },
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
+ },
2323
2351
  ];
2324
2352
 
2325
2353
  export const LOCAL_TOOL_NAMES = LOCAL_TOOLS.map((t) => t.function.name);
@@ -2407,6 +2435,29 @@ async function deliverAgentMessage({ fromId, to, text, parsed = {}, log }) {
2407
2435
  }
2408
2436
  }
2409
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 && !/^(new_page|list_pages|select_page|close_page)$/.test(tool)) {
2457
+ try { await callHostMcp(`${server}__select_page`, { pageId: pinned }); } catch { /* page gone; fall through */ }
2458
+ }
2459
+ return callHostMcp(`${server}__${tool}`, args);
2460
+ }
2410
2461
  async function runLocalTool(name, args, log, ctx = {}) {
2411
2462
  try {
2412
2463
  if (name === 'read_file') {
@@ -2525,8 +2576,40 @@ async function runLocalTool(name, args, log, ctx = {}) {
2525
2576
  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 }));
2526
2577
  if (name === 'x_release') return JSON.stringify(xclaims.releaseTweet({ tweet: args.tweet || args.id, by: me.id }));
2527
2578
  if (name === 'x_claims') return JSON.stringify(xclaims.listClaims());
2579
+ if (name === 'x_open') {
2580
+ const server = String(args.browser || 'chrome').toLowerCase() === 'brave' ? 'brave-devtools' : 'chrome-devtools';
2581
+ if (!hostMcpHas(`${server}__new_page`)) return JSON.stringify({ ok: false, error: `${server} not attached` });
2582
+ const out = String(await callHostMcp(`${server}__new_page`, { url: String(args.url || 'https://x.com') }));
2583
+ const pid = selectedPageId(out);
2584
+ if (pid == null) return JSON.stringify({ ok: false, error: 'could not read the new page id', raw: out.slice(0, 300) });
2585
+ pinPage(ctx.agentId, server, pid);
2586
+ return JSON.stringify({ ok: true, browser: server, pageId: pid, note: 'pinned: your browser calls now go to this tab' });
2587
+ }
2588
+ if (name === 'x_close') {
2589
+ const server = String(args.browser || 'chrome').toLowerCase() === 'brave' ? 'brave-devtools' : 'chrome-devtools';
2590
+ const pid = pinnedPage(ctx.agentId, server);
2591
+ if (pid == null) return JSON.stringify({ ok: true, closed: false });
2592
+ try { await callHostMcp(`${server}__close_page`, { pageId: pid }); } catch (e) { pinPage(ctx.agentId, server, null); return JSON.stringify({ ok: false, error: e.message }); }
2593
+ pinPage(ctx.agentId, server, null);
2594
+ return JSON.stringify({ ok: true, closed: true, pageId: pid });
2595
+ }
2596
+ if (name === 'x_compose') {
2597
+ const server = String(args.browser || 'chrome').toLowerCase() === 'brave' ? 'brave-devtools' : 'chrome-devtools';
2598
+ const plan = xclaims.composePlan(args.text);
2599
+ let last = '';
2600
+ for (const op of plan.ops) {
2601
+ const tname = `${server}__${op.tool}`;
2602
+ if (!hostMcpHas(tname)) return JSON.stringify({ ok: false, error: `${tname} not attached` });
2603
+ last = String(await callBrowserForAgent(server, op.tool, op.args, ctx.agentId));
2604
+ if (op.tool === 'evaluate_script' && /"ok":\s*false/.test(last)) return JSON.stringify({ ok: false, error: last.slice(0, 300) });
2605
+ }
2606
+ log(`cursor-backend: x_compose ${server} lines=${plan.lines}`);
2607
+ 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' });
2608
+ }
2528
2609
  }
2529
2610
  if (hostMcpHas(name)) {
2611
+ const m = name.match(/^(chrome-devtools|brave-devtools)__(.+)$/);
2612
+ if (m) return await callBrowserForAgent(m[1], m[2], args, ctx.agentId);
2530
2613
  return await callHostMcp(name, args);
2531
2614
  }
2532
2615
  return `unknown tool ${name}`;
@@ -2685,7 +2768,9 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
2685
2768
  return brief ? `${who} Standing brief (persisted): ${brief}` : who;
2686
2769
  })(),
2687
2770
  `You HAVE local tools on the user's computer via ${via}.`,
2688
- '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.',
2771
+ '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.',
2772
+ '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.',
2773
+ '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.',
2689
2774
  '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.',
2690
2775
  '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.',
2691
2776
  'Grok Ship (software factory): ship_crew sets up Firstmate + a crewmate per repo. A crewmate ships with ship_launch_worker -> ship_status until pushed -> ship_review (fresh, diff-only) -> ship_open_pr only when gate.clean. Never merge; the human does.',
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 bravePort = readActivePort(path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser'));
217
- if (bravePort && !seen.has('brave-devtools')) {
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', '--browserUrl', `http://127.0.0.1:${bravePort}`],
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')[0].trim();
282
- const n = Number(first);
283
- return Number.isFinite(n) && n > 0 ? n : 0;
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('--browserUrl', `http://127.0.0.1:${bravePort}`); return { args, mode: 'real-brave', detail: `Brave DevToolsActivePort ${bravePort}` }; }
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 bravePort = readActivePort(path.join(home, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser'));
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', '--browserUrl', `http://127.0.0.1:${bravePort}`], url: '' };
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
@@ -104,3 +104,21 @@ export function listClaims({ home = os.homedir(), now = Date.now() } = {}) {
104
104
  }
105
105
  return { done, live, total: Object.keys(claims).length };
106
106
  }
107
+
108
+ /**
109
+ * X's composer is a Draft-style contenteditable: one `fill` flattens every
110
+ * newline (measured: "openzoo.fun/core" + "x402 · PAID…" fused into
111
+ * "corex402"). Real keystrokes work: type a line, press Enter, type the next.
112
+ * Pure planner — the host executes these against chrome-devtools / brave-devtools.
113
+ */
114
+ export const X_COMPOSER_SELECTOR = '[data-testid="tweetTextarea_0"], [data-testid="tweetTextarea_0RichTextInputContainer"] [contenteditable="true"], div[role="textbox"][contenteditable="true"]';
115
+ export function composePlan(text) {
116
+ const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
117
+ const ops = [{ tool: 'evaluate_script', args: { function: `() => { const el = document.querySelector(${JSON.stringify(X_COMPOSER_SELECTOR)}); if (!el) return { ok: false, error: 'no composer on this page' }; el.focus(); return { ok: true }; }` } }];
118
+ lines.forEach((line, i) => {
119
+ if (i) ops.push({ tool: 'press_key', args: { key: 'Enter' } });
120
+ if (line) ops.push({ tool: 'type_text', args: { text: line } });
121
+ });
122
+ ops.push({ tool: 'evaluate_script', args: { function: `() => { const el = document.querySelector(${JSON.stringify(X_COMPOSER_SELECTOR)}); const t = el ? (el.innerText || el.textContent || '') : ''; return { ok: !!el, lines: t.split('\\n').filter(Boolean).length, text: t.slice(0, 600) }; }` } });
123
+ return { lines: lines.length, ops };
124
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.65",
3
+ "version": "0.50.67",
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",