chromeflow 0.9.11 → 0.9.12

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 +26 -11
  2. package/package.json +1 -1
@@ -25554,7 +25554,13 @@ ${r.body_text}` : "";
25554
25554
  function registerFlowTools(server, bridge) {
25555
25555
  server.tool(
25556
25556
  "click_element",
25557
- `Click an interactive element by its visible text or aria-label. Optionally pass an until_* clause to verify the click took effect:
25557
+ `Click an interactive element by its visible text/aria-label (textHint) OR by direct CSS selector (selector). Pass exactly one.
25558
+
25559
+ \`textHint\` mode: fuzzy-rank against visible text, aria-label, button content. Ranks visible candidates ahead of hidden.
25560
+
25561
+ \`selector\` mode: pierces open AND closed shadow roots via queryAllDeep. Use when the target has no visible text (icon buttons, custom-element placeholders like Reddit's collapsed comment composer, drop-zone overlays). Skips the textHint matcher entirely. \`nth\` still picks the Nth match.
25562
+
25563
+ Optionally pass an until_* clause to verify the click took effect:
25558
25564
  - until_selector \u2014 CSS selector that should appear after the click
25559
25565
  - until_url_contains \u2014 substring that should appear in the URL (requires an actual URL change if the substring was already in the pre-click URL)
25560
25566
  - until_text_contains \u2014 substring that should appear in page text
@@ -25563,14 +25569,17 @@ function registerFlowTools(server, bridge) {
25563
25569
 
25564
25570
  Returns {success, message, before_url, after_url, navigated}. \`navigated\` is true when the post-click URL differs from the pre-click URL \u2014 surfaces silent redirects without a second list_tabs call. Refuses to click 0\xD70 elements and now ranks visible candidates above hidden when text/aria match; when forced to refuse a hidden element it surfaces the next visible candidate in the error message.
25565
25571
 
25566
- Scope matching with \`within_selector\` or \`near_text\` restricts where matches are searched \u2014 useful for long forms with repeated labels per section (e.g. one "Minor Issue(s)" radio per evaluation axis). \`within_selector\` is a CSS selector; \`near_text\` finds the nearest container whose heading starts with the given text.
25572
+ Scope matching with \`within_selector\` or \`near_text\` restricts where matches are searched \u2014 useful for long forms with repeated labels per section (e.g. one "Approve" radio per row). \`within_selector\` is a CSS selector; \`near_text\` finds the nearest container whose heading starts with the given text.
25567
25573
 
25568
25574
  Shadow DOM (open AND closed) is pierced by default via chrome.dom.openOrClosedShadowRoot \u2014 Reddit faceplate-* / r-post-form-submit-button / web-component-heavy SPAs no longer need manual deepFind recipes.
25569
25575
 
25570
25576
  ANTI-BOT SUBMIT CEILING \u2014 synthetic clicks on social/auth platforms (Reddit, X / Twitter, GitHub device-code, mcp.so) are silently rejected by isTrusted-aware form validators and CSRF/reCAPTCHA gates. Pass \`expect_submit: true\` to detect this case (returns success=false with "submit silently rejected" when no signal fires within 4s). For confirmed anti-bot sites, do NOT retry \u2014 pre-fill the form, then highlight the submit button and call wait_for_click so a real human gesture fires the submission.`,
25571
25577
  {
25572
- textHint: external_exports.string().describe(
25573
- "The visible label of the button or link (e.g. 'Save product', 'Continue', 'Add a product', 'Create')"
25578
+ textHint: external_exports.string().optional().describe(
25579
+ "The visible label of the button or link (e.g. 'Save product', 'Continue', 'Add a product', 'Create'). Exactly one of textHint or selector must be set."
25580
+ ),
25581
+ selector: external_exports.string().optional().describe(
25582
+ `CSS selector for the element to click (e.g. 'faceplate-textarea-input', '#open-composer', 'button[aria-label="More options"]'). Pierces open AND closed shadow roots via queryAllDeep. Use when the target has no usable text. Exactly one of textHint or selector must be set.`
25574
25583
  ),
25575
25584
  nth: external_exports.number().int().min(1).optional().describe("Which match to click when multiple elements share the same label (1 = first/topmost, default 1). Visible candidates are ranked above hidden, so a hidden flair-dropdown won't claim nth=1 over the visible submit button."),
25576
25585
  until_selector: external_exports.string().optional().describe('Wait until this CSS selector appears on the page after the click (e.g. ".success-toast"). Returns success=false if it does not appear within until_timeout_ms.'),
@@ -25579,16 +25588,22 @@ ANTI-BOT SUBMIT CEILING \u2014 synthetic clicks on social/auth platforms (Reddit
25579
25588
  until_url_changes: external_exports.boolean().optional().describe('Wait until the URL changes after the click \u2014 for navigating submits whose destination URL is unknown ahead of time. Succeeds on any change away from the pre-click URL. Combine with until_url_contains for "must change AND must contain X".'),
25580
25589
  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
25590
  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
- 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.`),
25591
+ 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("Approve", nth=1, within_selector="#section-b"). Returns success=false with scope_missed=true if the selector does not match.`),
25583
25592
  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
25593
  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.`)
25585
25594
  },
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 }) => {
25595
+ async ({ textHint, selector, nth, until_selector, until_url_contains, until_text_contains, until_url_changes, until_timeout_ms, expect_submit, within_selector, near_text, try_fiber }) => {
25596
+ if (!textHint && !selector || textHint && selector) {
25597
+ return {
25598
+ content: [{ type: "text", text: "click_element requires exactly one of textHint or selector" }]
25599
+ };
25600
+ }
25601
+ const targetLabel = textHint ?? `selector="${selector}"`;
25587
25602
  const wsTimeout = Math.max(3e4, (until_timeout_ms ?? 0) + 1e4);
25588
25603
  let response;
25589
25604
  try {
25590
25605
  response = await bridge.request(
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 },
25606
+ { type: "click_element", textHint, selector, nth, until_selector, until_url_contains, until_text_contains, until_url_changes, until_timeout_ms, expect_submit, within_selector, near_text, try_fiber },
25592
25607
  wsTimeout
25593
25608
  );
25594
25609
  } catch (err) {
@@ -25606,14 +25621,14 @@ Current URL: ${activeTab.url}`;
25606
25621
  content: [
25607
25622
  {
25608
25623
  type: "text",
25609
- text: `Could not confirm click on "${textHint}": ${errMsg}. The click MAY have already fired \u2014 the page just took longer than ${wsTimeout}ms to respond. Verify with get_page_text or wait_for_selector before retrying. Re-clicking can toggle the wrong way on React-controlled radios.${stateLine}`
25624
+ text: `Could not confirm click on "${targetLabel}": ${errMsg}. The click MAY have already fired \u2014 the page just took longer than ${wsTimeout}ms to respond. Verify with get_page_text or wait_for_selector before retrying. Re-clicking can toggle the wrong way on React-controlled radios.${stateLine}`
25610
25625
  }
25611
25626
  ]
25612
25627
  };
25613
25628
  }
25614
25629
  return {
25615
25630
  content: [
25616
- { type: "text", text: `Could not click "${textHint}": ${errMsg}` }
25631
+ { type: "text", text: `Could not click "${targetLabel}": ${errMsg}` }
25617
25632
  ]
25618
25633
  };
25619
25634
  }
@@ -25635,7 +25650,7 @@ Current URL: ${activeTab.url}`;
25635
25650
  content: [
25636
25651
  {
25637
25652
  type: "text",
25638
- text: `Could not click "${textHint}": ${r.message}${navLine}${focusLine}`
25653
+ text: `Could not click "${targetLabel}": ${r.message}${navLine}${focusLine}`
25639
25654
  }
25640
25655
  ]
25641
25656
  };
@@ -25900,7 +25915,7 @@ ${lines.join("\n")}${shadowSection}` }] };
25900
25915
  }
25901
25916
 
25902
25917
  // packages/mcp-server/src/index.ts
25903
- var PACKAGE_VERSION = true ? "0.9.11" : "dev";
25918
+ var PACKAGE_VERSION = true ? "0.9.12" : "dev";
25904
25919
  main().catch((err) => {
25905
25920
  console.error("[chromeflow] Fatal error:", err);
25906
25921
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chromeflow",
3
- "version": "0.9.11",
3
+ "version": "0.9.12",
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",