pi-chrome 0.15.4 → 0.15.6
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 +17 -0
- package/README.md +19 -3
- package/SECURITY.md +6 -4
- package/docs/COMPARISON.md +1 -1
- package/docs/EXAMPLES.md +1 -1
- package/docs/FAQ.md +4 -2
- package/extensions/chrome-profile-bridge/browser-extension/manifest.json +1 -1
- package/extensions/chrome-profile-bridge/index.ts +201 -83
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-facing changes to `pi-chrome`.
|
|
4
4
|
|
|
5
|
+
## 0.15.6 — 2026-05-14
|
|
6
|
+
|
|
7
|
+
- **Bare `/chrome` is now a command menu.** Running `/chrome` shows interactive options for every `/chrome ...` command, including authorize/revoke/status/doctor/onboard/background variants.
|
|
8
|
+
|
|
9
|
+
## 0.15.5 — 2026-05-14
|
|
10
|
+
|
|
11
|
+
- **Chrome control authorization.** `chrome_*` tools are locked until the user runs `/chrome authorize` in the current Pi session. Grants can be one command, 15 minutes, 1 hour, or the session; `/chrome revoke` locks control again and `/chrome status` shows auth state.
|
|
12
|
+
- **Browser-origin bridge hardening.** The loopback bridge no longer sends wildcard CORS headers. `/command` accepts only local-process requests, while extension polling/result endpoints reject non-extension browser origins, blocking ordinary web pages from driving or draining the bridge through `127.0.0.1`.
|
|
13
|
+
|
|
14
|
+
## 0.15.4 — 2026-05-14
|
|
15
|
+
|
|
16
|
+
- **Quiet renamed to background.** Public UX now uses `/chrome background [on|off|toggle|status]` and docs/status say “run in background” instead of “quiet mode”.
|
|
17
|
+
|
|
18
|
+
## 0.15.3 — 2026-05-14
|
|
19
|
+
|
|
20
|
+
- **Chrome real input only.** Public trusted/synthetic mode controls were removed. Interactive tools now always use Chrome's real input layer; `/chrome clicks` and public `trusted` parameters are gone.
|
|
21
|
+
|
|
5
22
|
## 0.15.2 — 2026-05-13
|
|
6
23
|
|
|
7
24
|
- **Recipe prompts rewritten in user-language.** Earlier recipes leaked tool names into the `You:` prompts ("Use `chrome_tab list` to find my GitHub notifications tab…"), implying users need to know the tool catalog before they can ask anything. Prompts now read as natural intent; the agent trace below each one still shows the `chrome_*` primitives the agent picked. Affects the 30-second try-this block, all 3 hero recipes (PR triage / Linear standup / Bug repro), and 3 of the 6 collapsed recipes (auth-only data pull, network forensics, file upload).
|
package/README.md
CHANGED
|
@@ -34,10 +34,11 @@ Then in Pi:
|
|
|
34
34
|
|
|
35
35
|
On macOS this opens `chrome://extensions`, reveals the bundled `browser-extension/` folder in Finder, and copies its path to your clipboard. In Chrome: **Developer mode** → **Load unpacked** → paste the path. Done.
|
|
36
36
|
|
|
37
|
-
Verify:
|
|
37
|
+
Verify and authorize current Pi session:
|
|
38
38
|
|
|
39
39
|
```text
|
|
40
40
|
/chrome doctor
|
|
41
|
+
/chrome authorize
|
|
41
42
|
```
|
|
42
43
|
|
|
43
44
|
```text
|
|
@@ -185,6 +186,20 @@ Each tool is documented inline in Pi — agents see the parameters and gotchas (
|
|
|
185
186
|
|
|
186
187
|
`pi-chrome` drives interactive controls through Chrome's real input layer: clicks, typing, fill, keys, hover, drag, scroll, and touch. Under the hood it uses `chrome.debugger` / CDP, so input satisfies normal user-activation gates. Chrome may show the *"Pi Chrome Connector started debugging this browser"* banner while attached.
|
|
187
188
|
|
|
189
|
+
### Authorization
|
|
190
|
+
|
|
191
|
+
Chrome control is locked by default. Before any agent can use `chrome_*` tools, explicitly authorize the current Pi session:
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
/chrome authorize # authorize until this Pi session exits
|
|
195
|
+
/chrome authorize once # allow one Chrome command
|
|
196
|
+
/chrome authorize 15m # allow for 15 minutes
|
|
197
|
+
/chrome revoke # lock again
|
|
198
|
+
/chrome status # shows connection + auth + background
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This protects your signed-in Chrome profile from accidental agent use. The loopback bridge also rejects browser-origin command requests so arbitrary web pages cannot call into `127.0.0.1:17318` through CORS.
|
|
202
|
+
|
|
188
203
|
### Run in background / watch modes
|
|
189
204
|
|
|
190
205
|
By default, every `chrome_*` call focuses Chrome and activates the target tab so you can **watch the agent work** — invaluable for demos, debugging, and first-time confidence.
|
|
@@ -201,6 +216,7 @@ Per-call `background: true` wins over the session setting.
|
|
|
201
216
|
|
|
202
217
|
- `/chrome doctor` — single command: connectivity, extension version, bridge owner, version drift, MAIN-world helper injection, `chrome_evaluate("1+1") === 2`, fingerprint flags.
|
|
203
218
|
- `/chrome onboard` — guided first-time setup.
|
|
219
|
+
- `/chrome authorize status` — current Chrome-control authorization state.
|
|
204
220
|
- `/chrome background status` — current watch/background setting.
|
|
205
221
|
|
|
206
222
|
If the loaded Chrome extension is older than `pi-chrome` on disk, `/chrome doctor` tells you to reload it from `chrome://extensions`.
|
|
@@ -248,9 +264,9 @@ If you build a competing tool, please open a PR with your scores. We benchmark i
|
|
|
248
264
|
|
|
249
265
|
**Unpacked on purpose.** A Web Store extension cannot talk to a local bridge controlled by another tool on the same machine — so pi-chrome ships its bridge as an inspectable, MIT-licensed folder you load once with Developer Mode. Every line is yours to read in [`extensions/chrome-profile-bridge/browser-extension/`](./extensions/chrome-profile-bridge/browser-extension). `/chrome doctor` reports the loaded extension version and warns when it drifts from your installed `pi-chrome`.
|
|
250
266
|
|
|
251
|
-
The companion extension runs in the Chrome profile where you install it and has broad tab/scripting permissions. Only install it from a package source you trust.
|
|
267
|
+
The companion extension runs in the Chrome profile where you install it and has broad tab/scripting permissions. Only install it from a package source you trust. Even after install, `chrome_*` tools stay locked until you run `/chrome authorize` in Pi. Use `/chrome revoke` to lock them again.
|
|
252
268
|
|
|
253
|
-
The Pi side listens on `127.0.0.1:17318` by default. Override before starting Pi:
|
|
269
|
+
The Pi side listens on `127.0.0.1:17318` by default and rejects browser-origin command requests; ordinary web pages cannot use CORS to drive the bridge. Override before starting Pi:
|
|
254
270
|
|
|
255
271
|
```bash
|
|
256
272
|
PI_CHROME_BRIDGE_PORT=17319 pi
|
package/SECURITY.md
CHANGED
|
@@ -9,13 +9,14 @@ Open a GitHub issue prefixed with `[security]` at https://github.com/tianrendong
|
|
|
9
9
|
`pi-chrome` is a developer tool you install knowingly. It is **not** designed to defend against:
|
|
10
10
|
|
|
11
11
|
- Hostile pages running in your Chrome trying to detect or escape automation. (Standard browser security boundaries still apply, but a hostile page that already runs in your tab can do anything that page can already do.)
|
|
12
|
-
- Other processes on your local machine. The bridge binds to `127.0.0.1:17318` (loopback only)
|
|
12
|
+
- Other processes on your local machine. The bridge binds to `127.0.0.1:17318` (loopback only) and chrome_* tools require `/chrome authorize` inside Pi, but the bridge does not authenticate arbitrary non-browser local callers. If your threat model includes hostile local processes running as you, run pi-chrome on a separate user account.
|
|
13
13
|
|
|
14
14
|
`pi-chrome` **is** designed to:
|
|
15
15
|
|
|
16
16
|
- Never exfiltrate page state to the network. All communication is loopback (`127.0.0.1`).
|
|
17
17
|
- Surface every action with an honest result envelope so the agent can't silently do the wrong thing.
|
|
18
|
-
-
|
|
18
|
+
- Keep Chrome control locked until the user explicitly runs `/chrome authorize` in the current Pi session.
|
|
19
|
+
- Reject browser-origin command requests to the loopback bridge so ordinary web pages cannot use CORS to drive Chrome.
|
|
19
20
|
|
|
20
21
|
## The companion extension
|
|
21
22
|
|
|
@@ -24,8 +25,9 @@ The Chrome extension under `extensions/chrome-profile-bridge/browser-extension/`
|
|
|
24
25
|
## Defaults
|
|
25
26
|
|
|
26
27
|
- Loopback bridge only. No remote port. No telemetry.
|
|
27
|
-
-
|
|
28
|
-
-
|
|
28
|
+
- Chrome real input layer for interactive controls.
|
|
29
|
+
- Chrome control locked by default; `/chrome authorize` unlocks current Pi session, `/chrome revoke` locks it again.
|
|
30
|
+
- Run-in-background optional; tab/window focus is observable by default (the user can see Pi acting).
|
|
29
31
|
|
|
30
32
|
## Override the port
|
|
31
33
|
|
package/docs/COMPARISON.md
CHANGED
|
@@ -126,7 +126,7 @@ Yes — you can export cookies and replay them, or point Playwright at your exis
|
|
|
126
126
|
Different security boundary, not strictly safer.
|
|
127
127
|
|
|
128
128
|
- **CDP-based tools** require `chrome --remote-debugging-port=...`. That port is unauthenticated and exposes the whole browser to any local process. Easy to misconfigure.
|
|
129
|
-
- **pi-chrome** runs through an extension you install yourself with broad permissions (tabs, scripting, debugger, webNavigation). The bridge listens on `127.0.0.1:17318` loopback only. **Only install the bundled extension if you trust the source you got the npm package from.**
|
|
129
|
+
- **pi-chrome** runs through an extension you install yourself with broad permissions (tabs, scripting, debugger, webNavigation). The bridge listens on `127.0.0.1:17318` loopback only, rejects browser-origin command requests, and keeps chrome_* tools locked until `/chrome authorize` is run in the current Pi session. **Only install the bundled extension if you trust the source you got the npm package from.**
|
|
130
130
|
|
|
131
131
|
If your threat model excludes extensions with broad permissions, neither approach is a fit — you want a sandboxed CI runner.
|
|
132
132
|
|
package/docs/EXAMPLES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-chrome examples
|
|
2
2
|
|
|
3
|
-
Real, useful agent prompts. Drop any of these into Pi after running `/chrome onboard`. Each one uses Chrome tabs and accounts you already have.
|
|
3
|
+
Real, useful agent prompts. Drop any of these into Pi after running `/chrome onboard` and `/chrome authorize`. Each one uses Chrome tabs and accounts you already have.
|
|
4
4
|
|
|
5
5
|
## Daily workflow
|
|
6
6
|
|
package/docs/FAQ.md
CHANGED
|
@@ -24,11 +24,13 @@ That's Chrome's built-in warning when an extension uses `chrome.debugger`. pi-ch
|
|
|
24
24
|
|
|
25
25
|
## Can a malicious page escape and access my other tabs?
|
|
26
26
|
|
|
27
|
-
No — pages cannot directly talk to extensions. Commands flow agent → local bridge (`127.0.0.1:17318`) → extension → tab. The bridge binds to loopback only
|
|
27
|
+
No — pages cannot directly talk to extensions. Commands flow agent → local bridge (`127.0.0.1:17318`) → extension → tab. The bridge binds to loopback only and rejects browser-origin command requests, so ordinary web pages cannot use CORS to drive it.
|
|
28
|
+
|
|
29
|
+
Chrome control is also locked per Pi session until you run `/chrome authorize`; `/chrome revoke` locks it again. The remaining risk surface is **other local processes running as you** that can connect to loopback and imitate Pi. If that's in your threat model, run pi-chrome in a separate OS user account.
|
|
28
30
|
|
|
29
31
|
## Can multiple Pi sessions use it at once?
|
|
30
32
|
|
|
31
|
-
Yes. The first session opens the local bridge; later sessions detect it and pipe their commands through the same bridge.
|
|
33
|
+
Yes. The first session opens the local bridge; later sessions detect it and pipe their commands through the same bridge. Each Pi session must be authorized with `/chrome authorize` before its chrome_* tools work.
|
|
32
34
|
|
|
33
35
|
## Why can't this be on the Chrome Web Store?
|
|
34
36
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
|
|
4
2
|
import { Type } from "typebox";
|
|
5
3
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
4
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
@@ -127,12 +125,32 @@ function readRequestBody(request: IncomingMessage): Promise<string> {
|
|
|
127
125
|
});
|
|
128
126
|
}
|
|
129
127
|
|
|
128
|
+
function corsHeadersFor(request: IncomingMessage): Record<string, string> {
|
|
129
|
+
const origin = String(request.headers.origin ?? "");
|
|
130
|
+
if (!origin.startsWith("chrome-extension://")) return {};
|
|
131
|
+
return {
|
|
132
|
+
"access-control-allow-origin": origin,
|
|
133
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
134
|
+
"access-control-allow-headers": "content-type",
|
|
135
|
+
"access-control-expose-headers": "x-pi-chrome-version",
|
|
136
|
+
"vary": "origin",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isBrowserOriginAllowed(request: IncomingMessage): boolean {
|
|
141
|
+
const origin = String(request.headers.origin ?? "");
|
|
142
|
+
if (origin) return origin.startsWith("chrome-extension://");
|
|
143
|
+
const secFetchSite = String(request.headers["sec-fetch-site"] ?? "");
|
|
144
|
+
return !secFetchSite || secFetchSite === "none" || secFetchSite === "same-origin";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isLocalProcessRequest(request: IncomingMessage): boolean {
|
|
148
|
+
return !request.headers.origin && !request.headers["sec-fetch-site"];
|
|
149
|
+
}
|
|
150
|
+
|
|
130
151
|
function sendJson(response: ServerResponse, status: number, body: unknown, extraHeaders?: Record<string, string>): void {
|
|
131
152
|
response.writeHead(status, {
|
|
132
153
|
"content-type": "application/json; charset=utf-8",
|
|
133
|
-
"access-control-allow-origin": "*",
|
|
134
|
-
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
135
|
-
"access-control-allow-headers": "content-type",
|
|
136
154
|
"cache-control": "no-store",
|
|
137
155
|
...(extraHeaders ?? {}),
|
|
138
156
|
});
|
|
@@ -278,16 +296,25 @@ class ChromeProfileBridge {
|
|
|
278
296
|
}
|
|
279
297
|
|
|
280
298
|
private async handle(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
|
299
|
+
const url = new URL(request.url ?? "/", this.url);
|
|
300
|
+
const corsHeaders = corsHeadersFor(request);
|
|
281
301
|
if (request.method === "OPTIONS") {
|
|
282
|
-
|
|
302
|
+
if (!isBrowserOriginAllowed(request)) {
|
|
303
|
+
sendJson(response, 403, { ok: false, error: "browser origin not allowed" });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
sendJson(response, 200, { ok: true }, corsHeaders);
|
|
283
307
|
return;
|
|
284
308
|
}
|
|
285
|
-
const url = new URL(request.url ?? "/", this.url);
|
|
286
309
|
if (request.method === "GET" && url.pathname === "/status") {
|
|
287
310
|
sendJson(response, 200, this.status());
|
|
288
311
|
return;
|
|
289
312
|
}
|
|
290
313
|
if (request.method === "POST" && url.pathname === "/command") {
|
|
314
|
+
if (!isLocalProcessRequest(request)) {
|
|
315
|
+
sendJson(response, 403, { ok: false, error: "Chrome commands are accepted only from local Pi processes" });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
291
318
|
const body = JSON.parse(await readRequestBody(request)) as {
|
|
292
319
|
action?: string;
|
|
293
320
|
params?: Record<string, unknown>;
|
|
@@ -306,6 +333,10 @@ class ChromeProfileBridge {
|
|
|
306
333
|
return;
|
|
307
334
|
}
|
|
308
335
|
if (request.method === "GET" && url.pathname === "/next") {
|
|
336
|
+
if (!isBrowserOriginAllowed(request)) {
|
|
337
|
+
sendJson(response, 403, { ok: false, error: "browser origin not allowed" });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
309
340
|
this.lastSeenAt = Date.now();
|
|
310
341
|
this.clientName = url.searchParams.get("name") ?? undefined;
|
|
311
342
|
let aborted = false;
|
|
@@ -334,23 +365,27 @@ class ChromeProfileBridge {
|
|
|
334
365
|
command
|
|
335
366
|
? { type: "command", command, expectedExtensionVersion: currentVersion }
|
|
336
367
|
: { type: "none", expectedExtensionVersion: currentVersion },
|
|
337
|
-
{ "x-pi-chrome-version": currentVersion },
|
|
368
|
+
{ ...corsHeaders, "x-pi-chrome-version": currentVersion },
|
|
338
369
|
);
|
|
339
370
|
return;
|
|
340
371
|
}
|
|
341
372
|
if (request.method === "POST" && url.pathname === "/result") {
|
|
373
|
+
if (!isBrowserOriginAllowed(request)) {
|
|
374
|
+
sendJson(response, 403, { ok: false, error: "browser origin not allowed" });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
342
377
|
this.lastSeenAt = Date.now();
|
|
343
378
|
const result = JSON.parse(await readRequestBody(request)) as BridgeResult;
|
|
344
379
|
const pending = this.pending.get(result.id);
|
|
345
380
|
if (!pending) {
|
|
346
|
-
sendJson(response, 404, { ok: false, error: "unknown command id" });
|
|
381
|
+
sendJson(response, 404, { ok: false, error: "unknown command id" }, corsHeaders);
|
|
347
382
|
return;
|
|
348
383
|
}
|
|
349
384
|
clearTimeout(pending.timer);
|
|
350
385
|
this.pending.delete(result.id);
|
|
351
386
|
if (result.ok) pending.resolve(result.result);
|
|
352
387
|
else pending.reject(new Error(result.error ?? "Chrome extension command failed"));
|
|
353
|
-
sendJson(response, 200, { ok: true });
|
|
388
|
+
sendJson(response, 200, { ok: true }, corsHeaders);
|
|
354
389
|
return;
|
|
355
390
|
}
|
|
356
391
|
sendJson(response, 404, { error: "not found" });
|
|
@@ -399,6 +434,46 @@ export default function (pi: ExtensionAPI): void {
|
|
|
399
434
|
|
|
400
435
|
const bridge = new ChromeProfileBridge(DEFAULT_HOST, DEFAULT_PORT);
|
|
401
436
|
let backgroundDefault = false;
|
|
437
|
+
let chromeAuthorizedUntil: number | "session" | undefined;
|
|
438
|
+
let chromeAuthorizedCalls: number | undefined;
|
|
439
|
+
|
|
440
|
+
const authSummary = (): string => {
|
|
441
|
+
if (chromeAuthorizedUntil === "session") return "authorized for this session";
|
|
442
|
+
if (typeof chromeAuthorizedUntil === "number") {
|
|
443
|
+
const remainingMs = chromeAuthorizedUntil - Date.now();
|
|
444
|
+
if (remainingMs > 0) {
|
|
445
|
+
const minutes = Math.ceil(remainingMs / 60_000);
|
|
446
|
+
return `authorized for ${chromeAuthorizedCalls === 1 ? "one Chrome command" : `~${minutes}m`}`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return "locked";
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const chromeControlAuthorized = (): boolean => {
|
|
453
|
+
if (chromeAuthorizedUntil === "session") return true;
|
|
454
|
+
if (typeof chromeAuthorizedUntil === "number" && chromeAuthorizedUntil > Date.now()) return true;
|
|
455
|
+
chromeAuthorizedUntil = undefined;
|
|
456
|
+
chromeAuthorizedCalls = undefined;
|
|
457
|
+
return false;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const requireChromeControlAuthorized = (): void => {
|
|
461
|
+
if (!chromeControlAuthorized()) {
|
|
462
|
+
throw new Error("Chrome control locked. Ask the user to run /chrome authorize before using chrome_* tools.");
|
|
463
|
+
}
|
|
464
|
+
if (chromeAuthorizedCalls !== undefined) {
|
|
465
|
+
chromeAuthorizedCalls -= 1;
|
|
466
|
+
if (chromeAuthorizedCalls <= 0) {
|
|
467
|
+
chromeAuthorizedUntil = undefined;
|
|
468
|
+
chromeAuthorizedCalls = undefined;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const authorizedBridgeSend = (action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<unknown> => {
|
|
474
|
+
requireChromeControlAuthorized();
|
|
475
|
+
return bridge.send(action, params, timeoutMs);
|
|
476
|
+
};
|
|
402
477
|
|
|
403
478
|
// Translate the public `background` parameter (default false = visible/foreground) into the
|
|
404
479
|
// service worker's wire-level `foreground` flag, accepting legacy `foreground` as a fallback.
|
|
@@ -420,8 +495,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
420
495
|
ctx.ui.setStatus("chrome", `Chrome bridge :${DEFAULT_PORT}`);
|
|
421
496
|
ctx.ui.notify(
|
|
422
497
|
status.mode === "client"
|
|
423
|
-
? `pi-chrome connected (sharing the Chrome connection an earlier pi session opened).`
|
|
424
|
-
: `pi-chrome is ready and waiting for the Chrome companion to connect. Run /chrome onboard to install it.`,
|
|
498
|
+
? `pi-chrome connected (sharing the Chrome connection an earlier pi session opened). Run /chrome authorize before using chrome_* tools.`
|
|
499
|
+
: `pi-chrome is ready and waiting for the Chrome companion to connect. Run /chrome onboard to install it, then /chrome authorize to allow chrome_* tools.`,
|
|
425
500
|
"info",
|
|
426
501
|
);
|
|
427
502
|
});
|
|
@@ -442,13 +517,14 @@ Capability model (important):
|
|
|
442
517
|
- Tool results include \`pageMutated\`, \`defaultPrevented\`, \`elementVisible\`, \`occludedBy\`, and (for type/fill) \`valueMatches\`. If an action result indicates no page change or occlusion, inspect current page state instead of repeating blindly.
|
|
443
518
|
|
|
444
519
|
Usage rules:
|
|
445
|
-
1.
|
|
446
|
-
2. \`
|
|
447
|
-
3.
|
|
448
|
-
4. \`
|
|
449
|
-
5.
|
|
450
|
-
6.
|
|
451
|
-
7.
|
|
520
|
+
1. If a chrome_* tool says Chrome control is locked, ask the user to run \`/chrome authorize\` before retrying.
|
|
521
|
+
2. \`chrome_snapshot\` before clicking/typing; pass \`uid\` over \`selector\`.
|
|
522
|
+
3. \`includeSnapshot=true\` on click/type/fill to verify in one round trip.
|
|
523
|
+
4. If \`chrome_evaluate\` returns null when you expected a value, the expression evaluated to null/undefined in the page; surface the value via \`JSON.stringify\` to confirm.
|
|
524
|
+
5. \`chrome_navigate\` supports an optional \`initScript\` that runs at document_start in MAIN world for the next navigation (good for seeding localStorage or stubbing Date.now).
|
|
525
|
+
6. By default chrome_* tools focus Chrome so the user can watch; pass \`background=true\` or run /chrome background on for session-wide background execution.
|
|
526
|
+
7. If you hit a native file-picker or privileged browser prompt gate, tell the user; generic clicks/typing/CSP gates are handled by Chrome input.
|
|
527
|
+
8. Run /chrome doctor when in doubt about connectivity or capabilities.
|
|
452
528
|
</chrome-profile-bridge>`;
|
|
453
529
|
return { systemPrompt: event.systemPrompt + primer };
|
|
454
530
|
});
|
|
@@ -544,6 +620,42 @@ Usage rules:
|
|
|
544
620
|
ctx.ui.notify(`Run in background → ${nextLabel}. ${BACKGROUND_DESC[nextLabel]}`, "info");
|
|
545
621
|
};
|
|
546
622
|
|
|
623
|
+
const authorizeHandler = async (ctx: ExtensionContext, args: string) => {
|
|
624
|
+
const arg = (args || "").trim().toLowerCase() || "session";
|
|
625
|
+
if (arg === "status") {
|
|
626
|
+
ctx.ui.notify(`Chrome control auth: ${authSummary()}.`, "info");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const options: Record<string, { label: string; until: number | "session"; calls?: number }> = {
|
|
630
|
+
once: { label: "one Chrome command", until: Date.now() + 10 * 60_000, calls: 1 },
|
|
631
|
+
"15m": { label: "15 minutes", until: Date.now() + 15 * 60_000 },
|
|
632
|
+
"1h": { label: "1 hour", until: Date.now() + 60 * 60_000 },
|
|
633
|
+
session: { label: "this Pi session", until: "session" },
|
|
634
|
+
};
|
|
635
|
+
const grant = options[arg];
|
|
636
|
+
if (!grant) {
|
|
637
|
+
ctx.ui.notify("Unknown authorize duration. Pick one of: once | 15m | 1h | session | status.", "warning");
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const ok = await ctx.ui.confirm(
|
|
641
|
+
"Authorize pi-chrome control?",
|
|
642
|
+
`This Pi session will be allowed to inspect and control your existing Chrome profile for ${grant.label}.\n\nChrome actions use your signed-in browser state and real input. Only approve if you trust the current agent/task.`,
|
|
643
|
+
);
|
|
644
|
+
if (!ok) {
|
|
645
|
+
ctx.ui.notify("Chrome control remains locked.", "info");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
chromeAuthorizedUntil = grant.until;
|
|
649
|
+
chromeAuthorizedCalls = grant.calls;
|
|
650
|
+
ctx.ui.notify(`Chrome control authorized for ${grant.label}.`, "info");
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const revokeHandler = (ctx: ExtensionContext) => {
|
|
654
|
+
chromeAuthorizedUntil = undefined;
|
|
655
|
+
chromeAuthorizedCalls = undefined;
|
|
656
|
+
ctx.ui.notify("Chrome control locked. Run /chrome authorize to allow chrome_* tools again.", "info");
|
|
657
|
+
};
|
|
658
|
+
|
|
547
659
|
const onboardHandler = async (ctx: ExtensionContext) => {
|
|
548
660
|
const extensionPath = browserExtensionPath();
|
|
549
661
|
const proceed = await ctx.ui.confirm(
|
|
@@ -579,6 +691,7 @@ Usage rules:
|
|
|
579
691
|
} catch {
|
|
580
692
|
parts.push(`✗ Chrome not responding`);
|
|
581
693
|
}
|
|
694
|
+
parts.push(`auth: ${authSummary()}`);
|
|
582
695
|
parts.push(`background: ${backgroundDefault ? "on" : "off"}`);
|
|
583
696
|
return parts.join(" · ");
|
|
584
697
|
};
|
|
@@ -587,52 +700,45 @@ Usage rules:
|
|
|
587
700
|
ctx.ui.notify(await statusSummary(), "info");
|
|
588
701
|
};
|
|
589
702
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
return {
|
|
626
|
-
render: (w) => container.render(w),
|
|
627
|
-
invalidate: () => container.invalidate(),
|
|
628
|
-
handleInput: (data: string) => list.handleInput(data),
|
|
629
|
-
};
|
|
630
|
-
});
|
|
703
|
+
const openCommandMenu = async (ctx: ExtensionContext): Promise<void> => {
|
|
704
|
+
const choice = await ctx.ui.select("pi-chrome", [
|
|
705
|
+
"/chrome authorize",
|
|
706
|
+
"/chrome authorize once",
|
|
707
|
+
"/chrome authorize 15m",
|
|
708
|
+
"/chrome authorize 1h",
|
|
709
|
+
"/chrome authorize status",
|
|
710
|
+
"/chrome revoke",
|
|
711
|
+
"/chrome status",
|
|
712
|
+
"/chrome doctor",
|
|
713
|
+
"/chrome onboard",
|
|
714
|
+
"/chrome background",
|
|
715
|
+
"/chrome background toggle",
|
|
716
|
+
"/chrome background on",
|
|
717
|
+
"/chrome background off",
|
|
718
|
+
"/chrome background status",
|
|
719
|
+
]);
|
|
720
|
+
if (!choice) return;
|
|
721
|
+
switch (choice) {
|
|
722
|
+
case "/chrome authorize": return authorizeHandler(ctx, "session");
|
|
723
|
+
case "/chrome authorize once": return authorizeHandler(ctx, "once");
|
|
724
|
+
case "/chrome authorize 15m": return authorizeHandler(ctx, "15m");
|
|
725
|
+
case "/chrome authorize 1h": return authorizeHandler(ctx, "1h");
|
|
726
|
+
case "/chrome authorize status": return authorizeHandler(ctx, "status");
|
|
727
|
+
case "/chrome revoke": return revokeHandler(ctx);
|
|
728
|
+
case "/chrome status": return statusHandler(ctx);
|
|
729
|
+
case "/chrome doctor": return doctorHandler(ctx);
|
|
730
|
+
case "/chrome onboard": return onboardHandler(ctx);
|
|
731
|
+
case "/chrome background": return backgroundHandler(ctx, "");
|
|
732
|
+
case "/chrome background toggle": return backgroundHandler(ctx, "toggle");
|
|
733
|
+
case "/chrome background on": return backgroundHandler(ctx, "on");
|
|
734
|
+
case "/chrome background off": return backgroundHandler(ctx, "off");
|
|
735
|
+
case "/chrome background status": return backgroundHandler(ctx, "status");
|
|
736
|
+
}
|
|
631
737
|
};
|
|
632
738
|
|
|
633
739
|
pi.registerCommand("chrome", {
|
|
634
740
|
description:
|
|
635
|
-
"All pi-chrome controls in one place.\n /chrome status — one-line snapshot of connection
|
|
741
|
+
"All pi-chrome controls in one place.\n /chrome authorize [once|15m|1h|session|status] — allow this Pi session to use chrome_* tools.\n /chrome revoke — lock Chrome control.\n /chrome status — one-line snapshot of connection, auth, and background setting.\n /chrome doctor — full health check.\n /chrome onboard — install the Chrome companion extension.\n /chrome background [on|off|status|toggle] — whether pi-chrome runs without focusing Chrome.\nRun with no arguments for an interactive picker that shows current state.",
|
|
636
742
|
getArgumentCompletions: (prefix) => {
|
|
637
743
|
const raw = prefix;
|
|
638
744
|
const trimmedRight = raw.replace(/\s+$/, "");
|
|
@@ -649,11 +755,21 @@ Usage rules:
|
|
|
649
755
|
let candidates: Item[] = [];
|
|
650
756
|
if (path.length === 0) {
|
|
651
757
|
candidates = [
|
|
652
|
-
{ fullValue: "
|
|
758
|
+
{ fullValue: "authorize", label: "authorize", description: "Allow this Pi session to use chrome_* tools." },
|
|
759
|
+
{ fullValue: "revoke", label: "revoke", description: "Lock Chrome control for this Pi session." },
|
|
760
|
+
{ fullValue: "status", label: "status", description: "One-line summary: connection, auth, and background setting." },
|
|
653
761
|
{ fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." },
|
|
654
762
|
{ fullValue: "onboard", label: "onboard", description: "Install the Chrome companion extension (first-time setup)." },
|
|
655
763
|
{ fullValue: "background", label: "background", description: "Run pi-chrome in the background without focusing Chrome?" },
|
|
656
764
|
];
|
|
765
|
+
} else if (path[0] === "authorize" && path.length === 1) {
|
|
766
|
+
candidates = [
|
|
767
|
+
{ fullValue: "authorize once", label: "once", description: "Authorize one Chrome command." },
|
|
768
|
+
{ fullValue: "authorize 15m", label: "15m", description: "Authorize Chrome control for 15 minutes." },
|
|
769
|
+
{ fullValue: "authorize 1h", label: "1h", description: "Authorize Chrome control for 1 hour." },
|
|
770
|
+
{ fullValue: "authorize session", label: "session", description: "Authorize Chrome control until this Pi session exits." },
|
|
771
|
+
{ fullValue: "authorize status", label: "status", description: "Show Chrome control authorization state." },
|
|
772
|
+
];
|
|
657
773
|
} else if (path[0] === "background" && path.length === 1) {
|
|
658
774
|
candidates = [
|
|
659
775
|
{ fullValue: "background on", label: "on", description: "Run in background. Chrome stays in the background. Your editor keeps focus." },
|
|
@@ -670,12 +786,14 @@ Usage rules:
|
|
|
670
786
|
handler: async (args, ctx) => {
|
|
671
787
|
const tokens = (args || "").trim().split(/\s+/).filter(Boolean);
|
|
672
788
|
if (tokens.length === 0) {
|
|
673
|
-
await
|
|
789
|
+
await openCommandMenu(ctx);
|
|
674
790
|
return;
|
|
675
791
|
}
|
|
676
792
|
const [head, ...rest] = tokens;
|
|
677
793
|
const subArgs = rest.join(" ");
|
|
678
794
|
switch (head) {
|
|
795
|
+
case "authorize": return authorizeHandler(ctx, subArgs);
|
|
796
|
+
case "revoke": return revokeHandler(ctx);
|
|
679
797
|
case "status": return statusHandler(ctx);
|
|
680
798
|
case "doctor": return doctorHandler(ctx);
|
|
681
799
|
case "onboard": return onboardHandler(ctx);
|
|
@@ -689,7 +807,7 @@ Usage rules:
|
|
|
689
807
|
return;
|
|
690
808
|
}
|
|
691
809
|
default:
|
|
692
|
-
ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome status | doctor | onboard | background.`, "warning");
|
|
810
|
+
ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome authorize | revoke | status | doctor | onboard | background.`, "warning");
|
|
693
811
|
}
|
|
694
812
|
},
|
|
695
813
|
});
|
|
@@ -709,7 +827,7 @@ Usage rules:
|
|
|
709
827
|
}),
|
|
710
828
|
async execute(_id, params, _signal, _onUpdate, ctx): Promise<ToolTextResult> {
|
|
711
829
|
if (params.url && bridge.connected) {
|
|
712
|
-
const result = await
|
|
830
|
+
const result = await authorizedBridgeSend("tab.new", { url: params.url }, DEFAULT_TIMEOUT_MS);
|
|
713
831
|
return { content: [{ type: "text", text: `Chrome bridge connected; opened ${params.url}` }], details: { status: bridge.status(), result } };
|
|
714
832
|
}
|
|
715
833
|
return {
|
|
@@ -744,7 +862,7 @@ Usage rules:
|
|
|
744
862
|
port: Type.Optional(Type.Number()),
|
|
745
863
|
}),
|
|
746
864
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
747
|
-
const result = await
|
|
865
|
+
const result = await authorizedBridgeSend(`tab.${params.action}`, params, DEFAULT_TIMEOUT_MS);
|
|
748
866
|
if (params.action === "list") {
|
|
749
867
|
const tabs = result as Array<{ id: number; title: string; url: string; active: boolean; windowId: number }>;
|
|
750
868
|
const text = tabs.map((tab) => `${tab.id}\t${tab.active ? "*" : " "}\t${tab.title || "(untitled)"}\t${tab.url}`).join("\n") || "No tabs.";
|
|
@@ -775,7 +893,7 @@ Usage rules:
|
|
|
775
893
|
port: Type.Optional(Type.Number()),
|
|
776
894
|
}),
|
|
777
895
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
778
|
-
const snapshot = await
|
|
896
|
+
const snapshot = await authorizedBridgeSend(
|
|
779
897
|
"page.snapshot",
|
|
780
898
|
withBackground({ ...params, maxElements: params.maxElements ?? MAX_ELEMENTS }),
|
|
781
899
|
DEFAULT_TIMEOUT_MS,
|
|
@@ -805,7 +923,7 @@ Usage rules:
|
|
|
805
923
|
port: Type.Optional(Type.Number()),
|
|
806
924
|
}),
|
|
807
925
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
808
|
-
const result = await
|
|
926
|
+
const result = await authorizedBridgeSend("page.navigate", withBackground(params), (params.timeoutMs ?? 15_000) + 2_000);
|
|
809
927
|
return { content: [{ type: "text", text: `Navigated to ${params.url}${params.initScript ? " (with initScript)" : ""}` }], details: { result: result as Json } };
|
|
810
928
|
},
|
|
811
929
|
});
|
|
@@ -829,7 +947,7 @@ Usage rules:
|
|
|
829
947
|
port: Type.Optional(Type.Number()),
|
|
830
948
|
}),
|
|
831
949
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
832
|
-
const value = await
|
|
950
|
+
const value = await authorizedBridgeSend("page.evaluate", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
833
951
|
const text = value === undefined
|
|
834
952
|
? "undefined"
|
|
835
953
|
: typeof value === "string"
|
|
@@ -862,7 +980,7 @@ Usage rules:
|
|
|
862
980
|
port: Type.Optional(Type.Number()),
|
|
863
981
|
}),
|
|
864
982
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
865
|
-
const raw = await
|
|
983
|
+
const raw = await authorizedBridgeSend("page.click", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
866
984
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
867
985
|
const summary = summarizeActionResult(result);
|
|
868
986
|
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
|
@@ -894,7 +1012,7 @@ Usage rules:
|
|
|
894
1012
|
port: Type.Optional(Type.Number()),
|
|
895
1013
|
}),
|
|
896
1014
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
897
|
-
const raw = await
|
|
1015
|
+
const raw = await authorizedBridgeSend("page.type", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
898
1016
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
899
1017
|
const summary = summarizeActionResult(result);
|
|
900
1018
|
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
|
@@ -926,7 +1044,7 @@ Usage rules:
|
|
|
926
1044
|
port: Type.Optional(Type.Number()),
|
|
927
1045
|
}),
|
|
928
1046
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
929
|
-
const raw = await
|
|
1047
|
+
const raw = await authorizedBridgeSend("page.fill", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
930
1048
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
931
1049
|
const summary = summarizeActionResult(result);
|
|
932
1050
|
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
|
@@ -961,7 +1079,7 @@ Usage rules:
|
|
|
961
1079
|
port: Type.Optional(Type.Number()),
|
|
962
1080
|
}),
|
|
963
1081
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
964
|
-
const raw = await
|
|
1082
|
+
const raw = await authorizedBridgeSend("page.key", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
965
1083
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
966
1084
|
const summary = summarizeActionResult(result);
|
|
967
1085
|
const base = `Pressed ${params.key}.`;
|
|
@@ -986,7 +1104,7 @@ Usage rules:
|
|
|
986
1104
|
port: Type.Optional(Type.Number()),
|
|
987
1105
|
}),
|
|
988
1106
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
989
|
-
const result = await
|
|
1107
|
+
const result = await authorizedBridgeSend("page.waitFor", params, (params.timeoutMs ?? 10_000) + 2_000);
|
|
990
1108
|
return { content: [{ type: "text", text: `Observed ${params.kind}: ${params.value}` }], details: { result: result as Json } };
|
|
991
1109
|
},
|
|
992
1110
|
});
|
|
@@ -1007,7 +1125,7 @@ Usage rules:
|
|
|
1007
1125
|
port: Type.Optional(Type.Number()),
|
|
1008
1126
|
}),
|
|
1009
1127
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1010
|
-
const result = await
|
|
1128
|
+
const result = await authorizedBridgeSend("page.console.list", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1011
1129
|
return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } };
|
|
1012
1130
|
},
|
|
1013
1131
|
});
|
|
@@ -1029,7 +1147,7 @@ Usage rules:
|
|
|
1029
1147
|
port: Type.Optional(Type.Number()),
|
|
1030
1148
|
}),
|
|
1031
1149
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1032
|
-
const result = await
|
|
1150
|
+
const result = await authorizedBridgeSend("page.network.list", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1033
1151
|
return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } };
|
|
1034
1152
|
},
|
|
1035
1153
|
});
|
|
@@ -1049,7 +1167,7 @@ Usage rules:
|
|
|
1049
1167
|
port: Type.Optional(Type.Number()),
|
|
1050
1168
|
}),
|
|
1051
1169
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1052
|
-
const result = await
|
|
1170
|
+
const result = await authorizedBridgeSend("page.network.get", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1053
1171
|
return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } };
|
|
1054
1172
|
},
|
|
1055
1173
|
});
|
|
@@ -1079,7 +1197,7 @@ Usage rules:
|
|
|
1079
1197
|
const cwd = workspaceCwd(ctx);
|
|
1080
1198
|
const defaultPath = join(cwd, ".pi", "chrome-screenshots", `${new Date().toISOString().replace(/[:.]/g, "-")}.${format}`);
|
|
1081
1199
|
const outputPath = params.path ? resolve(cwd, params.path) : defaultPath;
|
|
1082
|
-
const result = (await
|
|
1200
|
+
const result = (await authorizedBridgeSend("page.screenshot", withBackground(params), params.fullPage ? 120_000 : DEFAULT_TIMEOUT_MS)) as {
|
|
1083
1201
|
dataUrl?: string;
|
|
1084
1202
|
tab?: unknown;
|
|
1085
1203
|
fullPage?: boolean;
|
|
@@ -1129,7 +1247,7 @@ Usage rules:
|
|
|
1129
1247
|
background: Type.Optional(Type.Boolean()),
|
|
1130
1248
|
}),
|
|
1131
1249
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1132
|
-
const result = await
|
|
1250
|
+
const result = await authorizedBridgeSend("page.hover", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1133
1251
|
return { content: [{ type: "text", text: `Hovered ${params.uid ?? params.selector ?? `${params.x},${params.y}`}` }], details: { result: result as Json } };
|
|
1134
1252
|
},
|
|
1135
1253
|
});
|
|
@@ -1155,7 +1273,7 @@ Usage rules:
|
|
|
1155
1273
|
background: Type.Optional(Type.Boolean()),
|
|
1156
1274
|
}),
|
|
1157
1275
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1158
|
-
const result = await
|
|
1276
|
+
const result = await authorizedBridgeSend("page.drag", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1159
1277
|
return { content: [{ type: "text", text: `Dragged from ${params.fromUid ?? params.fromSelector} to ${params.toUid ?? params.toSelector}` }], details: { result: result as Json } };
|
|
1160
1278
|
},
|
|
1161
1279
|
});
|
|
@@ -1177,7 +1295,7 @@ Usage rules:
|
|
|
1177
1295
|
background: Type.Optional(Type.Boolean()),
|
|
1178
1296
|
}),
|
|
1179
1297
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1180
|
-
const result = await
|
|
1298
|
+
const result = await authorizedBridgeSend("page.tap", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1181
1299
|
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
|
1182
1300
|
return { content: [{ type: "text", text: `Tapped ${target} (touch)` }], details: { result: result as Json } };
|
|
1183
1301
|
},
|
|
@@ -1200,7 +1318,7 @@ Usage rules:
|
|
|
1200
1318
|
background: Type.Optional(Type.Boolean()),
|
|
1201
1319
|
}),
|
|
1202
1320
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1203
|
-
const result = await
|
|
1321
|
+
const result = await authorizedBridgeSend("page.scroll", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1204
1322
|
return { content: [{ type: "text", text: `Scrolled dy=${params.deltaY ?? 0} dx=${params.deltaX ?? 0}` }], details: { result: result as Json } };
|
|
1205
1323
|
},
|
|
1206
1324
|
});
|
|
@@ -1222,7 +1340,7 @@ Usage rules:
|
|
|
1222
1340
|
async execute(_id, params, _signal, _onUpdate, ctx): Promise<ToolTextResult> {
|
|
1223
1341
|
const cwd = workspaceCwd(ctx);
|
|
1224
1342
|
const paths = params.paths.map((p) => resolve(cwd, p));
|
|
1225
|
-
const result = await
|
|
1343
|
+
const result = await authorizedBridgeSend("page.upload", withBackground({ ...params, paths }), DEFAULT_TIMEOUT_MS);
|
|
1226
1344
|
return { content: [{ type: "text", text: `Uploaded ${paths.length} file(s) to ${params.uid ?? params.selector}` }], details: { result: result as Json } };
|
|
1227
1345
|
},
|
|
1228
1346
|
});
|