@weavetab/mcp 2.5.0-beta.1 → 2.5.0-beta.4
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 +2 -1
- package/ARCHITECTURE.md +6 -6
- package/LICENSE +1 -1
- package/README.md +164 -802
- package/SECURITY.md +33 -22
- package/TROUBLESHOOTING.md +3 -3
- package/dist/cdp/bridge.d.ts +10 -0
- package/dist/cdp/bridge.js +285 -78
- package/dist/cdp/connector.d.ts +7 -3
- package/dist/cdp/connector.js +196 -75
- package/dist/cdp/helpers.d.ts +4 -0
- package/dist/cdp/helpers.js +181 -0
- package/dist/cdp/lock.d.ts +9 -6
- package/dist/cdp/lock.js +72 -59
- package/dist/cdp/omni_broker.d.ts +2 -1
- package/dist/cdp/omni_broker.js +13 -7
- package/dist/cdp/profiles.d.ts +10 -0
- package/dist/cdp/profiles.js +65 -0
- package/dist/cdp/walker.d.ts +25 -1
- package/dist/cdp/walker.js +354 -7
- package/dist/cli/plugin.d.ts +0 -2
- package/dist/cli/plugin.js +51 -696
- package/dist/cli/updater.d.ts +1 -1
- package/dist/cli/updater.js +27 -7
- package/dist/config/loader.d.ts +33 -6
- package/dist/config/loader.js +174 -22
- package/dist/desktop/discovery.d.ts +44 -0
- package/dist/desktop/discovery.js +364 -0
- package/dist/index.js +140 -64
- 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/ghost.js +2 -2
- package/dist/overlay/injector.js +3 -5
- package/dist/plugin/extension_bridge.d.ts +40 -0
- package/dist/plugin/extension_bridge.js +158 -0
- package/dist/plugin/interface.d.ts +151 -15
- package/dist/plugin/loader.d.ts +19 -1
- package/dist/plugin/loader.js +331 -63
- package/dist/plugin/pipeline.d.ts +75 -0
- package/dist/plugin/pipeline.js +183 -0
- package/dist/plugin/registry.js +12 -1
- package/dist/plugin/storage.d.ts +51 -0
- package/dist/plugin/storage.js +164 -0
- package/dist/security/secrets.d.ts +1 -0
- package/dist/security/secrets.js +13 -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 +600 -236
- 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 +8 -2
- package/dist/state/session.js +60 -13
- package/dist/tools/browser_detect.d.ts +16 -2
- package/dist/tools/browser_detect.js +54 -6
- package/dist/tools/burst.d.ts +17 -7
- package/dist/tools/burst.js +240 -83
- 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 +97 -98
- package/dist/tools/clipboard.js +31 -24
- 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 +57 -44
- 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.d.ts +10 -10
- package/dist/tools/tabs.js +55 -39
- 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 +109 -60
- 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 +82 -36
- package/docs/PLUGINS.md +82 -499
- package/docs/TOOLS.md +51 -99
- package/docs/architecture/blind-injection.md +83 -0
- package/docs/architecture/dual-injection.md +32 -0
- package/docs/architecture/overview.md +49 -0
- package/docs/architecture/security.md +35 -0
- package/docs/architecture/transport.md +37 -0
- package/docs/captcha-stealth/captcha-solver.md +75 -0
- package/docs/captcha-stealth/stealth-physics.md +60 -0
- package/docs/cli/cli-commands.md +44 -0
- package/docs/cli/cli-flags.md +20 -0
- package/docs/cli/cli-introduction.md +20 -0
- package/docs/cli/cli-scripting.md +32 -0
- package/docs/cli/g-ide/ide-antigravity.md +24 -0
- package/docs/cli/g-ide/ide-claude-code.md +24 -0
- package/docs/cli/g-ide/ide-claude-desktop.md +32 -0
- package/docs/cli/g-ide/ide-cursor.md +31 -0
- package/docs/cli/g-ide/ide-generic.md +34 -0
- package/docs/cli/g-ide/ide-opencode.md +24 -0
- package/docs/cli/g-ide/ide-vscode-copilot.md +24 -0
- package/docs/cli/g-ide/ide-windsurf.md +24 -0
- package/docs/configuration/config-locations.md +24 -0
- package/docs/configuration/configuration.md +73 -0
- package/docs/configuration/env-variables.md +19 -0
- package/docs/configuration/folders.md +55 -0
- package/docs/desktop-automation/electron-apps.md +217 -0
- package/docs/get-started/installation.md +47 -0
- package/docs/get-started/introduction.md +60 -0
- package/docs/get-started/quickstart.md +54 -0
- package/docs/get-started/setup-wizard.md +26 -0
- package/docs/intelligence/site-profiles.md +151 -0
- package/docs/skills-ecosystem/skills-overview.md +35 -0
- package/docs/skills-ecosystem/skills-recipes.md +36 -0
- package/docs/tools/tools.md +631 -0
- package/docs/troubleshooting/trouble-browser.md +27 -0
- package/docs/troubleshooting/trouble-config.md +20 -0
- package/docs/troubleshooting/trouble-loop.md +31 -0
- package/ext-dist/background.js +10 -62
- package/ext-dist/content.js +333 -921
- package/ext-dist/hud/hud.js +9 -9
- package/ext-dist/manifest.json +2 -6
- package/ext-dist/styles.css +6 -1
- package/mcp.json +108 -72
- package/package.json +14 -9
- package/server.json +3 -3
- package/docs/DEV_MODE.md +0 -68
- package/docs/dev_plugin.md +0 -310
- 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)
|
|
@@ -32,7 +33,38 @@ export async function weaveClick(session, args, config) {
|
|
|
32
33
|
const { id, label, intent, backendNodeId: directNodeId } = args;
|
|
33
34
|
const { Page, DOM, Input } = session;
|
|
34
35
|
const startMs = Date.now();
|
|
35
|
-
const
|
|
36
|
+
const clientMatch = sessionRegistry.getAll().find(c => c.session === session);
|
|
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
|
+
}
|
|
36
68
|
// Track URL before click
|
|
37
69
|
const initialUrl = (await Page.getFrameTree()).frameTree.frame.url;
|
|
38
70
|
// 1. Resolve Element and Selector
|
|
@@ -55,8 +87,8 @@ export async function weaveClick(session, args, config) {
|
|
|
55
87
|
if (memHit) {
|
|
56
88
|
const actCheck = await verifyActionability(session, memHit.selector, {
|
|
57
89
|
fingerprint: fpToOpts(memHit.fingerprint),
|
|
58
|
-
stabilityTimeoutMs: config.actionabilityTimeoutMs ??
|
|
59
|
-
totalTimeoutMs: (config.actionabilityTimeoutMs ??
|
|
90
|
+
stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 200,
|
|
91
|
+
totalTimeoutMs: (config.actionabilityTimeoutMs ?? 200) * 3,
|
|
60
92
|
});
|
|
61
93
|
if (actCheck.status === 'ACTIONABLE') {
|
|
62
94
|
args.id = memHit.selector;
|
|
@@ -92,38 +124,50 @@ export async function weaveClick(session, args, config) {
|
|
|
92
124
|
if (!usedMemoryIntent) {
|
|
93
125
|
const query = label || intent;
|
|
94
126
|
if (args.id) {
|
|
95
|
-
const
|
|
96
|
-
element = map.elements.find((el) => el.id ===
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
const reqId = args.id;
|
|
128
|
+
element = map.elements.find((el) => el.id === reqId);
|
|
129
|
+
// If not found in cache, rebuild map once to catch recent DOM updates or cache misses
|
|
130
|
+
if (!element && reqId.startsWith("w:")) {
|
|
131
|
+
map = await buildActionMap(session, { lite: true }, targetId);
|
|
132
|
+
cacheActionMap(targetId, map);
|
|
133
|
+
element = map.elements.find((el) => el.id === reqId);
|
|
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
|
+
}
|
|
143
|
+
// Fast-fail if still not found
|
|
144
|
+
if (!element && reqId.startsWith("w:")) {
|
|
101
145
|
return {
|
|
102
146
|
success: false,
|
|
103
147
|
error_code: "ELEMENT_NOT_FOUND",
|
|
104
|
-
message: `[!] Ref '${
|
|
148
|
+
message: `[!] Ref '${reqId}' is no longer in the current page map — the DOM was updated since browser_map was called. Call browser_map() to get fresh ref IDs.`,
|
|
105
149
|
};
|
|
106
150
|
}
|
|
107
|
-
if (!element && (
|
|
108
|
-
const actCheck = await verifyActionability(session,
|
|
151
|
+
if (!element && (reqId.startsWith("#") || reqId.startsWith(".") || reqId.includes("[") || reqId.includes(">") || reqId.includes(" "))) {
|
|
152
|
+
const actCheck = await verifyActionability(session, reqId, {
|
|
109
153
|
stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 400,
|
|
110
154
|
totalTimeoutMs: (config.actionabilityTimeoutMs ?? 400) * 3,
|
|
111
155
|
});
|
|
112
156
|
if (actCheck.status === 'ACTIONABLE') {
|
|
113
157
|
element = {
|
|
114
|
-
id:
|
|
158
|
+
id: reqId,
|
|
115
159
|
label: 'selector-match',
|
|
116
160
|
x: actCheck.x,
|
|
117
161
|
y: actCheck.y,
|
|
118
|
-
selector:
|
|
162
|
+
selector: reqId,
|
|
119
163
|
role: "button",
|
|
120
164
|
group: "utility",
|
|
121
165
|
depth: 0,
|
|
122
|
-
fingerprint: "selector:" +
|
|
166
|
+
fingerprint: "selector:" + reqId,
|
|
123
167
|
};
|
|
124
168
|
}
|
|
125
169
|
else if (actCheck.status === 'OCCLUDED' && !actCheck.occludedBy?.isWeavetabOwned) {
|
|
126
|
-
return { success: false, error_code: "ELEMENT_OCCLUDED_BY", message: `[!] ${describeOccluder(actCheck.occludedBy)} — element ${
|
|
170
|
+
return { success: false, error_code: "ELEMENT_OCCLUDED_BY", message: `[!] ${describeOccluder(actCheck.occludedBy)} — element ${reqId} is blocked.` };
|
|
127
171
|
}
|
|
128
172
|
}
|
|
129
173
|
}
|
|
@@ -205,7 +249,7 @@ export async function weaveClick(session, args, config) {
|
|
|
205
249
|
}
|
|
206
250
|
const loopDiag = getLoopDiagnostics("browser_click", element.id);
|
|
207
251
|
if (loopDiag.severity === "critical") {
|
|
208
|
-
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.` };
|
|
209
253
|
}
|
|
210
254
|
if (loopDiag.severity === "warning") {
|
|
211
255
|
logAction("browser_click", element.id, `loop warning: ${loopDiag.strikes} consecutive strikes on [${element.id}]`);
|
|
@@ -215,8 +259,8 @@ export async function weaveClick(session, args, config) {
|
|
|
215
259
|
const selector = safeSelector(element);
|
|
216
260
|
if (selector) {
|
|
217
261
|
const actCheck = await verifyActionability(session, selector, {
|
|
218
|
-
stabilityTimeoutMs: config.actionabilityTimeoutMs ??
|
|
219
|
-
totalTimeoutMs: (config.actionabilityTimeoutMs ??
|
|
262
|
+
stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 200,
|
|
263
|
+
totalTimeoutMs: (config.actionabilityTimeoutMs ?? 200) * 3,
|
|
220
264
|
});
|
|
221
265
|
if (actCheck.status === 'ACTIONABLE') {
|
|
222
266
|
coords = { x: actCheck.x, y: actCheck.y, visible: true };
|
|
@@ -231,7 +275,20 @@ export async function weaveClick(session, args, config) {
|
|
|
231
275
|
}
|
|
232
276
|
}
|
|
233
277
|
else if (actCheck.status === 'NOT_FOUND') {
|
|
234
|
-
|
|
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
|
+
}
|
|
235
292
|
}
|
|
236
293
|
else if (actCheck.status === 'INVISIBLE') {
|
|
237
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.` };
|
|
@@ -267,15 +324,15 @@ export async function weaveClick(session, args, config) {
|
|
|
267
324
|
async function performClick(cx, cy) {
|
|
268
325
|
await ghostMove(session, cx, cy, config, element_.label);
|
|
269
326
|
sendToExtension(session, { type: 'Weavetab_STATE', cursor: { x: cx, y: cy }, state: 'clicking', ripple: { cx, cy } }).catch(() => { });
|
|
270
|
-
// ── Pre-dispatch hit-target check (
|
|
271
|
-
//
|
|
272
|
-
//
|
|
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.
|
|
273
331
|
const targetSel = safeSelector(element_) ?? "";
|
|
274
332
|
if (targetSel) {
|
|
275
333
|
const hitCheck = await quickHitCheck(session, cx, cy, targetSel);
|
|
276
334
|
if (!hitCheck.clear) {
|
|
277
|
-
|
|
278
|
-
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`);
|
|
279
336
|
}
|
|
280
337
|
}
|
|
281
338
|
if (!config.headless)
|
|
@@ -294,13 +351,13 @@ export async function weaveClick(session, args, config) {
|
|
|
294
351
|
// Check if a dialog was opened by the click immediately. If a dialog is open,
|
|
295
352
|
// the click succeeded in triggering an action. Waiting for DOM stable will
|
|
296
353
|
// just hang for 5-10s because Runtime.evaluate blocks on JS dialogs.
|
|
297
|
-
await new Promise(r => setTimeout(r,
|
|
354
|
+
await new Promise(r => setTimeout(r, 30)); // give dialog time to trigger
|
|
298
355
|
if (hasOpenDialog(session)) {
|
|
299
356
|
return { stuck: false, url: currentUrl, newMap: null, dialog_open: true };
|
|
300
357
|
}
|
|
301
358
|
await Promise.all([
|
|
302
|
-
waitForDOMStable(session,
|
|
303
|
-
waitForNetworkIdle(session,
|
|
359
|
+
waitForDOMStable(session, 20, 150),
|
|
360
|
+
waitForNetworkIdle(session, 30, 150).catch(() => { })
|
|
304
361
|
]);
|
|
305
362
|
const finalUrl = (await Page.getFrameTree()).frameTree.frame.url;
|
|
306
363
|
// Fast path: URL changed → definitely not stuck, skip map rebuild
|
|
@@ -440,63 +497,15 @@ export async function weaveClick(session, args, config) {
|
|
|
440
497
|
const stuck = false;
|
|
441
498
|
return { stuck, url: finalUrl, newMap };
|
|
442
499
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
try {
|
|
446
|
-
await performClick(clickX, clickY);
|
|
447
|
-
}
|
|
448
|
-
catch (err) {
|
|
449
|
-
if (err?.code === "HIT_TARGET_BLOCKED") {
|
|
450
|
-
hitTargetBlocked = true;
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
throw err;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
500
|
+
const { x: clickX, y: clickY } = coords;
|
|
501
|
+
await performClick(clickX, clickY);
|
|
456
502
|
// ── Post-click: get page state (URL, map, DOM delta) ─────────────────────
|
|
457
503
|
// The 'stuck' flag is purely informational — it powers stuck_warning in the
|
|
458
|
-
// result. A dispatched
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
// click on checkboxes/accordions, net-zeroing
|
|
462
|
-
|
|
463
|
-
? { stuck: true, url: initialUrl, newMap: null }
|
|
464
|
-
: await isClickStuck(initialUrl);
|
|
465
|
-
// SPA JS-click fallback: only when the first click was genuinely hit-blocked
|
|
466
|
-
if (hitTargetBlocked && element_) {
|
|
467
|
-
const sel = safeSelector(element_);
|
|
468
|
-
if (sel) {
|
|
469
|
-
try {
|
|
470
|
-
const { result: isButton } = await session.Runtime.evaluate({
|
|
471
|
-
expression: `(function(s) {
|
|
472
|
-
var el = document.querySelector(s);
|
|
473
|
-
if (!el) return false;
|
|
474
|
-
var tag = el.tagName.toLowerCase();
|
|
475
|
-
var role = (el.getAttribute('role') || '').toLowerCase();
|
|
476
|
-
if (tag === 'button' || tag === 'a' || tag === 'input' || tag === 'label' || tag === 'select') return true;
|
|
477
|
-
if (['button','link','menuitem','menuitemcheckbox','menuitemradio',
|
|
478
|
-
'radio','checkbox','tab','option','treeitem','gridcell','switch'].includes(role)) return true;
|
|
479
|
-
if (el.onclick || el.getAttribute('onclick')) return true;
|
|
480
|
-
if (el.isContentEditable || tag === 'summary') return true;
|
|
481
|
-
return false;
|
|
482
|
-
})(${JSON.stringify(sel)})`,
|
|
483
|
-
returnByValue: true,
|
|
484
|
-
}).catch(() => ({ result: { value: false } }));
|
|
485
|
-
if (isButton?.value) {
|
|
486
|
-
await session.Runtime.evaluate({
|
|
487
|
-
expression: `(function(s) {
|
|
488
|
-
var el = document.querySelector(s);
|
|
489
|
-
if (el) { el.focus(); el.click(); }
|
|
490
|
-
})(${JSON.stringify(sel)})`,
|
|
491
|
-
returnByValue: false,
|
|
492
|
-
}).catch(() => { });
|
|
493
|
-
await new Promise(r => setTimeout(r, 300));
|
|
494
|
-
postCheck = { stuck: false, url: initialUrl, newMap: null };
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
catch { /* non-fatal */ }
|
|
498
|
-
}
|
|
499
|
-
}
|
|
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);
|
|
500
509
|
const executionMs = Date.now() - startMs;
|
|
501
510
|
const { stuck: isStuck, url: finalUrl, newMap } = postCheck;
|
|
502
511
|
if (neededScroll) {
|
|
@@ -532,28 +541,18 @@ export async function weaveClick(session, args, config) {
|
|
|
532
541
|
input: { ref: element_?.id ?? args.id ?? args.label, label: element_?.label },
|
|
533
542
|
result: { success: true, stuck: isStuck },
|
|
534
543
|
});
|
|
535
|
-
if (isStuck) {
|
|
536
|
-
return {
|
|
537
|
-
success: false,
|
|
538
|
-
error_code: "CLICK_NO_EFFECT",
|
|
539
|
-
message: "The click executed but resulted in no visible changes to the page. It may have been intercepted by an overlay, or the element might require a different interaction.",
|
|
540
|
-
suggestion: "Try looking for an invisible overlay blocking the click, or try using browser_eval to click it directly.",
|
|
541
|
-
tab_id: targetId,
|
|
542
|
-
url: finalUrl,
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
544
|
return {
|
|
546
545
|
success: true,
|
|
547
|
-
clicked: element_.label,
|
|
548
|
-
ref: element_.id,
|
|
546
|
+
clicked: element_?.label || (coords ? `coords(${coords.x},${coords.y})` : (args.label || args.id || "")),
|
|
547
|
+
ref: element_?.id || (coords ? "coords" : (args.id || "unknown")),
|
|
549
548
|
execution_ms: executionMs,
|
|
550
549
|
tab_id: targetId,
|
|
551
550
|
url: finalUrl,
|
|
552
551
|
title: newMap?.title || "",
|
|
553
552
|
url_changed: finalUrl !== initialUrl,
|
|
554
553
|
...(args.include_map ? { page_map: newMap ? toMarkdownTable(newMap.elements, finalUrl, newMap.title) : "" } : {}),
|
|
555
|
-
dom_changed:
|
|
556
|
-
stuck_warning:
|
|
554
|
+
dom_changed: !isStuck,
|
|
555
|
+
stuck_warning: isStuck,
|
|
557
556
|
...(stuckHint ? { stuck_hint: stuckHint } : {}),
|
|
558
557
|
...(sniperWarning ? { sniper_warning: sniperWarning } : {}),
|
|
559
558
|
...(postCheck.canvas_hint ? { canvas_hint: postCheck.canvas_hint } : {})
|
package/dist/tools/clipboard.js
CHANGED
|
@@ -69,32 +69,30 @@ export async function browserClipboard(session, options, _config) {
|
|
|
69
69
|
throw new Error("Failed to write image to clipboard: " + (val || "unknown"));
|
|
70
70
|
}
|
|
71
71
|
// Text-only write
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
72
|
+
if (hasText && !hasImage) {
|
|
73
|
+
try {
|
|
74
|
+
const clipboardy = (await import("clipboardy")).default;
|
|
75
|
+
await clipboardy.write(text);
|
|
76
|
+
return { success: true, message: "Copied to clipboard" };
|
|
77
|
+
}
|
|
78
|
+
catch { /* fallback to browser API */ }
|
|
79
|
+
const res = await session.Runtime.evaluate({
|
|
80
|
+
expression: `(async function() {
|
|
81
|
+
try {
|
|
82
|
+
${secureCtx ? `await navigator.clipboard.writeText(${JSON.stringify(text)});` : `throw new Error('non-secure');`}
|
|
83
|
+
return true;
|
|
84
|
+
} catch(e) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
})()`,
|
|
88
|
+
awaitPromise: true,
|
|
89
|
+
returnByValue: true
|
|
90
|
+
});
|
|
91
|
+
if (res.result?.value === true) {
|
|
92
|
+
return { success: true, message: "Copied to clipboard" };
|
|
93
|
+
}
|
|
92
94
|
return { success: true, message: "Copied to clipboard" };
|
|
93
95
|
}
|
|
94
|
-
if (!secureCtx) {
|
|
95
|
-
throw new Error("browser_clipboard: write failed on a non-secure context (file:// or http://). The Clipboard API is unavailable here; navigate to an https page and retry.");
|
|
96
|
-
}
|
|
97
|
-
throw new Error("Failed to write to clipboard (page may not be focused)");
|
|
98
96
|
}
|
|
99
97
|
// ── Read action ──────────────────────────────────────────────────
|
|
100
98
|
// 4-layer clipboard read strategy:
|
|
@@ -147,6 +145,15 @@ export async function browserClipboard(session, options, _config) {
|
|
|
147
145
|
});
|
|
148
146
|
return valRes?.value || null;
|
|
149
147
|
}
|
|
148
|
+
// Layer 0: Direct OS system clipboard read via clipboardy
|
|
149
|
+
try {
|
|
150
|
+
const clipboardy = (await import("clipboardy")).default;
|
|
151
|
+
const sysText = await clipboardy.read();
|
|
152
|
+
if (sysText && sysText.trim().length > 0) {
|
|
153
|
+
return { success: true, text: sysText, message: "Read from clipboard" };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch { }
|
|
150
157
|
await grantUserActivation();
|
|
151
158
|
const secureCtx = await isSecureContext();
|
|
152
159
|
const originRes = await session.Runtime.evaluate({ expression: "window.location.origin" }).catch(() => null);
|