@weavetab/mcp 2.5.0-beta.2 → 2.5.0-beta.5
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/AGENTS.md +1 -1
- package/ARCHITECTURE.md +6 -6
- package/LICENSE +216 -631
- package/README.md +153 -1074
- package/SECURITY.md +33 -22
- package/TROUBLESHOOTING.md +2 -2
- package/dist/cdp/bridge.js +2 -1
- package/dist/cdp/connector.d.ts +2 -0
- package/dist/cdp/connector.js +10 -6
- package/dist/cdp/helpers.d.ts +4 -0
- package/dist/cdp/helpers.js +181 -0
- package/dist/cdp/omni_broker.d.ts +1 -1
- package/dist/cdp/omni_broker.js +1 -1
- package/dist/cdp/walker.d.ts +25 -1
- package/dist/cdp/walker.js +304 -7
- package/dist/cli/plugin.d.ts +1 -0
- package/dist/cli/plugin.js +81 -18
- package/dist/cli/updater.d.ts +1 -1
- package/dist/cli/updater.js +27 -7
- package/dist/config/loader.d.ts +9 -1
- package/dist/config/loader.js +42 -3
- package/dist/desktop/discovery.d.ts +44 -0
- package/dist/desktop/discovery.js +364 -0
- package/dist/index.js +137 -21
- package/dist/intelligence/marks.d.ts +36 -0
- package/dist/intelligence/marks.js +30 -0
- package/dist/intelligence/patternCompiler.d.ts +118 -0
- package/dist/intelligence/patternCompiler.js +259 -0
- package/dist/intelligence/sessionMemory.d.ts +2 -0
- package/dist/intelligence/sessionMemory.js +18 -8
- package/dist/overlay/injector.js +2 -2
- package/dist/plugin/interface.d.ts +88 -5
- package/dist/plugin/loader.d.ts +12 -1
- package/dist/plugin/loader.js +280 -70
- package/dist/plugin/pipeline.d.ts +75 -0
- package/dist/plugin/pipeline.js +183 -0
- package/dist/plugin/registry.d.ts +2 -1
- package/dist/plugin/registry.js +24 -14
- package/dist/security/secrets.d.ts +7 -0
- package/dist/security/secrets.js +37 -0
- package/dist/sensors/actionability.js +1 -2
- package/dist/sensors/guard.js +4 -10
- package/dist/sensors/mutations.js +12 -0
- package/dist/sensors/operation_monitor.js +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +453 -178
- package/dist/state/agent.d.ts +1 -0
- package/dist/state/agent.js +3 -0
- package/dist/state/delta.d.ts +12 -4
- package/dist/state/delta.js +67 -14
- package/dist/state/session.d.ts +6 -0
- package/dist/state/session.js +27 -0
- package/dist/tools/browser_detect.d.ts +16 -3
- package/dist/tools/browser_detect.js +52 -7
- package/dist/tools/burst.d.ts +17 -8
- package/dist/tools/burst.js +236 -77
- package/dist/tools/captcha.js +1 -1
- package/dist/tools/checkpoint.d.ts +38 -0
- package/dist/tools/checkpoint.js +271 -0
- package/dist/tools/click.d.ts +1 -0
- package/dist/tools/click.js +74 -70
- package/dist/tools/design_extract.js +1 -1
- package/dist/tools/eval.js +1 -1
- package/dist/tools/fill.d.ts +20 -1
- package/dist/tools/fill.js +199 -40
- package/dist/tools/find.d.ts +16 -0
- package/dist/tools/find.js +67 -44
- package/dist/tools/github/index.d.ts +19 -4
- package/dist/tools/github/index.js +40 -4
- package/dist/tools/highlight.d.ts +6 -2
- package/dist/tools/highlight.js +158 -72
- package/dist/tools/inspect.js +1 -1
- package/dist/tools/key.js +65 -56
- package/dist/tools/navigate.d.ts +1 -1
- package/dist/tools/navigate.js +34 -39
- package/dist/tools/pdf.js +21 -13
- package/dist/tools/pointer.d.ts +18 -3
- package/dist/tools/pointer.js +89 -1
- package/dist/tools/read.d.ts +28 -8
- package/dist/tools/read.js +230 -56
- package/dist/tools/scrape.js +67 -13
- package/dist/tools/screenshot.js +21 -16
- package/dist/tools/scroll.js +6 -11
- package/dist/tools/select.js +8 -2
- package/dist/tools/snapshot.js +3 -34
- package/dist/tools/tabs.js +15 -7
- package/dist/tools/thought.d.ts +2 -0
- package/dist/tools/thought.js +13 -14
- package/dist/tools/type.d.ts +2 -0
- package/dist/tools/type.js +107 -58
- package/dist/tools/type_secret.js +42 -76
- package/dist/tools/upload.js +1 -1
- package/dist/tools/video_compiler.d.ts +1 -1
- package/dist/tools/video_compiler.js +11 -12
- package/dist/version.d.ts +5 -0
- package/dist/version.js +5 -0
- package/docs/DOCUMENTATION.md +37 -41
- package/docs/PLUGINS.md +121 -0
- package/docs/TOOLS.md +54 -0
- package/docs/architecture/blind-injection.md +83 -20
- package/docs/architecture/overview.md +49 -49
- package/docs/architecture/security.md +8 -7
- package/docs/architecture/transport.md +2 -2
- package/docs/cli/cli-commands.md +7 -1
- package/docs/cli/cli-flags.md +15 -7
- package/docs/cli/cli-scripting.md +6 -3
- package/docs/configuration/config-locations.md +3 -4
- package/docs/configuration/env-variables.md +4 -4
- package/docs/configuration/folders.md +55 -54
- package/docs/desktop-automation/electron-apps.md +214 -162
- package/docs/get-started/introduction.md +60 -60
- package/docs/get-started/setup-wizard.md +7 -6
- package/docs/intelligence/site-profiles.md +151 -0
- package/docs/tools/tools.md +631 -293
- package/docs/troubleshooting/trouble-browser.md +1 -1
- package/docs/troubleshooting/trouble-loop.md +31 -31
- package/ext-dist/background.js +10 -62
- package/ext-dist/content.js +333 -920
- package/ext-dist/hud/hud.js +9 -9
- package/ext-dist/manifest.json +2 -6
- package/ext-dist/styles.css +6 -1
- package/icon.png +0 -0
- package/mcp.json +87 -71
- package/package.json +118 -119
- package/server.json +5 -5
- package/ext-dist/sidepanel/sidepanel.js +0 -25
- package/ext-dist/sidepanel.html +0 -547
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weavetab — tools/checkpoint.ts
|
|
3
|
+
* browser_checkpoint: Session time-travel and rollback.
|
|
4
|
+
*
|
|
5
|
+
* Captures and restores named checkpoints of browser state:
|
|
6
|
+
* - Cookies (HttpOnly and Secure via CDP Network)
|
|
7
|
+
* - LocalStorage & SessionStorage
|
|
8
|
+
* - URL, page title, and viewport scroll position
|
|
9
|
+
* - Optional lightweight DOM snapshot
|
|
10
|
+
*
|
|
11
|
+
* Saves tokens and time when trial-and-error automation branches fail.
|
|
12
|
+
*
|
|
13
|
+
* Weavetab by fy2ne
|
|
14
|
+
*/
|
|
15
|
+
import { logAction } from "../audit/logger.js";
|
|
16
|
+
import { recordAction } from "../intelligence/trail.js";
|
|
17
|
+
import { clearTabCache } from "../state/cache.js";
|
|
18
|
+
import { sessionRegistry } from "../state/session.js";
|
|
19
|
+
import { getConfigAgentId } from "../config/loader.js";
|
|
20
|
+
// In-memory checkpoint registry keyed by targetId -> name -> CheckpointData
|
|
21
|
+
const checkpointRegistry = new Map();
|
|
22
|
+
export function getCheckpointsForTarget(targetId) {
|
|
23
|
+
let map = checkpointRegistry.get(targetId);
|
|
24
|
+
if (!map) {
|
|
25
|
+
map = new Map();
|
|
26
|
+
checkpointRegistry.set(targetId, map);
|
|
27
|
+
}
|
|
28
|
+
return map;
|
|
29
|
+
}
|
|
30
|
+
export function clearAllCheckpoints() {
|
|
31
|
+
checkpointRegistry.clear();
|
|
32
|
+
}
|
|
33
|
+
export async function browserCheckpoint(session, args, config) {
|
|
34
|
+
let targetId = "default";
|
|
35
|
+
const active = sessionRegistry.getActive(getConfigAgentId(config));
|
|
36
|
+
if (active)
|
|
37
|
+
targetId = active.targetId;
|
|
38
|
+
const targetCheckpoints = getCheckpointsForTarget(targetId);
|
|
39
|
+
// ── ACTION: LIST ─────────────────────────────────────────────────────────────
|
|
40
|
+
if (args.action === "list") {
|
|
41
|
+
const list = Array.from(targetCheckpoints.values()).map((cp) => ({
|
|
42
|
+
name: cp.name,
|
|
43
|
+
url: cp.url,
|
|
44
|
+
title: cp.title,
|
|
45
|
+
timestamp: cp.timestamp,
|
|
46
|
+
cookiesCount: cp.cookies.length,
|
|
47
|
+
localStorageKeys: Object.keys(cp.localStorage).length,
|
|
48
|
+
sessionStorageKeys: Object.keys(cp.sessionStorage).length,
|
|
49
|
+
hasDomSnapshot: !!cp.domSnapshot,
|
|
50
|
+
}));
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
action: "list",
|
|
54
|
+
count: list.length,
|
|
55
|
+
checkpoints: list,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// ── ACTION: DELETE ───────────────────────────────────────────────────────────
|
|
59
|
+
if (args.action === "delete") {
|
|
60
|
+
if (!args.name) {
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
error_code: "INVALID_ARGS",
|
|
64
|
+
message: "Checkpoint 'name' is required for action 'delete'.",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const existed = targetCheckpoints.delete(args.name);
|
|
68
|
+
return {
|
|
69
|
+
success: existed,
|
|
70
|
+
action: "delete",
|
|
71
|
+
name: args.name,
|
|
72
|
+
message: existed
|
|
73
|
+
? `Checkpoint '${args.name}' deleted.`
|
|
74
|
+
: `Checkpoint '${args.name}' not found.`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// ── ACTION: SAVE ─────────────────────────────────────────────────────────────
|
|
78
|
+
if (args.action === "save") {
|
|
79
|
+
if (!args.name) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error_code: "INVALID_ARGS",
|
|
83
|
+
message: "Checkpoint 'name' is required for action 'save'.",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const { Page, Network, Runtime } = session;
|
|
87
|
+
const { frameTree } = await Page.getFrameTree();
|
|
88
|
+
const currentUrl = frameTree.frame.url;
|
|
89
|
+
// 1. Snapshot cookies
|
|
90
|
+
let cookies = [];
|
|
91
|
+
try {
|
|
92
|
+
if (typeof Network.getCookies === "function") {
|
|
93
|
+
const res = await Network.getCookies({ urls: [currentUrl] });
|
|
94
|
+
cookies = res.cookies || [];
|
|
95
|
+
}
|
|
96
|
+
else if (typeof Network.getAllCookies === "function") {
|
|
97
|
+
const res = await Network.getAllCookies();
|
|
98
|
+
cookies = res.cookies || [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Cookies best-effort
|
|
103
|
+
}
|
|
104
|
+
// 2. Snapshot LocalStorage, SessionStorage, Scroll, and Title
|
|
105
|
+
let storageData = {
|
|
106
|
+
localStorage: {},
|
|
107
|
+
sessionStorage: {},
|
|
108
|
+
scroll: { x: 0, y: 0 },
|
|
109
|
+
title: "",
|
|
110
|
+
};
|
|
111
|
+
try {
|
|
112
|
+
const evalRes = await Runtime.evaluate({
|
|
113
|
+
expression: `(function() {
|
|
114
|
+
var ls = {};
|
|
115
|
+
try {
|
|
116
|
+
for (var i = 0; i < localStorage.length; i++) {
|
|
117
|
+
var k = localStorage.key(i);
|
|
118
|
+
if (k) ls[k] = localStorage.getItem(k);
|
|
119
|
+
}
|
|
120
|
+
} catch(e) {}
|
|
121
|
+
var ss = {};
|
|
122
|
+
try {
|
|
123
|
+
for (var j = 0; j < sessionStorage.length; j++) {
|
|
124
|
+
var sk = sessionStorage.key(j);
|
|
125
|
+
if (sk) ss[sk] = sessionStorage.getItem(sk);
|
|
126
|
+
}
|
|
127
|
+
} catch(e) {}
|
|
128
|
+
return {
|
|
129
|
+
localStorage: ls,
|
|
130
|
+
sessionStorage: ss,
|
|
131
|
+
scroll: { x: window.scrollX || 0, y: window.scrollY || 0 },
|
|
132
|
+
title: document.title || ""
|
|
133
|
+
};
|
|
134
|
+
})()`,
|
|
135
|
+
returnByValue: true,
|
|
136
|
+
});
|
|
137
|
+
if (evalRes?.result?.value) {
|
|
138
|
+
storageData = evalRes.result.value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Runtime best-effort
|
|
143
|
+
}
|
|
144
|
+
// 3. Optional lightweight DOM capture
|
|
145
|
+
let domSnapshot;
|
|
146
|
+
if (args.includeDom) {
|
|
147
|
+
try {
|
|
148
|
+
const domRes = await Runtime.evaluate({
|
|
149
|
+
expression: `document.documentElement.outerHTML.slice(0, 100000)`,
|
|
150
|
+
returnByValue: true,
|
|
151
|
+
});
|
|
152
|
+
domSnapshot = domRes?.result?.value;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// ignore
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const checkpoint = {
|
|
159
|
+
name: args.name,
|
|
160
|
+
url: currentUrl,
|
|
161
|
+
title: storageData.title,
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
cookies,
|
|
164
|
+
localStorage: storageData.localStorage,
|
|
165
|
+
sessionStorage: storageData.sessionStorage,
|
|
166
|
+
scroll: storageData.scroll,
|
|
167
|
+
domSnapshot,
|
|
168
|
+
};
|
|
169
|
+
targetCheckpoints.set(args.name, checkpoint);
|
|
170
|
+
logAction("browser_checkpoint", args.name, `Saved checkpoint at ${currentUrl}`);
|
|
171
|
+
recordAction({
|
|
172
|
+
timestamp: checkpoint.timestamp,
|
|
173
|
+
tool: "browser_checkpoint",
|
|
174
|
+
input: { action: "save", name: args.name, url: currentUrl },
|
|
175
|
+
result: { success: true },
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
action: "save",
|
|
180
|
+
name: args.name,
|
|
181
|
+
url: currentUrl,
|
|
182
|
+
title: storageData.title,
|
|
183
|
+
timestamp: checkpoint.timestamp,
|
|
184
|
+
cookiesCount: cookies.length,
|
|
185
|
+
localStorageKeysCount: Object.keys(storageData.localStorage).length,
|
|
186
|
+
sessionStorageKeysCount: Object.keys(storageData.sessionStorage).length,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// ── ACTION: RESTORE ──────────────────────────────────────────────────────────
|
|
190
|
+
if (args.action === "restore") {
|
|
191
|
+
if (!args.name) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
error_code: "INVALID_ARGS",
|
|
195
|
+
message: "Checkpoint 'name' is required for action 'restore'.",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const checkpoint = targetCheckpoints.get(args.name);
|
|
199
|
+
if (!checkpoint) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error_code: "CHECKPOINT_NOT_FOUND",
|
|
203
|
+
message: `No checkpoint found with name '${args.name}'. Use action: 'list' to see available checkpoints.`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const { Page, Network, Runtime } = session;
|
|
207
|
+
const { frameTree } = await Page.getFrameTree();
|
|
208
|
+
const currentUrl = frameTree.frame.url;
|
|
209
|
+
// 1. Navigate to checkpoint URL if different
|
|
210
|
+
if (currentUrl !== checkpoint.url) {
|
|
211
|
+
await Page.navigate({ url: checkpoint.url });
|
|
212
|
+
await Page.loadEventFired().catch(() => { });
|
|
213
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
214
|
+
}
|
|
215
|
+
// 2. Restore Cookies
|
|
216
|
+
if (checkpoint.cookies && checkpoint.cookies.length > 0) {
|
|
217
|
+
try {
|
|
218
|
+
if (typeof Network.setCookies === "function") {
|
|
219
|
+
await Network.setCookies({ cookies: checkpoint.cookies });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// Cookies best-effort
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// 3. Restore LocalStorage, SessionStorage, and Scroll position
|
|
227
|
+
try {
|
|
228
|
+
await Runtime.evaluate({
|
|
229
|
+
expression: `(function(ls, ss, scroll) {
|
|
230
|
+
try {
|
|
231
|
+
localStorage.clear();
|
|
232
|
+
for (var k in ls) { localStorage.setItem(k, ls[k]); }
|
|
233
|
+
} catch(e) {}
|
|
234
|
+
try {
|
|
235
|
+
sessionStorage.clear();
|
|
236
|
+
for (var sk in ss) { sessionStorage.setItem(sk, ss[sk]); }
|
|
237
|
+
} catch(e) {}
|
|
238
|
+
if (scroll) {
|
|
239
|
+
window.scrollTo(scroll.x, scroll.y);
|
|
240
|
+
}
|
|
241
|
+
})(${JSON.stringify(checkpoint.localStorage)}, ${JSON.stringify(checkpoint.sessionStorage)}, ${JSON.stringify(checkpoint.scroll)})`,
|
|
242
|
+
returnByValue: false,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Storage best-effort
|
|
247
|
+
}
|
|
248
|
+
// Invalidate cached action map for this target so fresh refs get generated
|
|
249
|
+
clearTabCache(targetId);
|
|
250
|
+
logAction("browser_checkpoint", args.name, `Restored checkpoint to ${checkpoint.url}`);
|
|
251
|
+
recordAction({
|
|
252
|
+
timestamp: new Date().toISOString(),
|
|
253
|
+
tool: "browser_checkpoint",
|
|
254
|
+
input: { action: "restore", name: args.name, targetUrl: checkpoint.url },
|
|
255
|
+
result: { success: true },
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
success: true,
|
|
259
|
+
action: "restore",
|
|
260
|
+
name: args.name,
|
|
261
|
+
url: checkpoint.url,
|
|
262
|
+
title: checkpoint.title,
|
|
263
|
+
restoredTimestamp: checkpoint.timestamp,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
error_code: "INVALID_ACTION",
|
|
269
|
+
message: `Unsupported action '${args.action}'. Supported actions: save, restore, list, delete.`,
|
|
270
|
+
};
|
|
271
|
+
}
|
package/dist/tools/click.d.ts
CHANGED
package/dist/tools/click.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/tools/click.ts
|
|
2
2
|
// Priority 2: Sub-10ms Click Path & Predictive Action Queue
|
|
3
|
-
import { buildActionMap, findAllMatches, safeSelector, toMarkdownTable, resolveByAria } from "../cdp/walker.js";
|
|
3
|
+
import { buildActionMap, findAllMatches, safeSelector, toMarkdownTable, resolveByAria, recoverDetachedRef } from "../cdp/walker.js";
|
|
4
4
|
import { logAction } from "../audit/logger.js";
|
|
5
5
|
import { recordAction, getLoopDiagnostics } from "../intelligence/trail.js";
|
|
6
6
|
import { waitForDOMStable } from "../sensors/guard.js";
|
|
@@ -15,6 +15,7 @@ import { sendToExtension } from "../cdp/helpers.js";
|
|
|
15
15
|
import { labelCache } from "../cdp/label_cache.js";
|
|
16
16
|
import { getBestSelectorForIntent, logMemorySuccess, logMemoryFailure } from "../intelligence/sessionMemory.js";
|
|
17
17
|
import { verifyActionability, quickHitCheck } from "../sensors/actionability.js";
|
|
18
|
+
import { marksGet } from "../intelligence/marks.js";
|
|
18
19
|
/** Converts SelectorFingerprint (sessionMemory) to the format verifyActionability expects. */
|
|
19
20
|
function fpToOpts(fingerprint) {
|
|
20
21
|
if (!fingerprint)
|
|
@@ -34,6 +35,36 @@ export async function weaveClick(session, args, config) {
|
|
|
34
35
|
const startMs = Date.now();
|
|
35
36
|
const clientMatch = sessionRegistry.getAll().find(c => c.session === session);
|
|
36
37
|
const targetId = clientMatch?.targetId ?? session.targetId ?? "default";
|
|
38
|
+
// ── MARK FAST PATH: resolve by SoM mark number ─────────────────────────────
|
|
39
|
+
// Highest priority — zero DOM walk, straight coordinate dispatch.
|
|
40
|
+
if (args.mark !== undefined) {
|
|
41
|
+
const entry = marksGet(targetId, args.mark);
|
|
42
|
+
if (!entry) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
error_code: "MARK_NOT_FOUND",
|
|
46
|
+
message: `[!] Mark [${args.mark}] not found. Call browser_map({ visual: true }) or browser_map({ visual: "auto" }) first to generate marks.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Ghost-move to coordinates, then dispatch hardware click
|
|
50
|
+
await ghostMove(session, entry.x, entry.y, config).catch(() => { });
|
|
51
|
+
await Input.dispatchMouseEvent({ type: "mousePressed", x: entry.x, y: entry.y, button: "left", clickCount: 1 });
|
|
52
|
+
await Input.dispatchMouseEvent({ type: "mouseReleased", x: entry.x, y: entry.y, button: "left", clickCount: 1 });
|
|
53
|
+
const initialUrl = (await Page.getFrameTree()).frameTree.frame.url;
|
|
54
|
+
logAction("browser_click", initialUrl, `mark:${args.mark} @(${entry.x},${entry.y})`);
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
clicked: entry.label ?? `mark:${args.mark}`,
|
|
58
|
+
ref: entry.ref ?? `mark:${args.mark}`,
|
|
59
|
+
execution_ms: Date.now() - startMs,
|
|
60
|
+
tab_id: targetId,
|
|
61
|
+
url: initialUrl,
|
|
62
|
+
title: "",
|
|
63
|
+
url_changed: false,
|
|
64
|
+
dom_changed: false,
|
|
65
|
+
stuck_warning: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
37
68
|
// Track URL before click
|
|
38
69
|
const initialUrl = (await Page.getFrameTree()).frameTree.frame.url;
|
|
39
70
|
// 1. Resolve Element and Selector
|
|
@@ -56,8 +87,8 @@ export async function weaveClick(session, args, config) {
|
|
|
56
87
|
if (memHit) {
|
|
57
88
|
const actCheck = await verifyActionability(session, memHit.selector, {
|
|
58
89
|
fingerprint: fpToOpts(memHit.fingerprint),
|
|
59
|
-
stabilityTimeoutMs: config.actionabilityTimeoutMs ??
|
|
60
|
-
totalTimeoutMs: (config.actionabilityTimeoutMs ??
|
|
90
|
+
stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 200,
|
|
91
|
+
totalTimeoutMs: (config.actionabilityTimeoutMs ?? 200) * 3,
|
|
61
92
|
});
|
|
62
93
|
if (actCheck.status === 'ACTIONABLE') {
|
|
63
94
|
args.id = memHit.selector;
|
|
@@ -101,6 +132,14 @@ export async function weaveClick(session, args, config) {
|
|
|
101
132
|
cacheActionMap(targetId, map);
|
|
102
133
|
element = map.elements.find((el) => el.id === reqId);
|
|
103
134
|
}
|
|
135
|
+
// If still not found, trigger the SPA auto-recovery heuristic before hard-failing
|
|
136
|
+
if (!element && reqId.startsWith("w:")) {
|
|
137
|
+
const recovery = await recoverDetachedRef(session, targetId, reqId);
|
|
138
|
+
if (recovery.recovered && recovery.element) {
|
|
139
|
+
element = recovery.element;
|
|
140
|
+
sniperWarning = recovery.warning || "";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
104
143
|
// Fast-fail if still not found
|
|
105
144
|
if (!element && reqId.startsWith("w:")) {
|
|
106
145
|
return {
|
|
@@ -210,7 +249,7 @@ export async function weaveClick(session, args, config) {
|
|
|
210
249
|
}
|
|
211
250
|
const loopDiag = getLoopDiagnostics("browser_click", element.id);
|
|
212
251
|
if (loopDiag.severity === "critical") {
|
|
213
|
-
return { success: false, error_code: "STUCK_LOOP_WARNING", message: `[!] Loop detected: You have tried to click [${element.id}] ${loopDiag.strikes} times without success.
|
|
252
|
+
return { success: false, error_code: "STUCK_LOOP_WARNING", message: `[!] Loop detected: You have tried to click [${element.id}] ${loopDiag.strikes} times without success. Run browser_map({ force: true }) if this is a new task.` };
|
|
214
253
|
}
|
|
215
254
|
if (loopDiag.severity === "warning") {
|
|
216
255
|
logAction("browser_click", element.id, `loop warning: ${loopDiag.strikes} consecutive strikes on [${element.id}]`);
|
|
@@ -220,8 +259,8 @@ export async function weaveClick(session, args, config) {
|
|
|
220
259
|
const selector = safeSelector(element);
|
|
221
260
|
if (selector) {
|
|
222
261
|
const actCheck = await verifyActionability(session, selector, {
|
|
223
|
-
stabilityTimeoutMs: config.actionabilityTimeoutMs ??
|
|
224
|
-
totalTimeoutMs: (config.actionabilityTimeoutMs ??
|
|
262
|
+
stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 200,
|
|
263
|
+
totalTimeoutMs: (config.actionabilityTimeoutMs ?? 200) * 3,
|
|
225
264
|
});
|
|
226
265
|
if (actCheck.status === 'ACTIONABLE') {
|
|
227
266
|
coords = { x: actCheck.x, y: actCheck.y, visible: true };
|
|
@@ -236,7 +275,20 @@ export async function weaveClick(session, args, config) {
|
|
|
236
275
|
}
|
|
237
276
|
}
|
|
238
277
|
else if (actCheck.status === 'NOT_FOUND') {
|
|
239
|
-
|
|
278
|
+
if (element.id.startsWith("w:")) {
|
|
279
|
+
const recovery = await recoverDetachedRef(session, targetId, element.id, element);
|
|
280
|
+
if (recovery.recovered && recovery.element) {
|
|
281
|
+
element = recovery.element;
|
|
282
|
+
coords = { x: element.x, y: element.y, visible: true };
|
|
283
|
+
sniperWarning = recovery.warning || "";
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
return { success: false, error_code: "ELEMENT_NOT_FOUND", message: `[!] Element ${selector} was in the cached map but no longer exists in the DOM. Run browser_map again to refresh refs.` };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
return { success: false, error_code: "ELEMENT_NOT_FOUND", message: `[!] Element ${selector} was in the cached map but no longer exists in the DOM. Run browser_map again to refresh refs.` };
|
|
291
|
+
}
|
|
240
292
|
}
|
|
241
293
|
else if (actCheck.status === 'INVISIBLE') {
|
|
242
294
|
return { success: false, error_code: "ELEMENT_INVISIBLE", message: `[!] Element ${selector} has zero dimensions (hidden or collapsed). Use browser_inspect to check display/visibility, or scroll to reveal it.` };
|
|
@@ -272,15 +324,15 @@ export async function weaveClick(session, args, config) {
|
|
|
272
324
|
async function performClick(cx, cy) {
|
|
273
325
|
await ghostMove(session, cx, cy, config, element_.label);
|
|
274
326
|
sendToExtension(session, { type: 'Weavetab_STATE', cursor: { x: cx, y: cy }, state: 'clicking', ripple: { cx, cy } }).catch(() => { });
|
|
275
|
-
// ── Pre-dispatch hit-target check (
|
|
276
|
-
//
|
|
277
|
-
//
|
|
327
|
+
// ── Pre-dispatch hit-target check (lightweight) ─────────────────────────
|
|
328
|
+
// verifyActionability already confirmed actionable; this is a last-chance race guard.
|
|
329
|
+
// We do NOT throw here — transient tooltips/overlays cause false positives.
|
|
330
|
+
// If genuinely blocked, the post-click stuck detection handles the fallback.
|
|
278
331
|
const targetSel = safeSelector(element_) ?? "";
|
|
279
332
|
if (targetSel) {
|
|
280
333
|
const hitCheck = await quickHitCheck(session, cx, cy, targetSel);
|
|
281
334
|
if (!hitCheck.clear) {
|
|
282
|
-
|
|
283
|
-
throw Object.assign(new Error(hitCheck.blockerDescription ?? "hit-target blocked"), { code: "HIT_TARGET_BLOCKED" });
|
|
335
|
+
logAction("browser_click", targetSel, `[quickHitCheck] soft-blocked by ${hitCheck.blockerDescription ?? "unknown"} — proceeding anyway`);
|
|
284
336
|
}
|
|
285
337
|
}
|
|
286
338
|
if (!config.headless)
|
|
@@ -299,13 +351,13 @@ export async function weaveClick(session, args, config) {
|
|
|
299
351
|
// Check if a dialog was opened by the click immediately. If a dialog is open,
|
|
300
352
|
// the click succeeded in triggering an action. Waiting for DOM stable will
|
|
301
353
|
// just hang for 5-10s because Runtime.evaluate blocks on JS dialogs.
|
|
302
|
-
await new Promise(r => setTimeout(r,
|
|
354
|
+
await new Promise(r => setTimeout(r, 30)); // give dialog time to trigger
|
|
303
355
|
if (hasOpenDialog(session)) {
|
|
304
356
|
return { stuck: false, url: currentUrl, newMap: null, dialog_open: true };
|
|
305
357
|
}
|
|
306
358
|
await Promise.all([
|
|
307
|
-
waitForDOMStable(session,
|
|
308
|
-
waitForNetworkIdle(session,
|
|
359
|
+
waitForDOMStable(session, 20, 150),
|
|
360
|
+
waitForNetworkIdle(session, 30, 150).catch(() => { })
|
|
309
361
|
]);
|
|
310
362
|
const finalUrl = (await Page.getFrameTree()).frameTree.frame.url;
|
|
311
363
|
// Fast path: URL changed → definitely not stuck, skip map rebuild
|
|
@@ -445,63 +497,15 @@ export async function weaveClick(session, args, config) {
|
|
|
445
497
|
const stuck = false;
|
|
446
498
|
return { stuck, url: finalUrl, newMap };
|
|
447
499
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
try {
|
|
451
|
-
await performClick(clickX, clickY);
|
|
452
|
-
}
|
|
453
|
-
catch (err) {
|
|
454
|
-
if (err?.code === "HIT_TARGET_BLOCKED") {
|
|
455
|
-
hitTargetBlocked = true;
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
throw err;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
500
|
+
const { x: clickX, y: clickY } = coords;
|
|
501
|
+
await performClick(clickX, clickY);
|
|
461
502
|
// ── Post-click: get page state (URL, map, DOM delta) ─────────────────────
|
|
462
503
|
// The 'stuck' flag is purely informational — it powers stuck_warning in the
|
|
463
|
-
// result. A dispatched
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
// click on checkboxes/accordions, net-zeroing
|
|
467
|
-
|
|
468
|
-
? { stuck: true, url: initialUrl, newMap: null }
|
|
469
|
-
: await isClickStuck(initialUrl);
|
|
470
|
-
// SPA JS-click fallback: only when the first click was genuinely hit-blocked
|
|
471
|
-
if (hitTargetBlocked && element_) {
|
|
472
|
-
const sel = safeSelector(element_);
|
|
473
|
-
if (sel) {
|
|
474
|
-
try {
|
|
475
|
-
const { result: isButton } = await session.Runtime.evaluate({
|
|
476
|
-
expression: `(function(s) {
|
|
477
|
-
var el = document.querySelector(s);
|
|
478
|
-
if (!el) return false;
|
|
479
|
-
var tag = el.tagName.toLowerCase();
|
|
480
|
-
var role = (el.getAttribute('role') || '').toLowerCase();
|
|
481
|
-
if (tag === 'button' || tag === 'a' || tag === 'input' || tag === 'label' || tag === 'select') return true;
|
|
482
|
-
if (['button','link','menuitem','menuitemcheckbox','menuitemradio',
|
|
483
|
-
'radio','checkbox','tab','option','treeitem','gridcell','switch'].includes(role)) return true;
|
|
484
|
-
if (el.onclick || el.getAttribute('onclick')) return true;
|
|
485
|
-
if (el.isContentEditable || tag === 'summary') return true;
|
|
486
|
-
return false;
|
|
487
|
-
})(${JSON.stringify(sel)})`,
|
|
488
|
-
returnByValue: true,
|
|
489
|
-
}).catch(() => ({ result: { value: false } }));
|
|
490
|
-
if (isButton?.value) {
|
|
491
|
-
await session.Runtime.evaluate({
|
|
492
|
-
expression: `(function(s) {
|
|
493
|
-
var el = document.querySelector(s);
|
|
494
|
-
if (el) { el.focus(); el.click(); }
|
|
495
|
-
})(${JSON.stringify(sel)})`,
|
|
496
|
-
returnByValue: false,
|
|
497
|
-
}).catch(() => { });
|
|
498
|
-
await new Promise(r => setTimeout(r, 300));
|
|
499
|
-
postCheck = { stuck: false, url: initialUrl, newMap: null };
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
catch { /* non-fatal */ }
|
|
503
|
-
}
|
|
504
|
-
}
|
|
504
|
+
// result. A dispatched click is NOT retried based on DOM delta alone, because:
|
|
505
|
+
// (a) many real interactions produce no measurable DOM delta (static buttons,
|
|
506
|
+
// CSS-only toggles, anchor scrolls);
|
|
507
|
+
// (b) retrying doubles the click on checkboxes/accordions, net-zeroing it.
|
|
508
|
+
const postCheck = await isClickStuck(initialUrl);
|
|
505
509
|
const executionMs = Date.now() - startMs;
|
|
506
510
|
const { stuck: isStuck, url: finalUrl, newMap } = postCheck;
|
|
507
511
|
if (neededScroll) {
|
|
@@ -630,7 +630,7 @@ ${tailwindPath ? "4. `tailwind.theme.json` — Tailwind CSS `theme.extend` confi
|
|
|
630
630
|
htmlBytes: beautifulHtml.length
|
|
631
631
|
}
|
|
632
632
|
},
|
|
633
|
-
message: `
|
|
633
|
+
message: `design saved to ${targetDir}. Saved tokens.json, template.html, styles.css, and README.md.`
|
|
634
634
|
};
|
|
635
635
|
}
|
|
636
636
|
finally {
|
package/dist/tools/eval.js
CHANGED
|
@@ -77,7 +77,7 @@ export async function browserEval(session, options, _config) {
|
|
|
77
77
|
// result.value is undefined and result.description is unhelpfully a type string.
|
|
78
78
|
// Return a clear message so agents know the eval succeeded — it just returned no value.
|
|
79
79
|
const rawResult = result.type === 'undefined'
|
|
80
|
-
? "(
|
|
80
|
+
? "(eval returned undefined. append ; 'ok' or return a string explicitly.)"
|
|
81
81
|
: result.value !== undefined ? result.value : result.description;
|
|
82
82
|
const redactedResult = typeof rawResult === 'string'
|
|
83
83
|
? SessionSecretTracker.redact(targetId, rawResult)
|
package/dist/tools/fill.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import CDP from "chrome-remote-interface";
|
|
2
2
|
import type { Config } from "../config/loader.js";
|
|
3
3
|
export interface FillField {
|
|
4
|
-
ref
|
|
4
|
+
ref?: string;
|
|
5
|
+
selector?: string;
|
|
6
|
+
target?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
placeholder?: string;
|
|
5
10
|
value: string;
|
|
6
11
|
}
|
|
7
12
|
/** A single cell write for Google Sheets / Canvas grid filling. */
|
|
@@ -15,6 +20,8 @@ export interface FillOptions {
|
|
|
15
20
|
fields?: FillField[];
|
|
16
21
|
submit?: string;
|
|
17
22
|
scan_only?: boolean;
|
|
23
|
+
scan?: boolean;
|
|
24
|
+
inputs?: boolean;
|
|
18
25
|
blast_mode?: boolean;
|
|
19
26
|
/** Grid-fill mode for Google Sheets and other canvas-rendered spreadsheets.
|
|
20
27
|
* Provide cell addresses in A1 notation with their values.
|
|
@@ -27,6 +34,18 @@ export interface FillResult {
|
|
|
27
34
|
succeeded: number;
|
|
28
35
|
failed: number;
|
|
29
36
|
submitted: boolean;
|
|
37
|
+
inputs?: Array<{
|
|
38
|
+
ref: string;
|
|
39
|
+
tag: string;
|
|
40
|
+
type: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
label?: string;
|
|
43
|
+
placeholder?: string;
|
|
44
|
+
value: string;
|
|
45
|
+
focused?: boolean;
|
|
46
|
+
disabled?: boolean;
|
|
47
|
+
visible?: boolean;
|
|
48
|
+
}>;
|
|
30
49
|
results: {
|
|
31
50
|
ref: string;
|
|
32
51
|
field_type: string;
|