chromeflow 0.11.1 → 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 +157 -35
- package/package.json +2 -2
package/bin/chromeflow.mjs
CHANGED
|
@@ -24760,6 +24760,10 @@ var WsBridge = class {
|
|
|
24760
24760
|
import { homedir } from "node:os";
|
|
24761
24761
|
import { join, dirname } from "node:path";
|
|
24762
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"];
|
|
24763
24767
|
function originKey(url) {
|
|
24764
24768
|
if (!url) return void 0;
|
|
24765
24769
|
try {
|
|
@@ -24772,20 +24776,47 @@ function originKey(url) {
|
|
|
24772
24776
|
}
|
|
24773
24777
|
}
|
|
24774
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
|
+
}
|
|
24775
24798
|
var FlowStore = class {
|
|
24776
24799
|
path;
|
|
24777
24800
|
data;
|
|
24778
24801
|
version;
|
|
24802
|
+
now;
|
|
24779
24803
|
// In-memory, per-session state (never persisted):
|
|
24780
24804
|
buffer = /* @__PURE__ */ new Map();
|
|
24781
24805
|
// notable atoms not yet committed, by origin
|
|
24782
24806
|
surfaced = /* @__PURE__ */ new Set();
|
|
24783
24807
|
// origins whose recall hint already fired this session
|
|
24808
|
+
recalled = /* @__PURE__ */ new Set();
|
|
24809
|
+
// origins whose trusted flow was actually shown this session
|
|
24784
24810
|
lastOrigin;
|
|
24785
|
-
constructor(version2, baseDir) {
|
|
24811
|
+
constructor(version2, baseDir, now = Date.now) {
|
|
24786
24812
|
this.version = version2;
|
|
24813
|
+
this.now = now;
|
|
24787
24814
|
this.path = join(baseDir ?? join(homedir(), ".chromeflow"), "flows.json");
|
|
24788
24815
|
this.data = this.load();
|
|
24816
|
+
this.pruneExpired();
|
|
24817
|
+
}
|
|
24818
|
+
nowIso() {
|
|
24819
|
+
return new Date(this.now()).toISOString();
|
|
24789
24820
|
}
|
|
24790
24821
|
load() {
|
|
24791
24822
|
try {
|
|
@@ -24810,10 +24841,36 @@ var FlowStore = class {
|
|
|
24810
24841
|
} catch {
|
|
24811
24842
|
}
|
|
24812
24843
|
}
|
|
24813
|
-
/**
|
|
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
|
+
*/
|
|
24814
24867
|
noteUrl(url) {
|
|
24815
24868
|
const k = originKey(url);
|
|
24816
|
-
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;
|
|
24817
24874
|
}
|
|
24818
24875
|
/** Buffer a notable atom against an origin (defaults to last-seen origin). */
|
|
24819
24876
|
observe(atom, url) {
|
|
@@ -24823,16 +24880,66 @@ var FlowStore = class {
|
|
|
24823
24880
|
const list = this.buffer.get(k) ?? [];
|
|
24824
24881
|
const sig = `${atom.tool}|${atom.target}|${atom.selector ?? ""}`;
|
|
24825
24882
|
if (list.some((a) => `${a.tool}|${a.target}|${a.selector ?? ""}` === sig)) return;
|
|
24826
|
-
list.push(atom);
|
|
24883
|
+
list.push(sanitizeAtom(atom));
|
|
24827
24884
|
this.buffer.set(k, list);
|
|
24828
24885
|
}
|
|
24829
|
-
/**
|
|
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. */
|
|
24830
24936
|
recallHint(url) {
|
|
24831
24937
|
const k = originKey(url);
|
|
24832
24938
|
if (!k || this.surfaced.has(k)) return "";
|
|
24833
|
-
const flows = this.data.origins[k];
|
|
24834
|
-
if (
|
|
24939
|
+
const flows = (this.data.origins[k] ?? []).filter((f) => f.tier === "trusted");
|
|
24940
|
+
if (flows.length === 0) return "";
|
|
24835
24941
|
this.surfaced.add(k);
|
|
24942
|
+
this.recalled.add(k);
|
|
24836
24943
|
const best = [...flows].sort((a, b) => b.success_count - a.success_count).slice(0, 3);
|
|
24837
24944
|
const lines = best.map((f) => {
|
|
24838
24945
|
const steps = f.steps.map((s, i) => {
|
|
@@ -24850,7 +24957,31 @@ ${steps}`;
|
|
|
24850
24957
|
\u2139 known_flow for ${k} \u2014 prefer these proven steps over rediscovery (verify each as usual):
|
|
24851
24958
|
${lines.join("\n")}`;
|
|
24852
24959
|
}
|
|
24853
|
-
/**
|
|
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. */
|
|
24854
24985
|
capturableHint(url) {
|
|
24855
24986
|
const k = originKey(url) ?? this.lastOrigin;
|
|
24856
24987
|
if (!k) return "";
|
|
@@ -24859,9 +24990,9 @@ ${lines.join("\n")}`;
|
|
|
24859
24990
|
const reasons = [...new Set(buf.map((a) => a.reason))].slice(0, 2).join("; ");
|
|
24860
24991
|
return `
|
|
24861
24992
|
|
|
24862
|
-
\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.`;
|
|
24863
24994
|
}
|
|
24864
|
-
/**
|
|
24995
|
+
/** Manual save_flow: commit the buffered atoms for an origin as a TRUSTED flow. */
|
|
24865
24996
|
commit(taskLabel, url) {
|
|
24866
24997
|
const k = originKey(url) ?? this.lastOrigin;
|
|
24867
24998
|
if (!k) return { saved: 0, origin: null, message: "No origin known yet \u2014 navigate or interact with a page first." };
|
|
@@ -24869,30 +25000,16 @@ ${lines.join("\n")}`;
|
|
|
24869
25000
|
if (buf.length === 0) {
|
|
24870
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.` };
|
|
24871
25002
|
}
|
|
24872
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
24873
|
-
const sig = JSON.stringify(buf.map((a) => [a.tool, a.target, a.selector ?? ""]));
|
|
24874
|
-
const flows = this.data.origins[k] ?? [];
|
|
24875
|
-
const existing = flows.find((f) => f.task_label === taskLabel && JSON.stringify(f.steps.map((a) => [a.tool, a.target, a.selector ?? ""])) === sig);
|
|
24876
|
-
if (existing) {
|
|
24877
|
-
existing.success_count += 1;
|
|
24878
|
-
existing.last_verified = now;
|
|
24879
|
-
existing.chromeflow_version = this.version;
|
|
24880
|
-
} else {
|
|
24881
|
-
flows.push({
|
|
24882
|
-
id: `${k}#${flows.length + 1}`,
|
|
24883
|
-
task_label: taskLabel,
|
|
24884
|
-
steps: buf,
|
|
24885
|
-
created_at: now,
|
|
24886
|
-
last_verified: now,
|
|
24887
|
-
success_count: 1,
|
|
24888
|
-
fail_count: 0,
|
|
24889
|
-
chromeflow_version: this.version
|
|
24890
|
-
});
|
|
24891
|
-
}
|
|
24892
|
-
this.data.origins[k] = flows;
|
|
24893
25003
|
this.buffer.delete(k);
|
|
25004
|
+
const { saved } = this.upsert(k, buf, taskLabel);
|
|
25005
|
+
this.pruneExpired();
|
|
24894
25006
|
this.persist();
|
|
24895
|
-
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] ?? [];
|
|
24896
25013
|
}
|
|
24897
25014
|
};
|
|
24898
25015
|
function isFragileSelector(selector) {
|
|
@@ -25954,6 +26071,7 @@ Current URL: ${activeTab.url}`;
|
|
|
25954
26071
|
const recall = flowStore.recallHint(nowUrl);
|
|
25955
26072
|
const capturable = flowStore.capturableHint(actionUrl);
|
|
25956
26073
|
if (!r.success) {
|
|
26074
|
+
flowStore.observeFailure(actionUrl, selector ?? textHint);
|
|
25957
26075
|
return {
|
|
25958
26076
|
content: [
|
|
25959
26077
|
{
|
|
@@ -25970,9 +26088,9 @@ Current URL: ${activeTab.url}`;
|
|
|
25970
26088
|
);
|
|
25971
26089
|
server.tool(
|
|
25972
26090
|
"save_flow",
|
|
25973
|
-
`
|
|
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.
|
|
25974
26092
|
|
|
25975
|
-
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.`,
|
|
25976
26094
|
{
|
|
25977
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".')
|
|
25978
26096
|
},
|
|
@@ -26294,7 +26412,7 @@ ${lines.join("\n")}${shadowSection}` }] };
|
|
|
26294
26412
|
}
|
|
26295
26413
|
|
|
26296
26414
|
// packages/mcp-server/src/index.ts
|
|
26297
|
-
var PACKAGE_VERSION = true ? "0.
|
|
26415
|
+
var PACKAGE_VERSION = true ? "0.12.0" : "dev";
|
|
26298
26416
|
main().catch((err) => {
|
|
26299
26417
|
console.error("[chromeflow] Fatal error:", err);
|
|
26300
26418
|
process.exit(1);
|
|
@@ -26370,6 +26488,10 @@ ${tabList}`
|
|
|
26370
26488
|
await server.connect(transport);
|
|
26371
26489
|
const exitClean = (reason) => {
|
|
26372
26490
|
console.error(`[chromeflow] host disconnected (${reason}), exiting.`);
|
|
26491
|
+
try {
|
|
26492
|
+
flowStore.flushAll();
|
|
26493
|
+
} catch {
|
|
26494
|
+
}
|
|
26373
26495
|
process.exit(0);
|
|
26374
26496
|
};
|
|
26375
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": {
|