chromeflow 0.9.8 → 0.9.10
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 +99 -25
- package/package.json +1 -1
package/bin/chromeflow.mjs
CHANGED
|
@@ -24775,21 +24775,41 @@ function registerBrowserTools(server, bridge) {
|
|
|
24775
24775
|
"open_page",
|
|
24776
24776
|
`Navigate to a URL. By default reuses the active tab. Set new_tab=true to open alongside the current tab without losing it. After navigating, call get_page_text to read the page \u2014 do NOT take a screenshot.
|
|
24777
24777
|
|
|
24778
|
-
Set background=true (only with new_tab=true) to open the new tab WITHOUT switching focus to it. Use this when the current tab has a partially-filled form whose page auto-saves on focus loss (e.g. eBay seller listings) \u2014 switching away would trigger the auto-save and corrupt the in-progress draft
|
|
24778
|
+
Set background=true (only with new_tab=true) to open the new tab WITHOUT switching focus to it. Use this when the current tab has a partially-filled form whose page auto-saves on focus loss (e.g. eBay seller listings) \u2014 switching away would trigger the auto-save and corrupt the in-progress draft.
|
|
24779
|
+
|
|
24780
|
+
After tabs.onUpdated fires status=complete, chromeflow also runs a 6s settle check (document.readyState=complete, no visible spinner element, 250ms of mutation quiet). If a spinner is still visible at the end of the window, the response carries \`stuck_spinner: true\` with the matching selector \u2014 the canonical case is an SPA route that left a permanent .spinner-wrapper because the API request died. Set expect_selector to wait for a known-good element to appear before considering the page settled \u2014 the response carries \`expect_selector_appeared: false\` if it never showed up.`,
|
|
24779
24781
|
{
|
|
24780
24782
|
url: external_exports.string().url().describe("The URL to navigate to"),
|
|
24781
24783
|
new_tab: external_exports.boolean().optional().describe("Open in a new tab instead of replacing the current one (default false)"),
|
|
24782
|
-
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.")
|
|
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).")
|
|
24783
24786
|
},
|
|
24784
|
-
async ({ url, new_tab, background }) => {
|
|
24787
|
+
async ({ url, new_tab, background, expect_selector }) => {
|
|
24785
24788
|
const block = isBlockedUrl(url);
|
|
24786
24789
|
if (block.blocked) {
|
|
24787
24790
|
return { content: [{ type: "text", text: `open_page refused: ${block.reason}` }] };
|
|
24788
24791
|
}
|
|
24789
|
-
await bridge.request({
|
|
24790
|
-
|
|
24791
|
-
|
|
24792
|
-
|
|
24792
|
+
const response = await bridge.request({
|
|
24793
|
+
type: "navigate",
|
|
24794
|
+
url,
|
|
24795
|
+
newTab: new_tab ?? false,
|
|
24796
|
+
background: background ?? false,
|
|
24797
|
+
expect_selector
|
|
24798
|
+
});
|
|
24799
|
+
const r = response;
|
|
24800
|
+
const newTabBit = new_tab ? background ? " (new background tab)" : " (new tab)" : "";
|
|
24801
|
+
let text = `Navigated to ${url}${newTabBit}`;
|
|
24802
|
+
if (r.stuck_spinner) {
|
|
24803
|
+
text += `
|
|
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.`;
|
|
24806
|
+
}
|
|
24807
|
+
if (expect_selector && r.expect_selector_appeared === false) {
|
|
24808
|
+
text += `
|
|
24809
|
+
|
|
24810
|
+
\u26A0 expect_selector "${expect_selector}" never appeared within the 6s settle window. The page may be partially loaded or stuck.`;
|
|
24811
|
+
}
|
|
24812
|
+
return { content: [{ type: "text", text }] };
|
|
24793
24813
|
}
|
|
24794
24814
|
);
|
|
24795
24815
|
server.tool(
|
|
@@ -25052,16 +25072,25 @@ ${lines.join("\n")}${r.warning ?? ""}${captchaLine}${oauthLine}` }] };
|
|
|
25052
25072
|
);
|
|
25053
25073
|
server.tool(
|
|
25054
25074
|
"type_text",
|
|
25055
|
-
`Type text into the currently focused element via CDP keystrokes (produces isTrusted=true events). Use when fill_input fails because the page validates isTrusted (CodeMirror/Monaco/Ace editors, shadow DOM inputs, isTrusted-gated forms).
|
|
25075
|
+
`Type text into the currently focused element via CDP keystrokes (produces isTrusted=true events). Use when fill_input fails because the page validates isTrusted (CodeMirror/Monaco/Ace editors, shadow DOM inputs, isTrusted-gated forms). Pass \`into_selector\` to focus the target before typing (shadow-piercing CSS) \u2014 combined with \`clear_first: true\`, this collapses the old "wait_for_click \u2192 execute_script selectAll \u2192 type_text" pattern into a single call. Pass \`frame: "iframe.selector"\` to type into a same-origin iframe's first editable element.`,
|
|
25056
25076
|
{
|
|
25057
25077
|
text: external_exports.string().describe("The text to type into the focused element"),
|
|
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)."
|
|
25080
|
+
),
|
|
25081
|
+
clear_first: external_exports.boolean().optional().describe(
|
|
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."
|
|
25083
|
+
),
|
|
25058
25084
|
frame: external_exports.string().optional().describe(
|
|
25059
25085
|
"CSS selector for an iframe whose contents you want to type into (e.g. 'iframe.se-rte-frame__summary'). Same-origin only. Before typing, the first contenteditable/input inside the iframe is focused; after typing, input/change events are dispatched in the iframe's context."
|
|
25060
25086
|
)
|
|
25061
25087
|
},
|
|
25062
|
-
async ({ text, frame }) => {
|
|
25088
|
+
async ({ text, frame, into_selector, clear_first }) => {
|
|
25063
25089
|
const timeoutMs = Math.max(3e4, text.length * 90 + 15e3);
|
|
25064
|
-
const response = await bridge.request(
|
|
25090
|
+
const response = await bridge.request(
|
|
25091
|
+
{ type: "type_text", text, frame, into_selector, clear_first },
|
|
25092
|
+
timeoutMs
|
|
25093
|
+
);
|
|
25065
25094
|
const r = response;
|
|
25066
25095
|
return {
|
|
25067
25096
|
content: [{ type: "text", text: r.message ?? (r.success ? "Text typed successfully" : "Failed to type text") }]
|
|
@@ -25275,7 +25304,18 @@ Never use take_screenshot just to read page content \u2014 paginate with startIn
|
|
|
25275
25304
|
async ({ selector, startIndex }) => {
|
|
25276
25305
|
const response = await bridge.request({ type: "get_page_text", selector, startIndex });
|
|
25277
25306
|
if (response.type !== "page_text_response") throw new Error("Unexpected response");
|
|
25278
|
-
const
|
|
25307
|
+
const r = response;
|
|
25308
|
+
let text = r.text;
|
|
25309
|
+
if (r.selector_in_shadow) {
|
|
25310
|
+
text = `[note: selector "${selector}" matched inside a closed shadow root \u2014 chromeflow tools that pierce (get_page_text, find_text, click_element, fill_input) see it, but execute_script cannot. Don't drop to screenshots.]
|
|
25311
|
+
|
|
25312
|
+
` + text;
|
|
25313
|
+
}
|
|
25314
|
+
if (!selector && r.shadow_hosts_seen && r.shadow_hosts_seen > 0) {
|
|
25315
|
+
text = `[note: ${r.shadow_hosts_seen} shadow host${r.shadow_hosts_seen === 1 ? "" : "s"} detected on this page \u2014 call list_frames to see them. If execute_script returns an empty document, switch to find_text / get_page_text / click_element / fill_input \u2014 those pierce closed shadow roots.]
|
|
25316
|
+
|
|
25317
|
+
` + text;
|
|
25318
|
+
}
|
|
25279
25319
|
return {
|
|
25280
25320
|
content: [{ type: "text", text: text || "(no text found on page)" }]
|
|
25281
25321
|
};
|
|
@@ -25540,14 +25580,15 @@ ANTI-BOT SUBMIT CEILING \u2014 synthetic clicks on social/auth platforms (Reddit
|
|
|
25540
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."),
|
|
25541
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).`),
|
|
25542
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.`),
|
|
25543
|
-
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.`)
|
|
25544
25585
|
},
|
|
25545
|
-
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 }) => {
|
|
25546
25587
|
const wsTimeout = Math.max(3e4, (until_timeout_ms ?? 0) + 1e4);
|
|
25547
25588
|
let response;
|
|
25548
25589
|
try {
|
|
25549
25590
|
response = await bridge.request(
|
|
25550
|
-
{ 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 },
|
|
25551
25592
|
wsTimeout
|
|
25552
25593
|
);
|
|
25553
25594
|
} catch (err) {
|
|
@@ -25579,18 +25620,28 @@ Current URL: ${activeTab.url}`;
|
|
|
25579
25620
|
const r = response;
|
|
25580
25621
|
const navLine = r.navigated && r.after_url ? `
|
|
25581
25622
|
\u2192 Navigated: ${r.after_url}` : "";
|
|
25623
|
+
let focusLine = "";
|
|
25624
|
+
const f = r.focused_after ?? null;
|
|
25625
|
+
if (f && !["button", "a"].includes(f.tag)) {
|
|
25626
|
+
const idBit = f.id ? `#${f.id}` : "";
|
|
25627
|
+
const nameBit = f.name ? ` name="${f.name}"` : "";
|
|
25628
|
+
const aria = f.aria_label ? ` aria-label="${f.aria_label.slice(0, 30)}"` : "";
|
|
25629
|
+
const valueBit = f.value_preview ? ` value="${f.value_preview.slice(0, 30)}"` : "";
|
|
25630
|
+
focusLine = `
|
|
25631
|
+
\u2192 Focused: <${f.tag}${idBit}${nameBit}${aria}${valueBit}>`;
|
|
25632
|
+
}
|
|
25582
25633
|
if (!r.success) {
|
|
25583
25634
|
return {
|
|
25584
25635
|
content: [
|
|
25585
25636
|
{
|
|
25586
25637
|
type: "text",
|
|
25587
|
-
text: `Could not click "${textHint}": ${r.message}${navLine}`
|
|
25638
|
+
text: `Could not click "${textHint}": ${r.message}${navLine}${focusLine}`
|
|
25588
25639
|
}
|
|
25589
25640
|
]
|
|
25590
25641
|
};
|
|
25591
25642
|
}
|
|
25592
25643
|
return {
|
|
25593
|
-
content: [{ type: "text", text: `${r.message}${navLine}` }]
|
|
25644
|
+
content: [{ type: "text", text: `${r.message}${navLine}${focusLine}` }]
|
|
25594
25645
|
};
|
|
25595
25646
|
}
|
|
25596
25647
|
);
|
|
@@ -25632,7 +25683,7 @@ Clicked element: <${r.target.tag}>${r.target.text ? ` "${r.target.text}"` : ""}
|
|
|
25632
25683
|
);
|
|
25633
25684
|
server.tool(
|
|
25634
25685
|
"wait_for",
|
|
25635
|
-
`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.`,
|
|
25636
25687
|
{
|
|
25637
25688
|
selector: external_exports.string().optional().describe("CSS selector to wait for."),
|
|
25638
25689
|
text: external_exports.string().optional().describe("Text substring (or regex with regex=true) to wait for."),
|
|
@@ -25640,14 +25691,15 @@ Clicked element: <${r.target.tag}>${r.target.text ? ` "${r.target.text}"` : ""}
|
|
|
25640
25691
|
timeout_ms: external_exports.number().int().optional().describe("Max ms to wait (default 30000)."),
|
|
25641
25692
|
poll_interval_ms: external_exports.number().int().optional().describe("Selector-mode poll interval (default 500). Set to 15000 for slow server-side jobs."),
|
|
25642
25693
|
shadow_root: external_exports.boolean().optional().describe("Selector mode: require the matched host to have an attached shadowRoot. Default false."),
|
|
25643
|
-
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."),
|
|
25644
25695
|
regex: external_exports.boolean().optional().describe("Text mode: interpret query as a case-insensitive regex."),
|
|
25645
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.`),
|
|
25646
25698
|
settle_ms: external_exports.number().int().optional().describe("change_in mode: ms to wait after the first mutation for batching (default 150)."),
|
|
25647
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.")
|
|
25648
25700
|
},
|
|
25649
25701
|
async (args) => {
|
|
25650
|
-
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;
|
|
25651
25703
|
const set = [selector, text, change_in].filter((v) => v !== void 0 && v !== null && v !== "").length;
|
|
25652
25704
|
if (set !== 1) {
|
|
25653
25705
|
return { content: [{ type: "text", text: "wait_for: pass exactly one of selector, text, or change_in." }] };
|
|
@@ -25663,7 +25715,7 @@ Clicked element: <${r.target.tag}>${r.target.text ? ` "${r.target.text}"` : ""}
|
|
|
25663
25715
|
}
|
|
25664
25716
|
if (text !== void 0) {
|
|
25665
25717
|
const response2 = await bridge.request(
|
|
25666
|
-
{ 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 },
|
|
25667
25719
|
timeoutMs + 5e3
|
|
25668
25720
|
);
|
|
25669
25721
|
const r2 = response2;
|
|
@@ -25791,10 +25843,12 @@ ${lines.join("\n")}`
|
|
|
25791
25843
|
);
|
|
25792
25844
|
server.tool(
|
|
25793
25845
|
"list_frames",
|
|
25794
|
-
`List every top-level iframe/frame on the active page, with its origin, whether its contentDocument is accessible (same-origin), and its on-screen position.
|
|
25846
|
+
`List every top-level iframe/frame on the active page, with its origin, whether its contentDocument is accessible (same-origin), and its on-screen position. Also reports shadow-host inventory so you can spot pages whose visible content is rendered inside closed shadow roots (Radix portals, Stencil/Lit, custom web components).
|
|
25795
25847
|
|
|
25796
25848
|
Use this BEFORE calling find_text({frame: "..."}) or other frame-targeted tools \u2014 it shows you which frames exist and which are reachable. Knowing a frame is cross-origin up front means you can route to read_attachment (for the frame's src URL) or take_screenshot instead of getting a "frame not accessible" error from another tool.
|
|
25797
25849
|
|
|
25850
|
+
Also use this as a quick diagnostic when execute_script returns an empty document on a page you can clearly see \u2014 non-zero \`shadow_hosts\` (especially closed roots) means switch to find_text / get_page_text / click_element / fill_input, which pierce shadow DOM via the extension's privileged API.
|
|
25851
|
+
|
|
25798
25852
|
Per-frame fields:
|
|
25799
25853
|
- selector: CSS selector you can pass to other tools' \`frame\` parameter
|
|
25800
25854
|
- src: the iframe's src attribute (may be empty for about:blank frames)
|
|
@@ -25803,14 +25857,34 @@ Per-frame fields:
|
|
|
25803
25857
|
- title: the iframe's title attribute, often the most human-readable identifier
|
|
25804
25858
|
- x, y, width, height: bounding-box position in viewport CSS pixels
|
|
25805
25859
|
|
|
25806
|
-
|
|
25860
|
+
Per-shadow-host fields:
|
|
25861
|
+
- selector: short CSS hint for the host element (tag, id, or .class)
|
|
25862
|
+
- open: true if the shadow root is exposed via \`el.shadowRoot\` (most web components), false if it is closed (Radix portals, Stencil/Lit defaults) \u2014 closed roots are invisible to execute_script but pierced by chromeflow's other tools.
|
|
25863
|
+
- depth: nesting depth (0 = top-level host attached directly to the document)
|
|
25864
|
+
|
|
25865
|
+
Note: this returns top-level frames only. Nested cross-origin frame trees are not enumerated. Shadow hosts are capped at 25 to keep the response compact.`,
|
|
25807
25866
|
{},
|
|
25808
25867
|
async () => {
|
|
25809
25868
|
const response = await bridge.request({ type: "list_frames" });
|
|
25810
25869
|
if (response.type !== "list_frames_response") throw new Error(`Unexpected response: ${response.type}`);
|
|
25811
25870
|
const r = response;
|
|
25871
|
+
const hosts = r.shadow_hosts ?? [];
|
|
25872
|
+
const closedCount = hosts.filter((h) => !h.open).length;
|
|
25873
|
+
const openCount = hosts.length - closedCount;
|
|
25874
|
+
let shadowSection = "";
|
|
25875
|
+
if (hosts.length > 0) {
|
|
25876
|
+
const hostLines = hosts.map((h) => {
|
|
25877
|
+
const kind = h.open ? "open" : "closed";
|
|
25878
|
+
const indent = " ".repeat(h.depth);
|
|
25879
|
+
return ` ${indent}${h.selector} [${kind}]`;
|
|
25880
|
+
});
|
|
25881
|
+
shadowSection = `
|
|
25882
|
+
|
|
25883
|
+
Shadow hosts (${hosts.length}: ${openCount} open, ${closedCount} closed):` + (closedCount > 0 ? "\n (Closed roots are invisible to execute_script. Use find_text / get_page_text / click_element / fill_input \u2014 they pierce.)" : "") + "\n" + hostLines.join("\n");
|
|
25884
|
+
}
|
|
25812
25885
|
if (r.frames.length === 0) {
|
|
25813
|
-
|
|
25886
|
+
const noFrames = "No iframes or frames on this page.";
|
|
25887
|
+
return { content: [{ type: "text", text: hosts.length > 0 ? `${noFrames}${shadowSection}` : noFrames }] };
|
|
25814
25888
|
}
|
|
25815
25889
|
const lines = r.frames.map((f) => {
|
|
25816
25890
|
const access = f.accessible ? "accessible" : "cross-origin";
|
|
@@ -25820,13 +25894,13 @@ Note: this returns top-level frames only. Nested cross-origin frame trees are no
|
|
|
25820
25894
|
src: ${f.src}` : ""}`;
|
|
25821
25895
|
});
|
|
25822
25896
|
return { content: [{ type: "text", text: `Found ${r.frames.length} frame${r.frames.length === 1 ? "" : "s"}:
|
|
25823
|
-
${lines.join("\n")}` }] };
|
|
25897
|
+
${lines.join("\n")}${shadowSection}` }] };
|
|
25824
25898
|
}
|
|
25825
25899
|
);
|
|
25826
25900
|
}
|
|
25827
25901
|
|
|
25828
25902
|
// packages/mcp-server/src/index.ts
|
|
25829
|
-
var PACKAGE_VERSION = true ? "0.9.
|
|
25903
|
+
var PACKAGE_VERSION = true ? "0.9.10" : "dev";
|
|
25830
25904
|
main().catch((err) => {
|
|
25831
25905
|
console.error("[chromeflow] Fatal error:", err);
|
|
25832
25906
|
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.10",
|
|
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",
|