pi-chrome 0.9.1 → 0.10.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/README.md
CHANGED
|
@@ -65,6 +65,25 @@ pi-chrome v<version>
|
|
|
65
65
|
✓ Companion Chrome extension responding (ID: <chrome-extension-id>, ext v<version>)
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
## Trusted-input mode (CDP)
|
|
69
|
+
|
|
70
|
+
By default, `chrome_*` clicks and keystrokes are **synthetic** DOM events (`event.isTrusted === false`). They drive React/Vue/Angular state correctly but **do not** satisfy Chrome's user-activation gates: clipboard write, fullscreen, file picker, and autoplay all need a real user gesture.
|
|
71
|
+
|
|
72
|
+
pi-chrome can optionally route input through `chrome.debugger` (CDP `Input.dispatchMouseEvent` / `Input.dispatchKeyEvent`) so each event arrives as `isTrusted=true`, satisfies user-activation, and bypasses site bot-detection that filters synthetic events. The tradeoff: Chrome pins a yellow *"Pi Existing Chrome Profile Bridge started debugging this browser"* banner to the top of any debugged tab.
|
|
73
|
+
|
|
74
|
+
Usage:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
/chrome-trusted on # all chrome_* tools dispatch via CDP
|
|
78
|
+
/chrome-trusted off # synthetic events only (default)
|
|
79
|
+
/chrome-trusted auto # CDP only when a tool passes trusted=true
|
|
80
|
+
/chrome-trusted status # show current mode and attached tabs
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For a single call, pass `trusted: true` on `chrome_click`, `chrome_type`, `chrome_fill`, `chrome_key`, `chrome_hover`, `chrome_drag`, or `chrome_scroll`. The per-call value always wins over the session toggle.
|
|
84
|
+
|
|
85
|
+
First time you upgrade to a pi-chrome that uses trusted input, Chrome will prompt to re-approve the extension's new `debugger` permission — do that once in `chrome://extensions`.
|
|
86
|
+
|
|
68
87
|
## Background mode
|
|
69
88
|
|
|
70
89
|
By default, `chrome_*` tools focus Chrome and activate the target tab so you can watch the agent work — great for demos, pair-driving, debugging, and first-time confidence that things are happening.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Pi Existing Chrome Profile Bridge",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"description": "Lets Pi control tabs in this existing Chrome profile via a local bridge at 127.0.0.1.",
|
|
6
|
-
"permissions": ["tabs", "scripting", "storage", "activeTab", "alarms", "webNavigation"],
|
|
6
|
+
"permissions": ["tabs", "scripting", "storage", "activeTab", "alarms", "webNavigation", "debugger"],
|
|
7
7
|
"host_permissions": ["<all_urls>", "http://127.0.0.1:17318/*"],
|
|
8
8
|
"background": {
|
|
9
9
|
"service_worker": "service_worker.js"
|
|
@@ -3,6 +3,350 @@ const CLIENT_NAME = `Pi Chrome Bridge ${chrome.runtime.id}`;
|
|
|
3
3
|
const POLL_ERROR_BACKOFF_MS = 2000;
|
|
4
4
|
let polling = false;
|
|
5
5
|
|
|
6
|
+
// =================== Trusted-input (CDP) layer ===================
|
|
7
|
+
// Tracks which tabs we have attached chrome.debugger to, plus session-level mode.
|
|
8
|
+
const attachedTabs = new Map(); // tabId -> { detachAt: number, pointer: {x,y} }
|
|
9
|
+
let TRUSTED_MODE = "off"; // "off" | "on" | "auto"
|
|
10
|
+
const TRUSTED_IDLE_DETACH_MS = 15_000;
|
|
11
|
+
const CDP_VERSION = "1.3";
|
|
12
|
+
|
|
13
|
+
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
14
|
+
function rng(min, max) { return min + Math.random() * (max - min); }
|
|
15
|
+
|
|
16
|
+
async function wantsTrusted(params) {
|
|
17
|
+
if (params && params.trusted === false) return false;
|
|
18
|
+
if (params && params.trusted === true) return true;
|
|
19
|
+
return TRUSTED_MODE === "on";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setTrustedMode(mode) {
|
|
23
|
+
const next = String(mode || "").toLowerCase();
|
|
24
|
+
if (!["off", "on", "auto"].includes(next)) throw new Error(`bad trusted mode: ${next}`);
|
|
25
|
+
TRUSTED_MODE = next;
|
|
26
|
+
if (next === "off") void detachAll();
|
|
27
|
+
return { mode: TRUSTED_MODE };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function trustedStatus() {
|
|
31
|
+
return {
|
|
32
|
+
mode: TRUSTED_MODE,
|
|
33
|
+
attachedTabs: Array.from(attachedTabs.keys()),
|
|
34
|
+
permissionGranted: typeof chrome !== "undefined" && !!chrome.debugger,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function attachDebugger(tabId) {
|
|
39
|
+
if (!chrome.debugger) throw new Error("chrome.debugger API unavailable; reload the extension to grant the new permission");
|
|
40
|
+
if (attachedTabs.has(tabId)) {
|
|
41
|
+
const entry = attachedTabs.get(tabId);
|
|
42
|
+
entry.detachAt = Date.now() + TRUSTED_IDLE_DETACH_MS;
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
await chrome.debugger.attach({ tabId }, CDP_VERSION);
|
|
46
|
+
// Seed pointer in a plausible "just left the address bar" location.
|
|
47
|
+
const entry = { detachAt: Date.now() + TRUSTED_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 } };
|
|
48
|
+
attachedTabs.set(tabId, entry);
|
|
49
|
+
return entry;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function detachDebugger(tabId) {
|
|
53
|
+
if (!attachedTabs.has(tabId)) return;
|
|
54
|
+
attachedTabs.delete(tabId);
|
|
55
|
+
try { await chrome.debugger.detach({ tabId }); } catch {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function detachAll() {
|
|
59
|
+
const ids = Array.from(attachedTabs.keys());
|
|
60
|
+
await Promise.all(ids.map(detachDebugger));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (chrome.debugger && chrome.debugger.onDetach) {
|
|
64
|
+
chrome.debugger.onDetach.addListener(({ tabId }, reason) => {
|
|
65
|
+
if (tabId !== undefined) attachedTabs.delete(tabId);
|
|
66
|
+
if (reason === "canceled_by_user") {
|
|
67
|
+
console.warn(`[pi-chrome] debugger canceled by user on tab ${tabId}; trusted mode will reattach on next call`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setInterval(() => {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
for (const [tabId, entry] of attachedTabs) {
|
|
75
|
+
if (entry.detachAt && entry.detachAt < now && TRUSTED_MODE !== "on") {
|
|
76
|
+
void detachDebugger(tabId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, 5000);
|
|
80
|
+
|
|
81
|
+
function cdp(tabId, method, params) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
chrome.debugger.sendCommand({ tabId }, method, params || {}, (result) => {
|
|
84
|
+
if (chrome.runtime.lastError) reject(new Error(`${method}: ${chrome.runtime.lastError.message}`));
|
|
85
|
+
else resolve(result);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Resolve target -> {x, y, rect} in viewport coords by running tiny script in tab.
|
|
91
|
+
async function resolveTargetInTab(tabId, params) {
|
|
92
|
+
const results = await chrome.scripting.executeScript({
|
|
93
|
+
target: { tabId, frameIds: [0] },
|
|
94
|
+
world: "MAIN",
|
|
95
|
+
func: (selector, uid, x, y) => {
|
|
96
|
+
const state = window.__PI_CHROME_STATE__;
|
|
97
|
+
let el = null;
|
|
98
|
+
if (uid && state && state.elements && state.elements[uid]) el = state.elements[uid];
|
|
99
|
+
else if (selector) el = document.querySelector(selector);
|
|
100
|
+
if (el) {
|
|
101
|
+
el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
|
|
102
|
+
const r = el.getBoundingClientRect();
|
|
103
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2, rect: { left: r.left, top: r.top, width: r.width, height: r.height }, tag: el.tagName, found: true };
|
|
104
|
+
}
|
|
105
|
+
if (typeof x === "number" && typeof y === "number") return { x, y, rect: null, tag: null, found: true };
|
|
106
|
+
return { found: false };
|
|
107
|
+
},
|
|
108
|
+
args: [params.selector ?? null, params.uid ?? null, params.x ?? null, params.y ?? null],
|
|
109
|
+
});
|
|
110
|
+
const v = results?.[0]?.result;
|
|
111
|
+
if (!v || !v.found) throw new Error("Could not resolve target element for trusted action");
|
|
112
|
+
return v;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function pickInsideRect(rect) {
|
|
116
|
+
if (!rect) return null;
|
|
117
|
+
const insetX = Math.min(rect.width * 0.35, Math.max(2, rect.width / 2 - 1));
|
|
118
|
+
const insetY = Math.min(rect.height * 0.35, Math.max(2, rect.height / 2 - 1));
|
|
119
|
+
return {
|
|
120
|
+
x: rect.left + rect.width / 2 + rng(-insetX, insetX),
|
|
121
|
+
y: rect.top + rect.height / 2 + rng(-insetY, insetY),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function cdpMoveTo(tabId, x, y) {
|
|
126
|
+
const entry = attachedTabs.get(tabId);
|
|
127
|
+
const startX = entry?.pointer?.x ?? Math.max(20, Math.min(400, x - 200));
|
|
128
|
+
const startY = entry?.pointer?.y ?? Math.max(20, Math.min(400, y - 200));
|
|
129
|
+
const n = Math.max(10, Math.min(36, Math.round(Math.hypot(x - startX, y - startY) / 20)));
|
|
130
|
+
for (let i = 1; i <= n; i++) {
|
|
131
|
+
const t = i / n;
|
|
132
|
+
const ease = t * t * (3 - 2 * t);
|
|
133
|
+
const wobble = Math.sin(t * Math.PI) * 8;
|
|
134
|
+
const px = startX + (x - startX) * ease + rng(-wobble, wobble);
|
|
135
|
+
const py = startY + (y - startY) * ease + rng(-wobble, wobble);
|
|
136
|
+
await cdp(tabId, "Input.dispatchMouseEvent", {
|
|
137
|
+
type: "mouseMoved", x: px, y: py, button: "none", buttons: 0, pointerType: "mouse",
|
|
138
|
+
});
|
|
139
|
+
await sleep(rng(5, 16));
|
|
140
|
+
}
|
|
141
|
+
if (entry) entry.pointer = { x, y };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function cdpModifiersFor(mods) {
|
|
145
|
+
let m = 0;
|
|
146
|
+
if (mods?.altKey) m |= 1;
|
|
147
|
+
if (mods?.ctrlKey) m |= 2;
|
|
148
|
+
if (mods?.metaKey) m |= 4;
|
|
149
|
+
if (mods?.shiftKey) m |= 8;
|
|
150
|
+
return m;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cdpKeyInfo(key, shifted) {
|
|
154
|
+
// Map common keys to CDP key event init fields. Returns { code, key, windowsVirtualKeyCode, text }.
|
|
155
|
+
const SPECIAL = {
|
|
156
|
+
Enter: { code: "Enter", windowsVirtualKeyCode: 13, text: "\r" },
|
|
157
|
+
Tab: { code: "Tab", windowsVirtualKeyCode: 9, text: "\t" },
|
|
158
|
+
Backspace: { code: "Backspace", windowsVirtualKeyCode: 8, text: "" },
|
|
159
|
+
Delete: { code: "Delete", windowsVirtualKeyCode: 46, text: "" },
|
|
160
|
+
Escape: { code: "Escape", windowsVirtualKeyCode: 27, text: "" },
|
|
161
|
+
ArrowLeft: { code: "ArrowLeft", windowsVirtualKeyCode: 37, text: "" },
|
|
162
|
+
ArrowUp: { code: "ArrowUp", windowsVirtualKeyCode: 38, text: "" },
|
|
163
|
+
ArrowRight: { code: "ArrowRight", windowsVirtualKeyCode: 39, text: "" },
|
|
164
|
+
ArrowDown: { code: "ArrowDown", windowsVirtualKeyCode: 40, text: "" },
|
|
165
|
+
Shift: { code: "ShiftLeft", windowsVirtualKeyCode: 16, text: "" },
|
|
166
|
+
Control: { code: "ControlLeft", windowsVirtualKeyCode: 17, text: "" },
|
|
167
|
+
Alt: { code: "AltLeft", windowsVirtualKeyCode: 18, text: "" },
|
|
168
|
+
Meta: { code: "MetaLeft", windowsVirtualKeyCode: 91, text: "" },
|
|
169
|
+
" ": { code: "Space", windowsVirtualKeyCode: 32, text: " " },
|
|
170
|
+
};
|
|
171
|
+
if (SPECIAL[key]) return { key, ...SPECIAL[key] };
|
|
172
|
+
if (key.length === 1) {
|
|
173
|
+
const ch = key;
|
|
174
|
+
let code, vk;
|
|
175
|
+
if (/^[a-zA-Z]$/.test(ch)) { code = `Key${ch.toUpperCase()}`; vk = ch.toUpperCase().charCodeAt(0); }
|
|
176
|
+
else if (/^[0-9]$/.test(ch)) { code = `Digit${ch}`; vk = ch.charCodeAt(0); }
|
|
177
|
+
else { code = ch; vk = ch.charCodeAt(0); }
|
|
178
|
+
return { key: ch, code, windowsVirtualKeyCode: vk, text: ch };
|
|
179
|
+
}
|
|
180
|
+
return { key, code: key, windowsVirtualKeyCode: 0, text: "" };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function cdpTypeChar(tabId, ch) {
|
|
184
|
+
const needShift = /^[A-Z]$/.test(ch) || "~!@#$%^&*()_+{}|:\"<>?".includes(ch);
|
|
185
|
+
let modifiers = 0;
|
|
186
|
+
if (needShift) {
|
|
187
|
+
await cdp(tabId, "Input.dispatchKeyEvent", { type: "keyDown", key: "Shift", code: "ShiftLeft", windowsVirtualKeyCode: 16, modifiers: 8 });
|
|
188
|
+
modifiers = 8;
|
|
189
|
+
await sleep(rng(8, 22));
|
|
190
|
+
}
|
|
191
|
+
const info = cdpKeyInfo(ch);
|
|
192
|
+
await cdp(tabId, "Input.dispatchKeyEvent", {
|
|
193
|
+
type: "keyDown", key: info.key, code: info.code,
|
|
194
|
+
windowsVirtualKeyCode: info.windowsVirtualKeyCode, nativeVirtualKeyCode: info.windowsVirtualKeyCode,
|
|
195
|
+
text: info.text, unmodifiedText: info.text, modifiers,
|
|
196
|
+
});
|
|
197
|
+
await sleep(rng(25, 90));
|
|
198
|
+
await cdp(tabId, "Input.dispatchKeyEvent", {
|
|
199
|
+
type: "keyUp", key: info.key, code: info.code,
|
|
200
|
+
windowsVirtualKeyCode: info.windowsVirtualKeyCode, modifiers,
|
|
201
|
+
});
|
|
202
|
+
if (needShift) {
|
|
203
|
+
await sleep(rng(5, 18));
|
|
204
|
+
await cdp(tabId, "Input.dispatchKeyEvent", { type: "keyUp", key: "Shift", code: "ShiftLeft", windowsVirtualKeyCode: 16, modifiers: 0 });
|
|
205
|
+
}
|
|
206
|
+
await sleep(rng(35, 130));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function trustedClick(params) {
|
|
210
|
+
const tab = await getTabByParams(params);
|
|
211
|
+
if (params.foreground) await bringToFront(tab);
|
|
212
|
+
await attachDebugger(tab.id);
|
|
213
|
+
const resolved = await resolveTargetInTab(tab.id, params);
|
|
214
|
+
const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
|
|
215
|
+
await cdpMoveTo(tab.id, point.x, point.y);
|
|
216
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse" });
|
|
217
|
+
await sleep(rng(45, 140));
|
|
218
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" });
|
|
219
|
+
return { trusted: true, x: point.x, y: point.y, tag: resolved.tag };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function trustedHover(params) {
|
|
223
|
+
const tab = await getTabByParams(params);
|
|
224
|
+
if (params.foreground) await bringToFront(tab);
|
|
225
|
+
await attachDebugger(tab.id);
|
|
226
|
+
const resolved = await resolveTargetInTab(tab.id, params);
|
|
227
|
+
const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
|
|
228
|
+
await cdpMoveTo(tab.id, point.x, point.y);
|
|
229
|
+
await sleep(rng(80, 220));
|
|
230
|
+
return { trusted: true, x: point.x, y: point.y, tag: resolved.tag };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function trustedKey(params) {
|
|
234
|
+
const tab = await getTabByParams(params);
|
|
235
|
+
if (params.foreground) await bringToFront(tab);
|
|
236
|
+
await attachDebugger(tab.id);
|
|
237
|
+
const key = String(params.key || "");
|
|
238
|
+
if (!key) throw new Error("trusted.key: missing key");
|
|
239
|
+
const info = cdpKeyInfo(key);
|
|
240
|
+
await cdp(tab.id, "Input.dispatchKeyEvent", {
|
|
241
|
+
type: "keyDown", key: info.key, code: info.code,
|
|
242
|
+
windowsVirtualKeyCode: info.windowsVirtualKeyCode, nativeVirtualKeyCode: info.windowsVirtualKeyCode,
|
|
243
|
+
text: info.text, unmodifiedText: info.text,
|
|
244
|
+
});
|
|
245
|
+
await sleep(rng(25, 90));
|
|
246
|
+
await cdp(tab.id, "Input.dispatchKeyEvent", {
|
|
247
|
+
type: "keyUp", key: info.key, code: info.code,
|
|
248
|
+
windowsVirtualKeyCode: info.windowsVirtualKeyCode,
|
|
249
|
+
});
|
|
250
|
+
return { trusted: true, key: info.key };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function trustedType(params) {
|
|
254
|
+
const tab = await getTabByParams(params);
|
|
255
|
+
if (params.foreground) await bringToFront(tab);
|
|
256
|
+
await attachDebugger(tab.id);
|
|
257
|
+
if (params.selector || params.uid) {
|
|
258
|
+
// Focus target by clicking it first.
|
|
259
|
+
const resolved = await resolveTargetInTab(tab.id, params);
|
|
260
|
+
const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
|
|
261
|
+
await cdpMoveTo(tab.id, point.x, point.y);
|
|
262
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse" });
|
|
263
|
+
await sleep(rng(45, 110));
|
|
264
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" });
|
|
265
|
+
await sleep(rng(50, 120));
|
|
266
|
+
}
|
|
267
|
+
const text = String(params.text || "");
|
|
268
|
+
for (const ch of Array.from(text)) await cdpTypeChar(tab.id, ch);
|
|
269
|
+
if (params.pressEnter) {
|
|
270
|
+
await cdpTypeChar(tab.id, "\r").catch(() => undefined);
|
|
271
|
+
await trustedKey({ ...params, key: "Enter" });
|
|
272
|
+
}
|
|
273
|
+
return { trusted: true, length: text.length };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function trustedFill(params) {
|
|
277
|
+
const tab = await getTabByParams(params);
|
|
278
|
+
if (params.foreground) await bringToFront(tab);
|
|
279
|
+
await attachDebugger(tab.id);
|
|
280
|
+
if (!(params.selector || params.uid)) throw new Error("trusted.fill: selector or uid required");
|
|
281
|
+
const resolved = await resolveTargetInTab(tab.id, params);
|
|
282
|
+
const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
|
|
283
|
+
await cdpMoveTo(tab.id, point.x, point.y);
|
|
284
|
+
// Triple-click selects all in input fields.
|
|
285
|
+
for (let i = 1; i <= 3; i++) {
|
|
286
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: i, pointerType: "mouse" });
|
|
287
|
+
await sleep(rng(20, 60));
|
|
288
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: i, pointerType: "mouse" });
|
|
289
|
+
await sleep(rng(20, 60));
|
|
290
|
+
}
|
|
291
|
+
// Delete selection.
|
|
292
|
+
await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyDown", key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 });
|
|
293
|
+
await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyUp", key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 });
|
|
294
|
+
await sleep(rng(20, 60));
|
|
295
|
+
const text = String(params.text || "");
|
|
296
|
+
for (const ch of Array.from(text)) await cdpTypeChar(tab.id, ch);
|
|
297
|
+
if (params.submit) await trustedKey({ ...params, key: "Enter" });
|
|
298
|
+
return { trusted: true, length: text.length };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function trustedScroll(params) {
|
|
302
|
+
const tab = await getTabByParams(params);
|
|
303
|
+
if (params.foreground) await bringToFront(tab);
|
|
304
|
+
await attachDebugger(tab.id);
|
|
305
|
+
const resolved = (params.selector || params.uid) ? await resolveTargetInTab(tab.id, params) : { x: 100, y: 100, rect: null };
|
|
306
|
+
const x = resolved.rect ? resolved.rect.left + Math.min(resolved.rect.width, 800) / 2 : resolved.x;
|
|
307
|
+
const y = resolved.rect ? resolved.rect.top + Math.min(resolved.rect.height, 600) / 2 : resolved.y;
|
|
308
|
+
const totalY = params.deltaY || 0, totalX = params.deltaX || 0;
|
|
309
|
+
const n = Math.max(3, Math.min(20, params.steps || Math.ceil(Math.abs(totalY) / 120)));
|
|
310
|
+
// momentum-shaped front-loaded weights
|
|
311
|
+
const w = []; for (let i = 1; i <= n; i++) w.push(1 / i);
|
|
312
|
+
const sumW = w.reduce((a, b) => a + b, 0);
|
|
313
|
+
for (let i = 0; i < n; i++) {
|
|
314
|
+
const dy = totalY * (w[i] / sumW), dx = totalX * (w[i] / sumW);
|
|
315
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", {
|
|
316
|
+
type: "mouseWheel", x, y, deltaX: dx, deltaY: dy, pointerType: "mouse",
|
|
317
|
+
});
|
|
318
|
+
await sleep(rng(14, 32));
|
|
319
|
+
}
|
|
320
|
+
return { trusted: true, deltaX: totalX, deltaY: totalY, steps: n };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function trustedDrag(params) {
|
|
324
|
+
const tab = await getTabByParams(params);
|
|
325
|
+
if (params.foreground) await bringToFront(tab);
|
|
326
|
+
await attachDebugger(tab.id);
|
|
327
|
+
const from = await resolveTargetInTab(tab.id, { selector: params.fromSelector ?? null, uid: params.fromUid ?? null, x: params.fromX ?? null, y: params.fromY ?? null });
|
|
328
|
+
const to = await resolveTargetInTab(tab.id, { selector: params.toSelector ?? null, uid: params.toUid ?? null, x: params.toX ?? null, y: params.toY ?? null });
|
|
329
|
+
const fp = from.rect ? pickInsideRect(from.rect) : { x: from.x, y: from.y };
|
|
330
|
+
const tp = to.rect ? pickInsideRect(to.rect) : { x: to.x, y: to.y };
|
|
331
|
+
await cdpMoveTo(tab.id, fp.x, fp.y);
|
|
332
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: fp.x, y: fp.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse" });
|
|
333
|
+
await sleep(rng(60, 140));
|
|
334
|
+
const steps = params.steps || 20;
|
|
335
|
+
for (let i = 1; i <= steps; i++) {
|
|
336
|
+
const t = i / steps;
|
|
337
|
+
const ease = t * t * (3 - 2 * t);
|
|
338
|
+
const wobble = Math.sin(t * Math.PI) * 6;
|
|
339
|
+
const x = fp.x + (tp.x - fp.x) * ease + rng(-wobble, wobble);
|
|
340
|
+
const y = fp.y + (tp.y - fp.y) * ease + rng(-wobble, wobble);
|
|
341
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "left", buttons: 1, pointerType: "mouse" });
|
|
342
|
+
await sleep(rng(10, 26));
|
|
343
|
+
}
|
|
344
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: tp.x, y: tp.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" });
|
|
345
|
+
return { trusted: true, from: fp, to: tp, steps };
|
|
346
|
+
}
|
|
347
|
+
// ===============================================================
|
|
348
|
+
|
|
349
|
+
|
|
6
350
|
function armKeepaliveAlarm() {
|
|
7
351
|
chrome.alarms.create("pi-bridge-keepalive", { periodInMinutes: 0.5 });
|
|
8
352
|
}
|
|
@@ -77,10 +421,6 @@ async function postResult(result) {
|
|
|
77
421
|
});
|
|
78
422
|
}
|
|
79
423
|
|
|
80
|
-
function sleep(ms) {
|
|
81
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
424
|
function isVersionOlder(a, b) {
|
|
85
425
|
const pa = String(a).split(".").map((n) => parseInt(n, 10) || 0);
|
|
86
426
|
const pb = String(b).split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -128,21 +468,32 @@ async function dispatch(action, params) {
|
|
|
128
468
|
case "page.evaluate":
|
|
129
469
|
return evaluateInTab(params);
|
|
130
470
|
case "page.click":
|
|
471
|
+
if (await wantsTrusted(params)) return trustedClick(params);
|
|
131
472
|
return executeActionInTab(params, clickPage, [params.selector ?? null, params.uid ?? null, params.x ?? null, params.y ?? null]);
|
|
132
473
|
case "page.hover":
|
|
474
|
+
if (await wantsTrusted(params)) return trustedHover(params);
|
|
133
475
|
return executeActionInTab(params, hoverPage, [params.selector ?? null, params.uid ?? null, params.x ?? null, params.y ?? null]);
|
|
134
476
|
case "page.drag":
|
|
477
|
+
if (await wantsTrusted(params)) return trustedDrag(params);
|
|
135
478
|
return executeActionInTab(params, dragPage, [params.fromUid ?? null, params.fromSelector ?? null, params.fromX ?? null, params.fromY ?? null, params.toUid ?? null, params.toSelector ?? null, params.toX ?? null, params.toY ?? null, params.steps ?? 12]);
|
|
136
479
|
case "page.upload":
|
|
137
480
|
return executeActionInTab(params, uploadFiles, [params.selector ?? null, params.uid ?? null, params.files || []]);
|
|
138
481
|
case "page.type":
|
|
482
|
+
if (await wantsTrusted(params)) return trustedType(params);
|
|
139
483
|
return executeActionInTab(params, typeIntoPage, [params.selector ?? null, params.uid ?? null, params.text || "", Boolean(params.pressEnter)]);
|
|
140
484
|
case "page.fill":
|
|
485
|
+
if (await wantsTrusted(params)) return trustedFill(params);
|
|
141
486
|
return executeActionInTab(params, fillPage, [params.selector ?? null, params.uid ?? null, params.text || "", params.submit === true]);
|
|
142
487
|
case "page.key":
|
|
488
|
+
if (await wantsTrusted(params)) return trustedKey(params);
|
|
143
489
|
return executeActionInTab(params, pressKeyInPage, [params.key]);
|
|
144
490
|
case "page.scroll":
|
|
491
|
+
if (await wantsTrusted(params)) return trustedScroll(params);
|
|
145
492
|
return executeActionInTab(params, scrollPage, [params.selector ?? null, params.uid ?? null, params.deltaY ?? 0, params.deltaX ?? 0, params.steps ?? null]);
|
|
493
|
+
case "trusted.mode":
|
|
494
|
+
return setTrustedMode(params.mode);
|
|
495
|
+
case "trusted.status":
|
|
496
|
+
return trustedStatus();
|
|
146
497
|
case "page.console.list":
|
|
147
498
|
return executeInTab(params, listConsoleMessages, [params.clear === true]);
|
|
148
499
|
case "page.network.list":
|
|
@@ -509,26 +509,77 @@ Usage rules:
|
|
|
509
509
|
lines.push(`… Skipped MAIN-world capability checks because the loaded extension is stale.`);
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
512
|
+
// Trusted-input (chrome.debugger) probe.
|
|
513
|
+
if (extensionAlive && !versionMismatch) {
|
|
514
|
+
try {
|
|
515
|
+
const status = (await bridge.send("trusted.status", {}, 5_000)) as {
|
|
516
|
+
mode?: string;
|
|
517
|
+
attachedTabs?: number[];
|
|
518
|
+
permissionGranted?: boolean;
|
|
519
|
+
};
|
|
520
|
+
if (status.permissionGranted) {
|
|
521
|
+
lines.push(`✓ Trusted-input mode available via chrome.debugger (current: ${status.mode ?? "off"}${status.attachedTabs && status.attachedTabs.length ? `; attached to tab ${status.attachedTabs.join(",")}` : ""}). Pass trusted=true on chrome_click/type/etc, or run /chrome-trusted on, to satisfy isTrusted + user-activation gates.`);
|
|
522
|
+
} else {
|
|
523
|
+
lines.push(`⚠ chrome.debugger API unavailable. The extension is missing the "debugger" permission — reload the extension in chrome://extensions and accept the new permission prompt.`);
|
|
524
|
+
}
|
|
525
|
+
} catch (error) {
|
|
526
|
+
lines.push(`⚠ trusted.status probe failed: ${(error as Error).message}`);
|
|
523
527
|
}
|
|
524
|
-
} catch {
|
|
525
|
-
lines.push(`• CDP probe inconclusive.`);
|
|
526
528
|
}
|
|
527
529
|
|
|
528
530
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
529
531
|
},
|
|
530
532
|
});
|
|
531
533
|
|
|
534
|
+
pi.registerCommand("chrome-trusted", {
|
|
535
|
+
description:
|
|
536
|
+
"Toggle trusted-input mode for chrome_* tools. ON: all clicks/types/etc go through chrome.debugger (CDP) so events are browser-trusted (isTrusted=true) and satisfy user-activation gates (clipboard, fullscreen, autoplay, file picker). Tradeoff: Chrome pins a yellow 'started debugging this browser' banner to the top of any tab in use. OFF (default): synthetic DOM events. AUTO: attach on demand only when a per-call trusted=true is passed. STATUS: print current mode and attached tabs.",
|
|
537
|
+
getArgumentCompletions: (prefix) => {
|
|
538
|
+
const items = [
|
|
539
|
+
{ value: "on", label: "on", description: "All chrome_* tools dispatch via CDP. Yellow debugger banner appears." },
|
|
540
|
+
{ value: "off", label: "off", description: "Synthetic events only (default)." },
|
|
541
|
+
{ value: "auto", label: "auto", description: "Use CDP only when a tool passes trusted=true." },
|
|
542
|
+
{ value: "status", label: "status", description: "Show current trusted mode and any attached tabs." },
|
|
543
|
+
];
|
|
544
|
+
const lowered = prefix.toLowerCase();
|
|
545
|
+
const matches = items.filter((item) => item.value.startsWith(lowered));
|
|
546
|
+
return matches.length > 0 ? matches : null;
|
|
547
|
+
},
|
|
548
|
+
handler: async (args, ctx) => {
|
|
549
|
+
const arg = (args || "").trim().toLowerCase();
|
|
550
|
+
if (arg === "status" || arg === "") {
|
|
551
|
+
try {
|
|
552
|
+
const status = (await bridge.send("trusted.status", {}, 5_000)) as { mode: string; attachedTabs: number[]; permissionGranted: boolean };
|
|
553
|
+
const attached = status.attachedTabs?.length ? ` (attached to tab ${status.attachedTabs.join(",")})` : "";
|
|
554
|
+
const perm = status.permissionGranted ? "" : " — chrome.debugger API unavailable; reload the extension and accept the new permission.";
|
|
555
|
+
ctx.ui.notify(`Trusted-input mode: ${status.mode}${attached}${perm}`, "info");
|
|
556
|
+
} catch (error) {
|
|
557
|
+
ctx.ui.notify(`Failed to read trusted mode: ${(error as Error).message}`, "warning");
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (!["on", "off", "auto"].includes(arg)) {
|
|
562
|
+
ctx.ui.notify(`Unknown argument '${arg}'. Use: on | off | auto | status`, "warning");
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
const result = (await bridge.send("trusted.mode", { mode: arg }, 5_000)) as { mode: string };
|
|
567
|
+
if (result.mode === "on") {
|
|
568
|
+
ctx.ui.notify(
|
|
569
|
+
"Trusted-input mode ON. All chrome_* tools now dispatch through chrome.debugger (CDP). Chrome will show a yellow 'started debugging this browser' banner. Events arrive as isTrusted=true and satisfy user-activation gates.",
|
|
570
|
+
"info",
|
|
571
|
+
);
|
|
572
|
+
} else if (result.mode === "off") {
|
|
573
|
+
ctx.ui.notify("Trusted-input mode OFF. Synthetic events only. Any attached debugger sessions detached.", "info");
|
|
574
|
+
} else {
|
|
575
|
+
ctx.ui.notify("Trusted-input mode AUTO. CDP attaches only when a tool passes trusted=true.", "info");
|
|
576
|
+
}
|
|
577
|
+
} catch (error) {
|
|
578
|
+
ctx.ui.notify(`Failed to set trusted mode: ${(error as Error).message}`, "warning");
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
|
|
532
583
|
pi.registerCommand("chrome-background", {
|
|
533
584
|
description:
|
|
534
585
|
"Toggle silent/background mode for chrome_* tools. Background ON: chrome_* tools act silently — your editor/terminal keeps focus, Chrome does not pop up, your workflow is not interrupted. Background OFF (default): Chrome focuses and activates the target tab so you can watch the agent work, useful for demos, pair-driving, and debugging — tradeoff: Chrome pops up and steals focus. Pass `on` / `off` to set explicitly, or no argument to toggle.",
|
|
@@ -728,7 +779,7 @@ Usage rules:
|
|
|
728
779
|
name: "chrome_click",
|
|
729
780
|
label: "Chrome Click",
|
|
730
781
|
description:
|
|
731
|
-
"Click a snapshot uid, CSS selector, or viewport coordinate in an existing Chrome tab through the companion extension.
|
|
782
|
+
"Click a snapshot uid, CSS selector, or viewport coordinate in an existing Chrome tab through the companion extension. Defaults to synthetic DOM events (isTrusted=false). Pass trusted=true (or run /chrome-trusted on) to route through chrome.debugger so events arrive as browser-trusted and satisfy user-activation gates — Chrome shows a yellow 'started debugging' banner while attached. Pass includeSnapshot=true to return a fresh snapshot after the click.",
|
|
732
783
|
promptSnippet: "Click page elements in Chrome by snapshot uid, selector, or viewport coordinate.",
|
|
733
784
|
parameters: Type.Object({
|
|
734
785
|
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot. Prefer uid over selector after taking a snapshot." })),
|
|
@@ -743,6 +794,7 @@ Usage rules:
|
|
|
743
794
|
background: Type.Optional(
|
|
744
795
|
Type.Boolean({ description: "If true, click silently without focusing Chrome. Default false." }),
|
|
745
796
|
),
|
|
797
|
+
trusted: Type.Optional(Type.Boolean({ description: "If true, dispatch through chrome.debugger / CDP so the event is browser-trusted (isTrusted=true, user-activation satisfied). Triggers Chrome's 'started debugging this browser' banner." })),
|
|
746
798
|
host: Type.Optional(Type.String()),
|
|
747
799
|
port: Type.Optional(Type.Number()),
|
|
748
800
|
}),
|
|
@@ -760,7 +812,7 @@ Usage rules:
|
|
|
760
812
|
name: "chrome_type",
|
|
761
813
|
label: "Chrome Type",
|
|
762
814
|
description:
|
|
763
|
-
"Focus an optional snapshot uid or CSS selector, then type text into an existing Chrome tab
|
|
815
|
+
"Focus an optional snapshot uid or CSS selector, then type text into an existing Chrome tab. Defaults to synthetic per-character keydown/beforeinput/input/keyup sequence. Pass trusted=true (or run /chrome-trusted on) to route through chrome.debugger so each keystroke is browser-trusted (isTrusted=true). Pass includeSnapshot=true to return a fresh snapshot after typing.",
|
|
764
816
|
promptSnippet: "Type text into Chrome, optionally focusing a snapshot uid or selector first.",
|
|
765
817
|
parameters: Type.Object({
|
|
766
818
|
text: Type.String(),
|
|
@@ -775,6 +827,7 @@ Usage rules:
|
|
|
775
827
|
background: Type.Optional(
|
|
776
828
|
Type.Boolean({ description: "If true, type silently without focusing Chrome. Default false." }),
|
|
777
829
|
),
|
|
830
|
+
trusted: Type.Optional(Type.Boolean({ description: "If true, dispatch through chrome.debugger / CDP so each keystroke is browser-trusted. Triggers Chrome's debugger banner." })),
|
|
778
831
|
host: Type.Optional(Type.String()),
|
|
779
832
|
port: Type.Optional(Type.Number()),
|
|
780
833
|
}),
|
|
@@ -807,6 +860,7 @@ Usage rules:
|
|
|
807
860
|
background: Type.Optional(
|
|
808
861
|
Type.Boolean({ description: "If true, fill silently without focusing Chrome. Default false." }),
|
|
809
862
|
),
|
|
863
|
+
trusted: Type.Optional(Type.Boolean({ description: "If true, dispatch through chrome.debugger / CDP for browser-trusted input. Triggers Chrome's debugger banner." })),
|
|
810
864
|
host: Type.Optional(Type.String()),
|
|
811
865
|
port: Type.Optional(Type.Number()),
|
|
812
866
|
}),
|
|
@@ -836,6 +890,7 @@ Usage rules:
|
|
|
836
890
|
background: Type.Optional(
|
|
837
891
|
Type.Boolean({ description: "If true, send the key silently without focusing Chrome. Default false." }),
|
|
838
892
|
),
|
|
893
|
+
trusted: Type.Optional(Type.Boolean({ description: "If true, dispatch through chrome.debugger / CDP so the keystroke is browser-trusted." })),
|
|
839
894
|
host: Type.Optional(Type.String()),
|
|
840
895
|
port: Type.Optional(Type.Number()),
|
|
841
896
|
}),
|
|
@@ -1006,6 +1061,7 @@ Usage rules:
|
|
|
1006
1061
|
urlIncludes: Type.Optional(Type.String()),
|
|
1007
1062
|
titleIncludes: Type.Optional(Type.String()),
|
|
1008
1063
|
background: Type.Optional(Type.Boolean()),
|
|
1064
|
+
trusted: Type.Optional(Type.Boolean({ description: "If true, dispatch through chrome.debugger / CDP for browser-trusted hover." })),
|
|
1009
1065
|
}),
|
|
1010
1066
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1011
1067
|
const result = await bridge.send("page.hover", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
@@ -1032,6 +1088,7 @@ Usage rules:
|
|
|
1032
1088
|
urlIncludes: Type.Optional(Type.String()),
|
|
1033
1089
|
titleIncludes: Type.Optional(Type.String()),
|
|
1034
1090
|
background: Type.Optional(Type.Boolean()),
|
|
1091
|
+
trusted: Type.Optional(Type.Boolean({ description: "If true, dispatch through chrome.debugger / CDP so the drag is browser-trusted (real HTML5 dragstart/drop with native DataTransfer)." })),
|
|
1035
1092
|
}),
|
|
1036
1093
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1037
1094
|
const result = await bridge.send("page.drag", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
@@ -1054,6 +1111,7 @@ Usage rules:
|
|
|
1054
1111
|
urlIncludes: Type.Optional(Type.String()),
|
|
1055
1112
|
titleIncludes: Type.Optional(Type.String()),
|
|
1056
1113
|
background: Type.Optional(Type.Boolean()),
|
|
1114
|
+
trusted: Type.Optional(Type.Boolean({ description: "If true, dispatch wheel events through chrome.debugger / CDP for browser-trusted scrolling." })),
|
|
1057
1115
|
}),
|
|
1058
1116
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1059
1117
|
const result = await bridge.send("page.scroll", withBackground(params), DEFAULT_TIMEOUT_MS);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-chrome",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Drive your existing logged-in Chrome from Pi — no re-login, no throwaway profile, watch the agent work in real time (or toggle quiet background mode).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|