openzoo 0.50.67 → 0.50.69

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.
@@ -1836,9 +1836,12 @@ const MODEL_ALIASES = {
1836
1836
  opus: 'anthropic/claude-opus-5',
1837
1837
  'opus-5': 'anthropic/claude-opus-5',
1838
1838
  sonnet: 'anthropic/claude-sonnet-5',
1839
- grok: 'x-ai/grok-4.6',
1840
- 'grok-4': 'x-ai/grok-4.6',
1841
- 'grok-4.6': 'x-ai/grok-4.6',
1839
+ // BARE grok-4.6 is the bazaar (x402 door) row; x-ai/grok-4.6 is OpenRouter's
1840
+ // and drains OpenRouter credits. Every spelling lands on the bare id.
1841
+ grok: 'grok-4.6',
1842
+ 'grok-4': 'grok-4.6',
1843
+ 'grok-4.6': 'grok-4.6',
1844
+ 'x-ai/grok-4.6': 'grok-4.6',
1842
1845
  glm: 'zai-org/glm-5.3-flash',
1843
1846
  'glm-5': 'zai-org/glm-5.3-flash',
1844
1847
  'glm-5.3': 'zai-org/glm-5.3-flash',
@@ -1872,10 +1875,14 @@ async function resolveModelId(raw) {
1872
1875
  return ids.find((id) => id === s) || ids.find((id) => id.toLowerCase() === lower) || null;
1873
1876
  } catch { return null; }
1874
1877
  }
1878
+ export function canonicalZooModel(id) {
1879
+ const s = String(id || '').trim();
1880
+ return MODEL_ALIASES[s.toLowerCase()] || s;
1881
+ }
1875
1882
  function currentModel(agentId) {
1876
- return agentModels.get(agentId)
1883
+ return canonicalZooModel(agentModels.get(agentId)
1877
1884
  || process.env.OPENZOO_DEFAULT_MODEL
1878
- || DEFAULT_ZOO_MODEL;
1885
+ || DEFAULT_ZOO_MODEL);
1879
1886
  }
1880
1887
 
1881
1888
  const execFileAsync = promisify(execFile);
@@ -2453,10 +2460,16 @@ export function selectedPageId(text) {
2453
2460
  }
2454
2461
  async function callBrowserForAgent(server, tool, args, agentId) {
2455
2462
  const pinned = pinnedPage(agentId, server);
2463
+ if (pinned != null && tool === 'select_page' && Number(args?.pageId) !== Number(pinned)) {
2464
+ // A pinned bot stays on its own tab. Answer with its tab, do not switch.
2465
+ return callHostMcp(`${server}__select_page`, { pageId: pinned });
2466
+ }
2456
2467
  if (pinned != null && !/^(new_page|list_pages|select_page|close_page)$/.test(tool)) {
2457
2468
  try { await callHostMcp(`${server}__select_page`, { pageId: pinned }); } catch { /* page gone; fall through */ }
2458
2469
  }
2459
- return callHostMcp(`${server}__${tool}`, args);
2470
+ const out = await callHostMcp(`${server}__${tool}`, args);
2471
+ if (tool === 'new_page') { const pid = selectedPageId(out); if (pid != null) pinPage(agentId, server, pid); }
2472
+ return out;
2460
2473
  }
2461
2474
  async function runLocalTool(name, args, log, ctx = {}) {
2462
2475
  try {
@@ -2595,16 +2608,21 @@ async function runLocalTool(name, args, log, ctx = {}) {
2595
2608
  }
2596
2609
  if (name === 'x_compose') {
2597
2610
  const server = String(args.browser || 'chrome').toLowerCase() === 'brave' ? 'brave-devtools' : 'chrome-devtools';
2598
- const plan = xclaims.composePlan(args.text);
2611
+ for (const t of ['evaluate_script', 'click_at', 'type_text', 'press_key']) {
2612
+ if (!hostMcpHas(`${server}__${t}`)) return JSON.stringify({ ok: false, error: `${server}__${t} not attached` });
2613
+ }
2614
+ const probeRaw = String(await callBrowserForAgent(server, 'evaluate_script', { function: xclaims.composeProbeScript() }, ctx.agentId));
2615
+ const probe = (() => { try { return JSON.parse(probeRaw.match(/\{[\s\S]*\}/)?.[0] || '{}'); } catch { return {}; } })();
2616
+ if (!probe.ok) return JSON.stringify({ ok: false, error: probe.error || `composer probe failed: ${probeRaw.slice(0, 200)}` });
2617
+ const plan = xclaims.composePlan(args.text, probe);
2599
2618
  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) });
2619
+ for (const op of plan.ops) last = String(await callBrowserForAgent(server, op.tool, op.args, ctx.agentId));
2620
+ const back = (() => { try { return JSON.parse(last.match(/\{[\s\S]*\}/)?.[0] || '{}'); } catch { return {}; } })();
2621
+ log(`cursor-backend: x_compose ${server} lines=${plan.lines} back=${back.lines} replyEnabled=${back.replyEnabled}`);
2622
+ if (!back.replyEnabled) {
2623
+ 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
2624
  }
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' });
2625
+ 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
2626
  }
2609
2627
  }
2610
2628
  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.69",
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",