chromeflow 0.9.9 → 0.9.11

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.
Files changed (2) hide show
  1. package/bin/chromeflow.mjs +13 -11
  2. package/package.json +1 -1
@@ -24782,7 +24782,7 @@ After tabs.onUpdated fires status=complete, chromeflow also runs a 6s settle che
24782
24782
  url: external_exports.string().url().describe("The URL to navigate to"),
24783
24783
  new_tab: external_exports.boolean().optional().describe("Open in a new tab instead of replacing the current one (default false)"),
24784
24784
  background: external_exports.boolean().optional().describe("If new_tab=true, do not switch focus to the new tab. Default false. Ignored when new_tab is false."),
24785
- expect_selector: external_exports.string().optional().describe("CSS selector for an element that must be present before the page is considered settled. The settle check waits up to 6s for it; if it never appears, the response carries expect_selector_appeared:false so you can detect dead-spinner routes (e.g. Outlier /en/expert/tasks).")
24785
+ expect_selector: external_exports.string().optional().describe("CSS selector for an element that must be present before the page is considered settled. The settle check waits up to 6s for it; if it never appears, the response carries expect_selector_appeared:false so you can detect dead-spinner routes (SPAs that leave a permanent spinner when the underlying API request dies).")
24786
24786
  },
24787
24787
  async ({ url, new_tab, background, expect_selector }) => {
24788
24788
  const block = isBlockedUrl(url);
@@ -24802,7 +24802,7 @@ After tabs.onUpdated fires status=complete, chromeflow also runs a 6s settle che
24802
24802
  if (r.stuck_spinner) {
24803
24803
  text += `
24804
24804
 
24805
- \u26A0 stuck_spinner: true \u2014 page settled with a visible spinner (${r.spinner_selector ?? "unknown selector"}) still on screen after 6s. Current URL: ${r.current_url ?? url}. The route may be dead (Outlier /en/expert/tasks pattern) \u2014 navigate elsewhere instead of reloading, or wait and try get_page_text to see if it ever recovers.`;
24805
+ \u26A0 stuck_spinner: true \u2014 page settled with a visible spinner (${r.spinner_selector ?? "unknown selector"}) still on screen after 6s. Current URL: ${r.current_url ?? url}. The route may be dead (some SPAs leave a permanent spinner when the underlying API request fails) \u2014 navigate elsewhere instead of reloading, or wait and try get_page_text to see if it ever recovers.`;
24806
24806
  }
24807
24807
  if (expect_selector && r.expect_selector_appeared === false) {
24808
24808
  text += `
@@ -25076,7 +25076,7 @@ ${lines.join("\n")}${r.warning ?? ""}${captchaLine}${oauthLine}` }] };
25076
25076
  {
25077
25077
  text: external_exports.string().describe("The text to type into the focused element"),
25078
25078
  into_selector: external_exports.string().optional().describe(
25079
- "CSS selector for the element to focus before typing (shadow-piercing \u2014 resolves selectors that find_text returns for closed-shadow-root content, e.g. Outlier-style Radix portals). When omitted, types into whatever is currently focused (the caller is responsible for focusing first via click_element)."
25079
+ "CSS selector for the element to focus before typing (shadow-piercing \u2014 resolves selectors that find_text returns for closed-shadow-root content, e.g. Radix portals). When omitted, types into whatever is currently focused (the caller is responsible for focusing first via click_element)."
25080
25080
  ),
25081
25081
  clear_first: external_exports.boolean().optional().describe(
25082
25082
  "Only with into_selector: run document.execCommand('selectAll') + 'delete' on the focused element before typing. Use to overwrite tiptap / ProseMirror editors and similar contenteditable surfaces in one call."
@@ -25580,14 +25580,15 @@ ANTI-BOT SUBMIT CEILING \u2014 synthetic clicks on social/auth platforms (Reddit
25580
25580
  until_timeout_ms: external_exports.number().int().min(500).optional().describe("How long to wait for the until-condition, in milliseconds (default 5000). Only used if one of until_* is set."),
25581
25581
  expect_submit: external_exports.boolean().optional().describe(`Broad anti-bot detector. After the click, watch up to 4s for ANY of: URL change, [role=alert] / [data-sonner-toast] / .toast / .notification / aria-live appearance, [role=dialog] / [aria-modal=true] appearance. Returns success=false with "submit silently rejected (likely anti-bot)" when no signal fires. Use on form submits when the until_* destination isn't known. Ignored when any until_* is set (those are more specific).`),
25582
25582
  within_selector: external_exports.string().optional().describe(`Limit candidate matches to this CSS selector's subtree (mirrors find_text's scope_selector). Use to scope nth-counting to one section of a long form: click_element("Minor Issue(s)", nth=1, within_selector="#response-b-style"). Returns success=false with scope_missed=true if the selector does not match.`),
25583
- near_text: external_exports.string().optional().describe("Find the nearest container whose heading starts with this text, then scope candidates to that container's subtree. Ignored when within_selector is set. Useful when the target section has no stable CSS selector but the heading is unique.")
25583
+ near_text: external_exports.string().optional().describe("Find the nearest container whose heading starts with this text, then scope candidates to that container's subtree. Ignored when within_selector is set. Useful when the target section has no stable CSS selector but the heading is unique."),
25584
+ try_fiber: external_exports.boolean().optional().describe(`Opt-in last-resort fallback when silently_rejected fires. After the 1500ms activity probe reports zero activity, chromeflow walks the React fiber tree from the matched element (up to 12 levels), finds the nearest \`__reactProps$.onClick\` prop, and invokes it with a minimal synthetic event. Useful on React-heavy SPAs whose action buttons pass through isTrusted=true checks even on CDP events. Returns fiber_attempted=true in the response when the path was taken. Do NOT default to this \u2014 fiber-prop walking is undocumented and may misbehave on mangled production builds. Reserve for repeat silently_rejected on a known-safe React site.`)
25584
25585
  },
25585
- async ({ textHint, nth, until_selector, until_url_contains, until_text_contains, until_url_changes, until_timeout_ms, expect_submit, within_selector, near_text }) => {
25586
+ async ({ textHint, nth, until_selector, until_url_contains, until_text_contains, until_url_changes, until_timeout_ms, expect_submit, within_selector, near_text, try_fiber }) => {
25586
25587
  const wsTimeout = Math.max(3e4, (until_timeout_ms ?? 0) + 1e4);
25587
25588
  let response;
25588
25589
  try {
25589
25590
  response = await bridge.request(
25590
- { type: "click_element", textHint, nth, until_selector, until_url_contains, until_text_contains, until_url_changes, until_timeout_ms, expect_submit, within_selector, near_text },
25591
+ { type: "click_element", textHint, nth, until_selector, until_url_contains, until_text_contains, until_url_changes, until_timeout_ms, expect_submit, within_selector, near_text, try_fiber },
25591
25592
  wsTimeout
25592
25593
  );
25593
25594
  } catch (err) {
@@ -25682,7 +25683,7 @@ Clicked element: <${r.target.tag}>${r.target.text ? ` "${r.target.text}"` : ""}
25682
25683
  );
25683
25684
  server.tool(
25684
25685
  "wait_for",
25685
- `Wait for one of: a CSS selector to appear, a text substring to appear, or an existing element's subtree to mutate. Pass exactly one of \`selector\`, \`text\`, or \`change_in\`. Pierces open shadow roots. Pass \`shadow_root: true\` when waiting for the host's shadowRoot to attach (post-SPA-navigation hydration). \`scope_selector\` limits text-mode search; \`regex: true\` interprets text as a case-insensitive regex; \`frame: "iframe.selector"\` waits inside a same-origin iframe (text mode).`,
25686
+ `Wait for one of: a CSS selector to appear, a text substring to appear, or an existing element's subtree to mutate. Pass exactly one of \`selector\`, \`text\`, or \`change_in\`. Pierces open AND closed shadow roots (text \`scope_selector\` pierces too). Pass \`shadow_root: true\` when waiting for the host's shadowRoot to attach (post-SPA-navigation hydration). \`scope_selector\` limits text-mode search; \`regex: true\` interprets text as a case-insensitive regex; \`frame: "iframe.selector"\` waits inside a same-origin iframe (text mode). Pass \`since: "now"\` in text mode to skip the initial check and only resolve on text appearing in a NEW DOM mutation \u2014 defeats the "stale instruction panels still in DOM" false-positive.`,
25686
25687
  {
25687
25688
  selector: external_exports.string().optional().describe("CSS selector to wait for."),
25688
25689
  text: external_exports.string().optional().describe("Text substring (or regex with regex=true) to wait for."),
@@ -25690,14 +25691,15 @@ Clicked element: <${r.target.tag}>${r.target.text ? ` "${r.target.text}"` : ""}
25690
25691
  timeout_ms: external_exports.number().int().optional().describe("Max ms to wait (default 30000)."),
25691
25692
  poll_interval_ms: external_exports.number().int().optional().describe("Selector-mode poll interval (default 500). Set to 15000 for slow server-side jobs."),
25692
25693
  shadow_root: external_exports.boolean().optional().describe("Selector mode: require the matched host to have an attached shadowRoot. Default false."),
25693
- scope_selector: external_exports.string().optional().describe("Text mode: limit search to this CSS selector's subtree."),
25694
+ scope_selector: external_exports.string().optional().describe("Text mode: limit search to this CSS selector's subtree. Pierces shadow roots."),
25694
25695
  regex: external_exports.boolean().optional().describe("Text mode: interpret query as a case-insensitive regex."),
25695
25696
  frame: external_exports.string().optional().describe("Same-origin iframe CSS selector to wait inside (text mode)."),
25697
+ since: external_exports.enum(["now"]).optional().describe(`Text mode: gate on a NEW mutation. Skips the initial check so already-present matches don't short-circuit. Use when the page keeps stale text in the DOM after a route change (e.g. stacked instruction panels) and you need to wait for the next render.`),
25696
25698
  settle_ms: external_exports.number().int().optional().describe("change_in mode: ms to wait after the first mutation for batching (default 150)."),
25697
25699
  max_chars: external_exports.number().int().min(50).optional().describe("change_in mode: cap the returned text content (default 1000). Chat-style mutations can dump huge text; agents that need more should opt in explicitly.")
25698
25700
  },
25699
25701
  async (args) => {
25700
- const { selector, text, change_in, timeout_ms, poll_interval_ms, shadow_root, scope_selector, regex, frame, settle_ms, max_chars } = args;
25702
+ const { selector, text, change_in, timeout_ms, poll_interval_ms, shadow_root, scope_selector, regex, frame, since, settle_ms, max_chars } = args;
25701
25703
  const set = [selector, text, change_in].filter((v) => v !== void 0 && v !== null && v !== "").length;
25702
25704
  if (set !== 1) {
25703
25705
  return { content: [{ type: "text", text: "wait_for: pass exactly one of selector, text, or change_in." }] };
@@ -25713,7 +25715,7 @@ Clicked element: <${r.target.tag}>${r.target.text ? ` "${r.target.text}"` : ""}
25713
25715
  }
25714
25716
  if (text !== void 0) {
25715
25717
  const response2 = await bridge.request(
25716
- { type: "wait_for_text", query: text, timeout_ms: timeoutMs, scope_selector, regex, frame },
25718
+ { type: "wait_for_text", query: text, timeout_ms: timeoutMs, scope_selector, regex, frame, since },
25717
25719
  timeoutMs + 5e3
25718
25720
  );
25719
25721
  const r2 = response2;
@@ -25898,7 +25900,7 @@ ${lines.join("\n")}${shadowSection}` }] };
25898
25900
  }
25899
25901
 
25900
25902
  // packages/mcp-server/src/index.ts
25901
- var PACKAGE_VERSION = true ? "0.9.9" : "dev";
25903
+ var PACKAGE_VERSION = true ? "0.9.11" : "dev";
25902
25904
  main().catch((err) => {
25903
25905
  console.error("[chromeflow] Fatal error:", err);
25904
25906
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chromeflow",
3
- "version": "0.9.9",
3
+ "version": "0.9.11",
4
4
  "description": "MCP server for chromeflow — lets Claude Code or Codex CLI drive your real Chrome browser with sessions intact. Plugin install recommended; npx chromeflow for manual MCP wiring.",
5
5
  "type": "module",
6
6
  "main": "./bin/chromeflow.mjs",