chromeflow 0.9.10 → 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.
- package/bin/chromeflow.mjs +29 -14
- package/package.json +1 -1
package/bin/chromeflow.mjs
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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.
|
|
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."
|
|
@@ -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
|
|
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 "
|
|
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("
|
|
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 "${
|
|
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 "${
|
|
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 "${
|
|
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.
|
|
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.
|
|
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",
|