chromeflow 0.10.25 → 0.12.0
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 +199 -42
- package/package.json +2 -2
package/bin/chromeflow.mjs
CHANGED
|
@@ -24670,7 +24670,8 @@ var WsBridge = class {
|
|
|
24670
24670
|
return;
|
|
24671
24671
|
}
|
|
24672
24672
|
if (msg.type === "ready") {
|
|
24673
|
-
|
|
24673
|
+
const token = typeof msg.token === "string" ? msg.token : void 0;
|
|
24674
|
+
console.error(`[chromeflow] Extension ready${token ? " (token presented)" : ""}`);
|
|
24674
24675
|
const cwd = process.cwd();
|
|
24675
24676
|
const host = process.env.CHROMEFLOW_HOST ?? (process.env.CLAUDE_PLUGIN_ROOT ? "claude" : void 0);
|
|
24676
24677
|
ws.send(JSON.stringify({
|
|
@@ -24759,6 +24760,10 @@ var WsBridge = class {
|
|
|
24759
24760
|
import { homedir } from "node:os";
|
|
24760
24761
|
import { join, dirname } from "node:path";
|
|
24761
24762
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
24763
|
+
var PROMOTE_AT_SUCCESS = 2;
|
|
24764
|
+
var PRUNE_AT_FAILS = 2;
|
|
24765
|
+
var PROVISIONAL_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
24766
|
+
var ATOM_KEYS = ["tool", "target", "selector", "recovered_via", "signal", "fragile", "reason"];
|
|
24762
24767
|
function originKey(url) {
|
|
24763
24768
|
if (!url) return void 0;
|
|
24764
24769
|
try {
|
|
@@ -24771,20 +24776,47 @@ function originKey(url) {
|
|
|
24771
24776
|
}
|
|
24772
24777
|
}
|
|
24773
24778
|
var FRAGILE_RE = /:nth-(of-type|child)\(|>\s*\w+:nth/;
|
|
24779
|
+
function signatureOf(steps) {
|
|
24780
|
+
return JSON.stringify(steps.map((a) => [a.tool, a.target, a.selector ?? ""]));
|
|
24781
|
+
}
|
|
24782
|
+
function sanitizeAtom(a) {
|
|
24783
|
+
const out = {};
|
|
24784
|
+
for (const k of ATOM_KEYS) {
|
|
24785
|
+
if (a[k] !== void 0) out[k] = a[k];
|
|
24786
|
+
}
|
|
24787
|
+
return out;
|
|
24788
|
+
}
|
|
24789
|
+
function autoLabel(k, steps) {
|
|
24790
|
+
const tools = [...new Set(steps.map((s) => s.tool.replace("_element", "")))].join("+");
|
|
24791
|
+
let path2 = "/";
|
|
24792
|
+
try {
|
|
24793
|
+
path2 = new URL(k).pathname || "/";
|
|
24794
|
+
} catch {
|
|
24795
|
+
}
|
|
24796
|
+
return `auto: ${tools} @ ${path2}`;
|
|
24797
|
+
}
|
|
24774
24798
|
var FlowStore = class {
|
|
24775
24799
|
path;
|
|
24776
24800
|
data;
|
|
24777
24801
|
version;
|
|
24802
|
+
now;
|
|
24778
24803
|
// In-memory, per-session state (never persisted):
|
|
24779
24804
|
buffer = /* @__PURE__ */ new Map();
|
|
24780
24805
|
// notable atoms not yet committed, by origin
|
|
24781
24806
|
surfaced = /* @__PURE__ */ new Set();
|
|
24782
24807
|
// origins whose recall hint already fired this session
|
|
24808
|
+
recalled = /* @__PURE__ */ new Set();
|
|
24809
|
+
// origins whose trusted flow was actually shown this session
|
|
24783
24810
|
lastOrigin;
|
|
24784
|
-
constructor(version2, baseDir) {
|
|
24811
|
+
constructor(version2, baseDir, now = Date.now) {
|
|
24785
24812
|
this.version = version2;
|
|
24813
|
+
this.now = now;
|
|
24786
24814
|
this.path = join(baseDir ?? join(homedir(), ".chromeflow"), "flows.json");
|
|
24787
24815
|
this.data = this.load();
|
|
24816
|
+
this.pruneExpired();
|
|
24817
|
+
}
|
|
24818
|
+
nowIso() {
|
|
24819
|
+
return new Date(this.now()).toISOString();
|
|
24788
24820
|
}
|
|
24789
24821
|
load() {
|
|
24790
24822
|
try {
|
|
@@ -24809,10 +24841,36 @@ var FlowStore = class {
|
|
|
24809
24841
|
} catch {
|
|
24810
24842
|
}
|
|
24811
24843
|
}
|
|
24812
|
-
/**
|
|
24844
|
+
/** Drop expired provisional flows and any flow already past the fail ceiling. */
|
|
24845
|
+
pruneExpired() {
|
|
24846
|
+
const cutoff = this.now() - PROVISIONAL_TTL_MS;
|
|
24847
|
+
let changed = false;
|
|
24848
|
+
for (const [k, flows] of Object.entries(this.data.origins)) {
|
|
24849
|
+
const kept = flows.filter((f) => {
|
|
24850
|
+
if (f.fail_count >= PRUNE_AT_FAILS) return false;
|
|
24851
|
+
if (f.tier === "provisional" && Date.parse(f.last_verified) < cutoff) return false;
|
|
24852
|
+
return true;
|
|
24853
|
+
});
|
|
24854
|
+
if (kept.length !== flows.length) {
|
|
24855
|
+
changed = true;
|
|
24856
|
+
if (kept.length === 0) delete this.data.origins[k];
|
|
24857
|
+
else this.data.origins[k] = kept;
|
|
24858
|
+
}
|
|
24859
|
+
}
|
|
24860
|
+
if (changed) this.persist();
|
|
24861
|
+
}
|
|
24862
|
+
/**
|
|
24863
|
+
* Update the "current origin". Crossing to a DIFFERENT origin first autosaves
|
|
24864
|
+
* the origin we are leaving — that boundary is our best server-side proxy for
|
|
24865
|
+
* "a task on that site just finished".
|
|
24866
|
+
*/
|
|
24813
24867
|
noteUrl(url) {
|
|
24814
24868
|
const k = originKey(url);
|
|
24815
|
-
if (k)
|
|
24869
|
+
if (!k) return;
|
|
24870
|
+
if (this.lastOrigin && this.lastOrigin !== k && (this.buffer.get(this.lastOrigin)?.length ?? 0) > 0) {
|
|
24871
|
+
this.autoCommit(this.lastOrigin);
|
|
24872
|
+
}
|
|
24873
|
+
this.lastOrigin = k;
|
|
24816
24874
|
}
|
|
24817
24875
|
/** Buffer a notable atom against an origin (defaults to last-seen origin). */
|
|
24818
24876
|
observe(atom, url) {
|
|
@@ -24822,16 +24880,66 @@ var FlowStore = class {
|
|
|
24822
24880
|
const list = this.buffer.get(k) ?? [];
|
|
24823
24881
|
const sig = `${atom.tool}|${atom.target}|${atom.selector ?? ""}`;
|
|
24824
24882
|
if (list.some((a) => `${a.tool}|${a.target}|${a.selector ?? ""}` === sig)) return;
|
|
24825
|
-
list.push(atom);
|
|
24883
|
+
list.push(sanitizeAtom(atom));
|
|
24826
24884
|
this.buffer.set(k, list);
|
|
24827
24885
|
}
|
|
24828
|
-
/**
|
|
24886
|
+
/** Autosave a single origin's buffer as a provisional flow (or promote a match). */
|
|
24887
|
+
autoCommit(k) {
|
|
24888
|
+
const buf = this.buffer.get(k);
|
|
24889
|
+
if (!buf || buf.length === 0) return;
|
|
24890
|
+
this.buffer.delete(k);
|
|
24891
|
+
this.upsert(k, buf, null);
|
|
24892
|
+
this.pruneExpired();
|
|
24893
|
+
this.persist();
|
|
24894
|
+
}
|
|
24895
|
+
/** Flush every buffered origin. Call on shutdown and for single-origin sessions. */
|
|
24896
|
+
flushAll() {
|
|
24897
|
+
for (const k of [...this.buffer.keys()]) this.autoCommit(k);
|
|
24898
|
+
}
|
|
24899
|
+
/**
|
|
24900
|
+
* Insert or reinforce a flow for an origin.
|
|
24901
|
+
* label === null → autosave: create provisional, or bump+maybe-promote a match.
|
|
24902
|
+
* label is string → manual save_flow: create trusted, or promote+relabel a match.
|
|
24903
|
+
*/
|
|
24904
|
+
upsert(k, steps, label) {
|
|
24905
|
+
const now = this.nowIso();
|
|
24906
|
+
const sig = signatureOf(steps);
|
|
24907
|
+
const flows = this.data.origins[k] ?? [];
|
|
24908
|
+
const existing = flows.find((f) => signatureOf(f.steps) === sig);
|
|
24909
|
+
if (existing) {
|
|
24910
|
+
existing.success_count += 1;
|
|
24911
|
+
existing.last_verified = now;
|
|
24912
|
+
existing.chromeflow_version = this.version;
|
|
24913
|
+
if (label !== null) {
|
|
24914
|
+
existing.tier = "trusted";
|
|
24915
|
+
existing.task_label = label;
|
|
24916
|
+
} else if (existing.success_count >= PROMOTE_AT_SUCCESS) {
|
|
24917
|
+
existing.tier = "trusted";
|
|
24918
|
+
}
|
|
24919
|
+
} else {
|
|
24920
|
+
flows.push({
|
|
24921
|
+
id: `${k}#${flows.length + 1}`,
|
|
24922
|
+
task_label: label ?? autoLabel(k, steps),
|
|
24923
|
+
steps,
|
|
24924
|
+
tier: label !== null ? "trusted" : "provisional",
|
|
24925
|
+
created_at: now,
|
|
24926
|
+
last_verified: now,
|
|
24927
|
+
success_count: 1,
|
|
24928
|
+
fail_count: 0,
|
|
24929
|
+
chromeflow_version: this.version
|
|
24930
|
+
});
|
|
24931
|
+
}
|
|
24932
|
+
this.data.origins[k] = flows;
|
|
24933
|
+
return { saved: steps.length };
|
|
24934
|
+
}
|
|
24935
|
+
/** Compact recall hint for an origin (TRUSTED flows only), at most once per origin per session. */
|
|
24829
24936
|
recallHint(url) {
|
|
24830
24937
|
const k = originKey(url);
|
|
24831
24938
|
if (!k || this.surfaced.has(k)) return "";
|
|
24832
|
-
const flows = this.data.origins[k];
|
|
24833
|
-
if (
|
|
24939
|
+
const flows = (this.data.origins[k] ?? []).filter((f) => f.tier === "trusted");
|
|
24940
|
+
if (flows.length === 0) return "";
|
|
24834
24941
|
this.surfaced.add(k);
|
|
24942
|
+
this.recalled.add(k);
|
|
24835
24943
|
const best = [...flows].sort((a, b) => b.success_count - a.success_count).slice(0, 3);
|
|
24836
24944
|
const lines = best.map((f) => {
|
|
24837
24945
|
const steps = f.steps.map((s, i) => {
|
|
@@ -24849,7 +24957,31 @@ ${steps}`;
|
|
|
24849
24957
|
\u2139 known_flow for ${k} \u2014 prefer these proven steps over rediscovery (verify each as usual):
|
|
24850
24958
|
${lines.join("\n")}`;
|
|
24851
24959
|
}
|
|
24852
|
-
/**
|
|
24960
|
+
/**
|
|
24961
|
+
* A recalled trusted step failed on replay. Raise its fail_count; drop the flow
|
|
24962
|
+
* once it crosses PRUNE_AT_FAILS. Gated on the flow having actually been recalled
|
|
24963
|
+
* this session, so an unrelated failure can't ding a flow the agent never used.
|
|
24964
|
+
*/
|
|
24965
|
+
observeFailure(url, selectorOrText) {
|
|
24966
|
+
const k = originKey(url) ?? this.lastOrigin;
|
|
24967
|
+
if (!k || !selectorOrText || !this.recalled.has(k)) return;
|
|
24968
|
+
const flows = this.data.origins[k];
|
|
24969
|
+
if (!flows) return;
|
|
24970
|
+
let changed = false;
|
|
24971
|
+
for (const f of flows) {
|
|
24972
|
+
if (f.tier !== "trusted") continue;
|
|
24973
|
+
const hit = f.steps.some((s) => s.selector === selectorOrText || s.target === selectorOrText || s.target === `selector=${selectorOrText}`);
|
|
24974
|
+
if (hit) {
|
|
24975
|
+
f.fail_count += 1;
|
|
24976
|
+
changed = true;
|
|
24977
|
+
}
|
|
24978
|
+
}
|
|
24979
|
+
if (changed) {
|
|
24980
|
+
this.pruneExpired();
|
|
24981
|
+
this.persist();
|
|
24982
|
+
}
|
|
24983
|
+
}
|
|
24984
|
+
/** Nudge to save buffered hard-won steps — kept as the manual instant-vouch path. */
|
|
24853
24985
|
capturableHint(url) {
|
|
24854
24986
|
const k = originKey(url) ?? this.lastOrigin;
|
|
24855
24987
|
if (!k) return "";
|
|
@@ -24858,9 +24990,9 @@ ${lines.join("\n")}`;
|
|
|
24858
24990
|
const reasons = [...new Set(buf.map((a) => a.reason))].slice(0, 2).join("; ");
|
|
24859
24991
|
return `
|
|
24860
24992
|
|
|
24861
|
-
\u2139 flow_capturable: ${buf.length} hard-won step(s) on ${k}
|
|
24993
|
+
\u2139 flow_capturable: ${buf.length} hard-won step(s) on ${k} buffered (${reasons}). They autosave on leaving the site; call save_flow("<task label>") to trust them immediately.`;
|
|
24862
24994
|
}
|
|
24863
|
-
/**
|
|
24995
|
+
/** Manual save_flow: commit the buffered atoms for an origin as a TRUSTED flow. */
|
|
24864
24996
|
commit(taskLabel, url) {
|
|
24865
24997
|
const k = originKey(url) ?? this.lastOrigin;
|
|
24866
24998
|
if (!k) return { saved: 0, origin: null, message: "No origin known yet \u2014 navigate or interact with a page first." };
|
|
@@ -24868,30 +25000,16 @@ ${lines.join("\n")}`;
|
|
|
24868
25000
|
if (buf.length === 0) {
|
|
24869
25001
|
return { saved: 0, origin: k, message: `Nothing notable buffered for ${k}. Flows capture hard-won steps (a fallback fired, a verified submit, a field needing real keystrokes) \u2014 an ordinary first-try click isn't recorded.` };
|
|
24870
25002
|
}
|
|
24871
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24872
|
-
const sig = JSON.stringify(buf.map((a) => [a.tool, a.target, a.selector ?? ""]));
|
|
24873
|
-
const flows = this.data.origins[k] ?? [];
|
|
24874
|
-
const existing = flows.find((f) => f.task_label === taskLabel && JSON.stringify(f.steps.map((a) => [a.tool, a.target, a.selector ?? ""])) === sig);
|
|
24875
|
-
if (existing) {
|
|
24876
|
-
existing.success_count += 1;
|
|
24877
|
-
existing.last_verified = now;
|
|
24878
|
-
existing.chromeflow_version = this.version;
|
|
24879
|
-
} else {
|
|
24880
|
-
flows.push({
|
|
24881
|
-
id: `${k}#${flows.length + 1}`,
|
|
24882
|
-
task_label: taskLabel,
|
|
24883
|
-
steps: buf,
|
|
24884
|
-
created_at: now,
|
|
24885
|
-
last_verified: now,
|
|
24886
|
-
success_count: 1,
|
|
24887
|
-
fail_count: 0,
|
|
24888
|
-
chromeflow_version: this.version
|
|
24889
|
-
});
|
|
24890
|
-
}
|
|
24891
|
-
this.data.origins[k] = flows;
|
|
24892
25003
|
this.buffer.delete(k);
|
|
25004
|
+
const { saved } = this.upsert(k, buf, taskLabel);
|
|
25005
|
+
this.pruneExpired();
|
|
24893
25006
|
this.persist();
|
|
24894
|
-
return { saved
|
|
25007
|
+
return { saved, origin: k, message: `Saved flow "${taskLabel}" (${saved} steps) for ${k} \u2014 trusted.` };
|
|
25008
|
+
}
|
|
25009
|
+
/** Test-only accessor: the persisted flows for an origin. */
|
|
25010
|
+
_flowsFor(url) {
|
|
25011
|
+
const k = originKey(url) ?? url;
|
|
25012
|
+
return this.data.origins[k] ?? [];
|
|
24895
25013
|
}
|
|
24896
25014
|
};
|
|
24897
25015
|
function isFragileSelector(selector) {
|
|
@@ -25299,17 +25417,51 @@ ${lines.join("\n")}${r.warning ?? ""}${captchaLine}${oauthLine}` }] };
|
|
|
25299
25417
|
);
|
|
25300
25418
|
server.tool(
|
|
25301
25419
|
"set_file_input",
|
|
25302
|
-
|
|
25420
|
+
`Upload a file to a file input \u2014 works even when the input is hidden behind a custom drag-and-drop zone. Returns success=true only after an observable commit (file count goes up, input gets reset, or verify_selector appears within wait_ms). See CLAUDE.md for batch-upload guidance.
|
|
25421
|
+
|
|
25422
|
+
Two ways to supply the file:
|
|
25423
|
+
- file_path (CDP mode): an absolute path on the machine running this server. Reaches both open AND closed shadow roots.
|
|
25424
|
+
- file_content + file_name (inline-content mode): base64 file bytes plus a filename, materialized into the input directly. Use this when the server has no local disk access (e.g. a remote endpoint that can't see your filesystem). Caveat: inline-content mode reaches OPEN shadow roots only \u2014 if the input lives in a closed shadow root, use file_path instead.
|
|
25425
|
+
|
|
25426
|
+
Provide file_path OR file_content, not both.`,
|
|
25303
25427
|
{
|
|
25304
25428
|
hint: external_exports.string().describe("Label text, name, or surrounding text of the file input. Use empty string to target the first file input on the page."),
|
|
25305
|
-
file_path: external_exports.string().describe("Absolute path to the file to upload (e.g. /Users/you/Downloads/task.zip)"),
|
|
25429
|
+
file_path: external_exports.string().optional().describe("Absolute path to the file to upload (e.g. /Users/you/Downloads/task.zip). CDP mode \u2014 reaches closed shadow roots. Provide this OR file_content."),
|
|
25430
|
+
file_content: external_exports.string().optional().describe("Base64-encoded file bytes (no data: prefix) for inline-content mode. Use when the server has no local disk access. Requires file_name. Reaches OPEN shadow roots only. Provide this OR file_path."),
|
|
25431
|
+
file_name: external_exports.string().optional().describe('Filename to present to the page in inline-content mode (e.g. "report.pdf"). Required when file_content is set.'),
|
|
25432
|
+
mime_type: external_exports.string().optional().describe('Optional MIME type for inline-content mode (e.g. "application/pdf"). Inferred from file_name when omitted.'),
|
|
25306
25433
|
wait_ms: external_exports.number().int().min(0).optional().describe("How long to wait for an observable change after setting the file (default 3000). Increase for slow uploaders that take a moment to render thumbnails."),
|
|
25307
25434
|
verify_selector: external_exports.string().optional().describe('Optional CSS selector that should appear after a successful upload (e.g. ".photo-thumbnail", "[data-uploaded=true]"). When matched, set_file_input returns success immediately.')
|
|
25308
25435
|
},
|
|
25309
|
-
async ({ hint, file_path, wait_ms, verify_selector }) => {
|
|
25436
|
+
async ({ hint, file_path, file_content, file_name, mime_type, wait_ms, verify_selector }) => {
|
|
25437
|
+
if (!file_path && !file_content) {
|
|
25438
|
+
return {
|
|
25439
|
+
content: [{ type: "text", text: "Failed to set file: provide either file_path or file_content." }]
|
|
25440
|
+
};
|
|
25441
|
+
}
|
|
25442
|
+
if (file_path && file_content) {
|
|
25443
|
+
return {
|
|
25444
|
+
content: [{ type: "text", text: "Failed to set file: provide file_path OR file_content, not both." }]
|
|
25445
|
+
};
|
|
25446
|
+
}
|
|
25447
|
+
if (file_content && !file_name) {
|
|
25448
|
+
return {
|
|
25449
|
+
content: [{ type: "text", text: "Failed to set file: file_content requires file_name." }]
|
|
25450
|
+
};
|
|
25451
|
+
}
|
|
25310
25452
|
const wsTimeout = Math.max(3e4, (wait_ms ?? 3e3) + 1e4);
|
|
25311
25453
|
const response = await bridge.request(
|
|
25312
|
-
{
|
|
25454
|
+
{
|
|
25455
|
+
type: "set_file_input",
|
|
25456
|
+
hint,
|
|
25457
|
+
// snake -> camel, mirroring the existing file_path -> filePath mapping.
|
|
25458
|
+
filePath: file_path,
|
|
25459
|
+
fileContent: file_content,
|
|
25460
|
+
fileName: file_name,
|
|
25461
|
+
mimeType: mime_type,
|
|
25462
|
+
waitMs: wait_ms,
|
|
25463
|
+
verifySelector: verify_selector
|
|
25464
|
+
},
|
|
25313
25465
|
wsTimeout
|
|
25314
25466
|
);
|
|
25315
25467
|
const r = response;
|
|
@@ -25327,7 +25479,7 @@ ${lines.join("\n")}${r.warning ?? ""}${captchaLine}${oauthLine}` }] };
|
|
|
25327
25479
|
**Shadow-piercing helpers are pre-injected** into every script:
|
|
25328
25480
|
- \`$deep(selector, root?)\` \u2014 querySelector that walks open shadow roots
|
|
25329
25481
|
- \`$deepAll(selector, root?)\` \u2014 querySelectorAll equivalent, returns an array
|
|
25330
|
-
- \`shadowDocument\` \u2014 the open shadow root with the most interactive elements (buttons, inputs, links), or \`document\` if none. Useful when an SPA mounts ALL of its UI inside a single root shadow host (
|
|
25482
|
+
- \`shadowDocument\` \u2014 the open shadow root with the most interactive elements (buttons, inputs, links), or \`document\` if none. Useful when an SPA mounts ALL of its UI inside a single root shadow host (annotation-style dashboards that wrap the whole app in one web component): replace every \`document.querySelector*\` call with \`shadowDocument.querySelector*\` and the same code now reaches the SPA's content. On pages with multiple shadow roots (e.g. one for CSS theme vars, one for content), this picks the content root automatically.
|
|
25331
25483
|
- \`shadowDocuments\` \u2014 array of ALL open shadow roots on the page (in DOM order). Use when you need to search across multiple shadow roots or when the automatic pick is wrong.
|
|
25332
25484
|
|
|
25333
25485
|
The helpers pierce OPEN shadow roots only \u2014 MAIN world can't reach closed roots. For closed roots, use find_text / get_page_text / click_element / fill_input which pierce both kinds via chrome.dom.openOrClosedShadowRoot.
|
|
@@ -25843,7 +25995,7 @@ ANTI-BOT SUBMIT CEILING \u2014 synthetic clicks on social/auth platforms (Reddit
|
|
|
25843
25995
|
try_fiber: external_exports.boolean().optional().describe(`Opt-in last-resort fallback when silently_rejected fires. After the 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. Now shadow-DOM-aware: when the element is inside a shadow root, the fiber walk searches for React root containers inside that shadow root instead of walking the light DOM. Returns fiber_attempted=true in the response when the path was taken. Do NOT default to this; reserve for repeat silently_rejected on a known-safe React site.`),
|
|
25844
25996
|
activity_timeout_ms: external_exports.number().int().min(500).optional().describe(`How long the activity probe watches for DOM mutations, focus changes, URL changes, or alert/toast/modal appearance after the click (default 1500ms). The probe returns early as soon as activity is detected, so the default adds only ~100ms on successful clicks. Increase to 2500-4000ms for buttons that trigger async API calls before producing visible DOM changes. The probe reports "silently_rejected" when zero activity is seen within this window; a higher value reduces false negatives but slows genuine rejection detection. For buttons where the click triggers a 3-5s API call with no immediate DOM change, use skip_activity_probe instead.`),
|
|
25845
25997
|
skip_activity_probe: external_exports.boolean().optional().describe(`Skip the 1500ms activity probe AND all automatic fallbacks (tap gesture, pointer chain, DOM .click(), fiber walk) after the CDP click. The click is dispatched and success is returned immediately without verifying page activity. Use for buttons that trigger slow async API calls (3-5s+) before producing visible DOM changes, where the activity probe would falsely report "silently_rejected" and the fallback chain would double-fire. Verify state yourself via find_text or execute_script after an appropriate delay. NOTE: this is set IMPLICITLY when any until_* clause is passed, since the until-clause already provides verification. WARNING: no automatic silent-rejection detection when this is set without an until-clause.`),
|
|
25846
|
-
via: external_exports.enum(["auto", "cdp", "fiber"]).optional().describe(`Click dispatch mode. "auto" (default): CDP click, then fiber fallback when try_fiber=true and the activity probe failed. "cdp": CDP click only, no fiber fallback ever. "fiber": skip the CDP bezier + activity probe entirely and invoke __reactProps$.onClick directly. Use "fiber" on React-heavy SPAs (
|
|
25998
|
+
via: external_exports.enum(["auto", "cdp", "fiber"]).optional().describe(`Click dispatch mode. "auto" (default): CDP click, then fiber fallback when try_fiber=true and the activity probe failed. "cdp": CDP click only, no fiber fallback ever. "fiber": skip the CDP bezier + activity probe entirely and invoke __reactProps$.onClick directly. Use "fiber" on React-heavy SPAs (fiber-only annotation dashboards) where you already know the site is fiber-only \u2014 cuts ~3 seconds of ceremony off the round trip. The fiber path is undocumented React internal access, prefer "auto" until you've confirmed the site needs it.`),
|
|
25847
25999
|
in_dialog: external_exports.boolean().optional().describe(`Scope candidate matches to the topmost open dialog (\`[role=dialog]\`, \`[role=alertdialog]\`, or \`<dialog open>\`), highest z-index wins. Use when Radix/Headless UI dialogs portal to document.body and a generic textHint like "Cancel" would otherwise match the wrong button. Returns scope_missed=true when no dialog is open.`),
|
|
25848
26000
|
dialog_query: external_exports.string().optional().describe(`Scope candidate matches to a specific dialog by heading or aria-label substring. Use when multiple dialogs are open and in_dialog (topmost) would pick the wrong one \u2014 e.g. click_element("Confirm", dialog_query="Delete account"). Mutually exclusive with in_dialog; dialog_query wins when both are set.`),
|
|
25849
26001
|
wait_until_enabled_ms: external_exports.number().int().min(0).optional().describe(`When the matched target is currently disabled (native disabled OR aria-disabled=true), poll for up to this many ms waiting for it to become enabled before clicking. Default 0 (do not wait \u2014 return target_disabled immediately). Use 2000-5000 for Submit-style buttons that briefly disable while an async copilot/validator/save is in flight. On timeout the response carries target_disabled=true plus a structured disabled_state snapshot (disabled, aria_disabled, pointer_events, opacity, visible) so the caller can decide between "wait more" or "field is genuinely missing \u2014 run get_form_fields(only_empty:true)".`)
|
|
@@ -25919,6 +26071,7 @@ Current URL: ${activeTab.url}`;
|
|
|
25919
26071
|
const recall = flowStore.recallHint(nowUrl);
|
|
25920
26072
|
const capturable = flowStore.capturableHint(actionUrl);
|
|
25921
26073
|
if (!r.success) {
|
|
26074
|
+
flowStore.observeFailure(actionUrl, selector ?? textHint);
|
|
25922
26075
|
return {
|
|
25923
26076
|
content: [
|
|
25924
26077
|
{
|
|
@@ -25935,9 +26088,9 @@ Current URL: ${activeTab.url}`;
|
|
|
25935
26088
|
);
|
|
25936
26089
|
server.tool(
|
|
25937
26090
|
"save_flow",
|
|
25938
|
-
`
|
|
26091
|
+
`Trust the hard-won interaction steps chromeflow buffered for the current site, immediately, as a named flow. chromeflow auto-buffers only NOTABLE resolutions (a click that needed a fallback, a verified submit, a field that needed real keystrokes), and AUTOSAVES them as a provisional flow when you leave the site \u2014 so memory works even if you never call this. Provisional flows are not recalled until they have been independently re-observed, or until you vouch for them here. Calling save_flow promotes the buffered steps to TRUSTED right away (an explicit "I confirm this worked"), so they are recalled next session instead of waiting to earn it.
|
|
25939
26092
|
|
|
25940
|
-
Call this when a response shows \`flow_capturable
|
|
26093
|
+
Call this when a response shows \`flow_capturable\` and you are confident the task genuinely succeeded. Stored locally only (~/.chromeflow/flows.json), selectors/signals only \u2014 never typed text. Guidance, not autopilot: recalled steps are still verified on replay.`,
|
|
25941
26094
|
{
|
|
25942
26095
|
task_label: external_exports.string().describe('Short human label for what this flow accomplishes, e.g. "submit text post", "set flair and submit", "log report time".')
|
|
25943
26096
|
},
|
|
@@ -26259,7 +26412,7 @@ ${lines.join("\n")}${shadowSection}` }] };
|
|
|
26259
26412
|
}
|
|
26260
26413
|
|
|
26261
26414
|
// packages/mcp-server/src/index.ts
|
|
26262
|
-
var PACKAGE_VERSION = true ? "0.
|
|
26415
|
+
var PACKAGE_VERSION = true ? "0.12.0" : "dev";
|
|
26263
26416
|
main().catch((err) => {
|
|
26264
26417
|
console.error("[chromeflow] Fatal error:", err);
|
|
26265
26418
|
process.exit(1);
|
|
@@ -26335,6 +26488,10 @@ ${tabList}`
|
|
|
26335
26488
|
await server.connect(transport);
|
|
26336
26489
|
const exitClean = (reason) => {
|
|
26337
26490
|
console.error(`[chromeflow] host disconnected (${reason}), exiting.`);
|
|
26491
|
+
try {
|
|
26492
|
+
flowStore.flushAll();
|
|
26493
|
+
} catch {
|
|
26494
|
+
}
|
|
26338
26495
|
process.exit(0);
|
|
26339
26496
|
};
|
|
26340
26497
|
process.stdin.on("end", () => exitClean("stdin end"));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chromeflow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for chromeflow
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "MCP server for chromeflow \u2014 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",
|
|
7
7
|
"bin": {
|