cdp-toolkit 2.2.0 → 2.5.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/CHANGELOG.md +33 -0
- package/README.md +43 -17
- package/dist/bidi/mux-daemon.js +2 -0
- package/dist/cli.js +695 -42
- package/dist/index.js +514 -0
- package/dist/mcp.js +724 -30
- package/package.json +5 -3
- package/src/cdp/driver.ts +1 -1
- package/src/driver.ts +3 -0
- package/src/firefox-tools.ts +6 -3
- package/src/index.ts +16 -3
- package/src/manifest.ts +138 -0
- package/src/toolDocs.ts +37 -0
- package/src/toolGroups.ts +3 -2
- package/src/tools/extract.ts +777 -0
- package/src/tools/focus-emulation.ts +185 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* focus-emulation.ts: `focus_emulation` + `click_focus_gated` — the Chrome CDP primitive behind
|
|
3
|
+
* driving a focus-gated page (a button the site disables unless the tab is focused) WITHOUT
|
|
4
|
+
* stealing OS window focus.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS (the mechanism it ports)
|
|
7
|
+
* ========================================
|
|
8
|
+
* Anthropic's Claude Code OAuth page (claude.ai/oauth/*) gates its Authorize button behind
|
|
9
|
+
* document.hasFocus()/visibilitychange: a background tab reports hasFocus()===false and the
|
|
10
|
+
* button stays disabled, so an agent driving claude -p's login has to keep a human clicking.
|
|
11
|
+
* The danbuhler/claude-code-auto-authorize Chrome extension defeats it with two CDP primitives:
|
|
12
|
+
* 1. Emulation.setFocusEmulationEnabled {enabled:true} — the page BELIEVES it is focused
|
|
13
|
+
* (hasFocus()→true, visibilitychange stays "visible", focus events fire) while the real
|
|
14
|
+
* OS window stays put; and
|
|
15
|
+
* 2. Input.dispatchMouseEvent — a TRUSTED click (isTrusted:true) on that background tab,
|
|
16
|
+
* which a content script's synthesized events (isTrusted:false) can never produce.
|
|
17
|
+
* This module is that pair as toolkit tools: focus_emulation is the raw toggle;
|
|
18
|
+
* click_focus_gated composes toggle → locate-button → trusted-click → restore.
|
|
19
|
+
*
|
|
20
|
+
* WHY CHROME-ONLY (capability "emulate.focus", declared in ../driver.ts /
|
|
21
|
+
* ../cdp/driver.ts): WebDriver BiDi has no focus-emulation module (the emulation module covers
|
|
22
|
+
* geolocation/timezone/locale/scripting only), so under --browser firefox BOTH tools are absent
|
|
23
|
+
* from tools/list (ADR-001: absent, never present-and-throwing) — like dispatch_mouse and
|
|
24
|
+
* start_screen_recording.
|
|
25
|
+
*
|
|
26
|
+
* Lease gate: withPage (client.ts) → resolveTarget → assertLeaseOk("chrome", ...), the same
|
|
27
|
+
* choke point as every input tool.
|
|
28
|
+
*/
|
|
29
|
+
import { sendInput } from "../activity.ts";
|
|
30
|
+
import { withPage } from "../client.ts";
|
|
31
|
+
import type { TargetSelector } from "../types.ts";
|
|
32
|
+
|
|
33
|
+
export interface FocusEmulationArgs {
|
|
34
|
+
target?: TargetSelector;
|
|
35
|
+
lease?: string;
|
|
36
|
+
/** true → the page reports focused; false → restore real focus state. */
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FocusEmulationResult {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ClickFocusGatedArgs {
|
|
45
|
+
target?: TargetSelector;
|
|
46
|
+
lease?: string;
|
|
47
|
+
/** CSS selector of the button to click once focus is emulated. Exactly one of selector/text. */
|
|
48
|
+
selector?: string;
|
|
49
|
+
/** Visible-text match for the button (e.g. "Authorize"); exact-trim match, case-sensitive. */
|
|
50
|
+
text?: string;
|
|
51
|
+
/** Max ms to poll for the button to exist AND become enabled. Default 30000. */
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
/** Poll interval ms. Default 500. */
|
|
54
|
+
pollMs?: number;
|
|
55
|
+
/** Leave focus emulation ON after the click (default false — always restored). */
|
|
56
|
+
keepFocus?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ClickFocusGatedResult {
|
|
60
|
+
clicked: boolean;
|
|
61
|
+
x: number;
|
|
62
|
+
y: number;
|
|
63
|
+
/** How the button was located. */
|
|
64
|
+
matchedBy: "selector" | "text";
|
|
65
|
+
/** ms from call to click. */
|
|
66
|
+
elapsedMs: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function validateFocusEmulationArgs(args: FocusEmulationArgs): void {
|
|
70
|
+
if (typeof args.enabled !== "boolean") {
|
|
71
|
+
throw new Error("focus_emulation requires { enabled } of true or false");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function validateClickFocusGatedArgs(args: ClickFocusGatedArgs): void {
|
|
76
|
+
const hasSel = typeof args.selector === "string" && args.selector.length > 0;
|
|
77
|
+
const hasText = typeof args.text === "string" && args.text.length > 0;
|
|
78
|
+
if (hasSel === hasText) {
|
|
79
|
+
throw new Error("click_focus_gated requires exactly one of { selector } or { text }");
|
|
80
|
+
}
|
|
81
|
+
if (args.timeoutMs !== undefined && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
|
|
82
|
+
throw new Error("click_focus_gated: timeoutMs must be a positive number");
|
|
83
|
+
}
|
|
84
|
+
if (args.pollMs !== undefined && (!Number.isFinite(args.pollMs) || args.pollMs <= 0)) {
|
|
85
|
+
throw new Error("click_focus_gated: pollMs must be a positive number");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const FIND_BUTTON = `(() => {
|
|
90
|
+
const SEL = %SEL%;
|
|
91
|
+
const TEXT = %TEXT%;
|
|
92
|
+
let btn = null;
|
|
93
|
+
let matchedBy = null;
|
|
94
|
+
if (SEL) {
|
|
95
|
+
btn = document.querySelector(SEL);
|
|
96
|
+
if (btn) matchedBy = "selector";
|
|
97
|
+
}
|
|
98
|
+
if (!btn && TEXT) {
|
|
99
|
+
btn = Array.from(document.querySelectorAll("button,[role=button],input[type=submit]"))
|
|
100
|
+
.find((b) => (b.textContent || b.value || "").trim() === TEXT);
|
|
101
|
+
if (btn) matchedBy = "text";
|
|
102
|
+
}
|
|
103
|
+
if (!btn) return null;
|
|
104
|
+
const r = btn.getBoundingClientRect();
|
|
105
|
+
if (r.width === 0 || r.height === 0) return null;
|
|
106
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2,
|
|
107
|
+
disabled: !!btn.disabled || btn.getAttribute("aria-disabled") === "true",
|
|
108
|
+
matchedBy, hasFocus: document.hasFocus(), visibility: document.visibilityState };
|
|
109
|
+
})()`;
|
|
110
|
+
|
|
111
|
+
function findButtonExpression(selector: string | undefined, text: string | undefined): string {
|
|
112
|
+
return FIND_BUTTON.replace("%SEL%", JSON.stringify(selector ?? null)).replace(
|
|
113
|
+
"%TEXT%",
|
|
114
|
+
JSON.stringify(text ?? null),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Toggle page focus emulation. The page reports focused/visible without the OS window moving. */
|
|
119
|
+
export async function focusEmulation(args: FocusEmulationArgs): Promise<FocusEmulationResult> {
|
|
120
|
+
validateFocusEmulationArgs(args);
|
|
121
|
+
return withPage(
|
|
122
|
+
args.target,
|
|
123
|
+
async (conn) => {
|
|
124
|
+
await conn.send("Emulation.setFocusEmulationEnabled", { enabled: args.enabled });
|
|
125
|
+
return { enabled: args.enabled };
|
|
126
|
+
},
|
|
127
|
+
{ lease: args.lease },
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Emulate focus, poll for a focus-gated button to become enabled, trusted-click it, restore.
|
|
133
|
+
* The restore runs in `finally` unless keepFocus — a thrown click never strands the page in a
|
|
134
|
+
* fake-focused state.
|
|
135
|
+
*/
|
|
136
|
+
export async function clickFocusGated(args: ClickFocusGatedArgs): Promise<ClickFocusGatedResult> {
|
|
137
|
+
validateClickFocusGatedArgs(args);
|
|
138
|
+
const timeoutMs = args.timeoutMs ?? 30_000;
|
|
139
|
+
const pollMs = args.pollMs ?? 500;
|
|
140
|
+
const start = Date.now();
|
|
141
|
+
return withPage(
|
|
142
|
+
args.target,
|
|
143
|
+
async (conn, target) => {
|
|
144
|
+
await conn.send("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
145
|
+
try {
|
|
146
|
+
const expression = findButtonExpression(args.selector, args.text);
|
|
147
|
+
for (;;) {
|
|
148
|
+
const { result } = await conn.send<{ result: { value?: unknown } }>("Runtime.evaluate", {
|
|
149
|
+
expression,
|
|
150
|
+
returnByValue: true,
|
|
151
|
+
});
|
|
152
|
+
const btn = (result as { value?: unknown } | undefined)?.value as
|
|
153
|
+
| { x: number; y: number; disabled: boolean; matchedBy: "selector" | "text" }
|
|
154
|
+
| null
|
|
155
|
+
| undefined;
|
|
156
|
+
if (btn && !btn.disabled) {
|
|
157
|
+
const base = { x: btn.x, y: btn.y, button: "left", buttons: 1, clickCount: 1 };
|
|
158
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: btn.x, y: btn.y });
|
|
159
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", { ...base, type: "mousePressed" });
|
|
160
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", { ...base, type: "mouseReleased" });
|
|
161
|
+
return {
|
|
162
|
+
clicked: true,
|
|
163
|
+
x: btn.x,
|
|
164
|
+
y: btn.y,
|
|
165
|
+
matchedBy: btn.matchedBy,
|
|
166
|
+
elapsedMs: Date.now() - start,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (Date.now() - start >= timeoutMs) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`click_focus_gated: button never became clickable within ${timeoutMs}ms` +
|
|
172
|
+
(btn ? " (found but stayed disabled — focus emulation may not satisfy this gate)" : " (never located)"),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
if (!args.keepFocus) {
|
|
179
|
+
await conn.send("Emulation.setFocusEmulationEnabled", { enabled: false }).catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{ lease: args.lease, timeoutMs: timeoutMs + 5_000 },
|
|
184
|
+
);
|
|
185
|
+
}
|
package/src/version.ts
CHANGED