openzoo 0.50.67 → 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.
@@ -2453,10 +2453,16 @@ export function selectedPageId(text) {
2453
2453
  }
2454
2454
  async function callBrowserForAgent(server, tool, args, agentId) {
2455
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
+ }
2456
2460
  if (pinned != null && !/^(new_page|list_pages|select_page|close_page)$/.test(tool)) {
2457
2461
  try { await callHostMcp(`${server}__select_page`, { pageId: pinned }); } catch { /* page gone; fall through */ }
2458
2462
  }
2459
- return callHostMcp(`${server}__${tool}`, args);
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;
2460
2466
  }
2461
2467
  async function runLocalTool(name, args, log, ctx = {}) {
2462
2468
  try {
@@ -2595,16 +2601,21 @@ async function runLocalTool(name, args, log, ctx = {}) {
2595
2601
  }
2596
2602
  if (name === 'x_compose') {
2597
2603
  const server = String(args.browser || 'chrome').toLowerCase() === 'brave' ? 'brave-devtools' : 'chrome-devtools';
2598
- const plan = xclaims.composePlan(args.text);
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);
2599
2611
  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) });
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 });
2605
2617
  }
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' });
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' });
2608
2619
  }
2609
2620
  }
2610
2621
  if (hostMcpHas(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: 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.
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 function composePlan(text) {
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 = [{ 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 }; }` } }];
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: `() => { 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) }; }` } });
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.67",
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",