pi-chrome 0.15.3 → 0.15.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +26 -10
- 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 +189 -63
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-facing changes to `pi-chrome`.
|
|
4
4
|
|
|
5
|
+
## 0.15.5 — 2026-05-14
|
|
6
|
+
|
|
7
|
+
- **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.
|
|
8
|
+
- **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`.
|
|
9
|
+
|
|
10
|
+
## 0.15.4 — 2026-05-14
|
|
11
|
+
|
|
12
|
+
- **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”.
|
|
13
|
+
|
|
14
|
+
## 0.15.3 — 2026-05-14
|
|
15
|
+
|
|
16
|
+
- **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.
|
|
17
|
+
|
|
5
18
|
## 0.15.2 — 2026-05-13
|
|
6
19
|
|
|
7
20
|
- **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
|
|
@@ -134,7 +135,7 @@ You: [files the ticket with the folder attached]
|
|
|
134
135
|
| Uses your real signed-in Chrome | ✅ extension in your profile | ❌ throwaway profile | ⚠️ requires `--remote-debug` | ❌ throwaway profile |
|
|
135
136
|
| Re-login required | **Never** | Every run | Sometimes | Every run |
|
|
136
137
|
| **Multiple agents drive the same Chrome at once** | ✅ shared bridge | ❌ port collisions | ❌ | ❌ |
|
|
137
|
-
| Watch agent work, live | ✅ default;
|
|
138
|
+
| Watch agent work, live | ✅ default; run in background optional | ❌ headless or new window | ⚠️ debugger banner always | ❌ new window |
|
|
138
139
|
| Real browser input | ✅ always for input tools | ✅ | ✅ | ✅ |
|
|
139
140
|
| Network/console capture | ✅ built-in | ✅ | ✅ | ⚠️ via extensions |
|
|
140
141
|
| **Honest result envelopes¹** | ✅ | ⚠️ | ❌ | ❌ |
|
|
@@ -185,23 +186,38 @@ 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
|
|
|
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
|
+
|
|
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.
|
|
191
206
|
|
|
192
207
|
```text
|
|
193
|
-
/chrome
|
|
194
|
-
/chrome
|
|
195
|
-
/chrome
|
|
208
|
+
/chrome background # toggle for the whole session
|
|
209
|
+
/chrome background on # run in background
|
|
210
|
+
/chrome background off # bring Chrome forward so you can watch
|
|
196
211
|
```
|
|
197
212
|
|
|
198
|
-
Per-call `background: true` wins over the session
|
|
213
|
+
Per-call `background: true` wins over the session setting.
|
|
199
214
|
|
|
200
215
|
### Diagnostics
|
|
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.
|
|
204
|
-
- `/chrome
|
|
219
|
+
- `/chrome authorize status` — current Chrome-control authorization state.
|
|
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`.
|
|
207
223
|
|
|
@@ -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
|
|
|
@@ -127,12 +127,32 @@ function readRequestBody(request: IncomingMessage): Promise<string> {
|
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
function corsHeadersFor(request: IncomingMessage): Record<string, string> {
|
|
131
|
+
const origin = String(request.headers.origin ?? "");
|
|
132
|
+
if (!origin.startsWith("chrome-extension://")) return {};
|
|
133
|
+
return {
|
|
134
|
+
"access-control-allow-origin": origin,
|
|
135
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
136
|
+
"access-control-allow-headers": "content-type",
|
|
137
|
+
"access-control-expose-headers": "x-pi-chrome-version",
|
|
138
|
+
"vary": "origin",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isBrowserOriginAllowed(request: IncomingMessage): boolean {
|
|
143
|
+
const origin = String(request.headers.origin ?? "");
|
|
144
|
+
if (origin) return origin.startsWith("chrome-extension://");
|
|
145
|
+
const secFetchSite = String(request.headers["sec-fetch-site"] ?? "");
|
|
146
|
+
return !secFetchSite || secFetchSite === "none" || secFetchSite === "same-origin";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isLocalProcessRequest(request: IncomingMessage): boolean {
|
|
150
|
+
return !request.headers.origin && !request.headers["sec-fetch-site"];
|
|
151
|
+
}
|
|
152
|
+
|
|
130
153
|
function sendJson(response: ServerResponse, status: number, body: unknown, extraHeaders?: Record<string, string>): void {
|
|
131
154
|
response.writeHead(status, {
|
|
132
155
|
"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
156
|
"cache-control": "no-store",
|
|
137
157
|
...(extraHeaders ?? {}),
|
|
138
158
|
});
|
|
@@ -278,16 +298,25 @@ class ChromeProfileBridge {
|
|
|
278
298
|
}
|
|
279
299
|
|
|
280
300
|
private async handle(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
|
301
|
+
const url = new URL(request.url ?? "/", this.url);
|
|
302
|
+
const corsHeaders = corsHeadersFor(request);
|
|
281
303
|
if (request.method === "OPTIONS") {
|
|
282
|
-
|
|
304
|
+
if (!isBrowserOriginAllowed(request)) {
|
|
305
|
+
sendJson(response, 403, { ok: false, error: "browser origin not allowed" });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
sendJson(response, 200, { ok: true }, corsHeaders);
|
|
283
309
|
return;
|
|
284
310
|
}
|
|
285
|
-
const url = new URL(request.url ?? "/", this.url);
|
|
286
311
|
if (request.method === "GET" && url.pathname === "/status") {
|
|
287
312
|
sendJson(response, 200, this.status());
|
|
288
313
|
return;
|
|
289
314
|
}
|
|
290
315
|
if (request.method === "POST" && url.pathname === "/command") {
|
|
316
|
+
if (!isLocalProcessRequest(request)) {
|
|
317
|
+
sendJson(response, 403, { ok: false, error: "Chrome commands are accepted only from local Pi processes" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
291
320
|
const body = JSON.parse(await readRequestBody(request)) as {
|
|
292
321
|
action?: string;
|
|
293
322
|
params?: Record<string, unknown>;
|
|
@@ -306,6 +335,10 @@ class ChromeProfileBridge {
|
|
|
306
335
|
return;
|
|
307
336
|
}
|
|
308
337
|
if (request.method === "GET" && url.pathname === "/next") {
|
|
338
|
+
if (!isBrowserOriginAllowed(request)) {
|
|
339
|
+
sendJson(response, 403, { ok: false, error: "browser origin not allowed" });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
309
342
|
this.lastSeenAt = Date.now();
|
|
310
343
|
this.clientName = url.searchParams.get("name") ?? undefined;
|
|
311
344
|
let aborted = false;
|
|
@@ -334,23 +367,27 @@ class ChromeProfileBridge {
|
|
|
334
367
|
command
|
|
335
368
|
? { type: "command", command, expectedExtensionVersion: currentVersion }
|
|
336
369
|
: { type: "none", expectedExtensionVersion: currentVersion },
|
|
337
|
-
{ "x-pi-chrome-version": currentVersion },
|
|
370
|
+
{ ...corsHeaders, "x-pi-chrome-version": currentVersion },
|
|
338
371
|
);
|
|
339
372
|
return;
|
|
340
373
|
}
|
|
341
374
|
if (request.method === "POST" && url.pathname === "/result") {
|
|
375
|
+
if (!isBrowserOriginAllowed(request)) {
|
|
376
|
+
sendJson(response, 403, { ok: false, error: "browser origin not allowed" });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
342
379
|
this.lastSeenAt = Date.now();
|
|
343
380
|
const result = JSON.parse(await readRequestBody(request)) as BridgeResult;
|
|
344
381
|
const pending = this.pending.get(result.id);
|
|
345
382
|
if (!pending) {
|
|
346
|
-
sendJson(response, 404, { ok: false, error: "unknown command id" });
|
|
383
|
+
sendJson(response, 404, { ok: false, error: "unknown command id" }, corsHeaders);
|
|
347
384
|
return;
|
|
348
385
|
}
|
|
349
386
|
clearTimeout(pending.timer);
|
|
350
387
|
this.pending.delete(result.id);
|
|
351
388
|
if (result.ok) pending.resolve(result.result);
|
|
352
389
|
else pending.reject(new Error(result.error ?? "Chrome extension command failed"));
|
|
353
|
-
sendJson(response, 200, { ok: true });
|
|
390
|
+
sendJson(response, 200, { ok: true }, corsHeaders);
|
|
354
391
|
return;
|
|
355
392
|
}
|
|
356
393
|
sendJson(response, 404, { error: "not found" });
|
|
@@ -399,6 +436,46 @@ export default function (pi: ExtensionAPI): void {
|
|
|
399
436
|
|
|
400
437
|
const bridge = new ChromeProfileBridge(DEFAULT_HOST, DEFAULT_PORT);
|
|
401
438
|
let backgroundDefault = false;
|
|
439
|
+
let chromeAuthorizedUntil: number | "session" | undefined;
|
|
440
|
+
let chromeAuthorizedCalls: number | undefined;
|
|
441
|
+
|
|
442
|
+
const authSummary = (): string => {
|
|
443
|
+
if (chromeAuthorizedUntil === "session") return "authorized for this session";
|
|
444
|
+
if (typeof chromeAuthorizedUntil === "number") {
|
|
445
|
+
const remainingMs = chromeAuthorizedUntil - Date.now();
|
|
446
|
+
if (remainingMs > 0) {
|
|
447
|
+
const minutes = Math.ceil(remainingMs / 60_000);
|
|
448
|
+
return `authorized for ${chromeAuthorizedCalls === 1 ? "one Chrome command" : `~${minutes}m`}`;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return "locked";
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const chromeControlAuthorized = (): boolean => {
|
|
455
|
+
if (chromeAuthorizedUntil === "session") return true;
|
|
456
|
+
if (typeof chromeAuthorizedUntil === "number" && chromeAuthorizedUntil > Date.now()) return true;
|
|
457
|
+
chromeAuthorizedUntil = undefined;
|
|
458
|
+
chromeAuthorizedCalls = undefined;
|
|
459
|
+
return false;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const requireChromeControlAuthorized = (): void => {
|
|
463
|
+
if (!chromeControlAuthorized()) {
|
|
464
|
+
throw new Error("Chrome control locked. Ask the user to run /chrome authorize before using chrome_* tools.");
|
|
465
|
+
}
|
|
466
|
+
if (chromeAuthorizedCalls !== undefined) {
|
|
467
|
+
chromeAuthorizedCalls -= 1;
|
|
468
|
+
if (chromeAuthorizedCalls <= 0) {
|
|
469
|
+
chromeAuthorizedUntil = undefined;
|
|
470
|
+
chromeAuthorizedCalls = undefined;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const authorizedBridgeSend = (action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<unknown> => {
|
|
476
|
+
requireChromeControlAuthorized();
|
|
477
|
+
return bridge.send(action, params, timeoutMs);
|
|
478
|
+
};
|
|
402
479
|
|
|
403
480
|
// Translate the public `background` parameter (default false = visible/foreground) into the
|
|
404
481
|
// service worker's wire-level `foreground` flag, accepting legacy `foreground` as a fallback.
|
|
@@ -420,8 +497,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
420
497
|
ctx.ui.setStatus("chrome", `Chrome bridge :${DEFAULT_PORT}`);
|
|
421
498
|
ctx.ui.notify(
|
|
422
499
|
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.`,
|
|
500
|
+
? `pi-chrome connected (sharing the Chrome connection an earlier pi session opened). Run /chrome authorize before using chrome_* tools.`
|
|
501
|
+
: `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
502
|
"info",
|
|
426
503
|
);
|
|
427
504
|
});
|
|
@@ -442,13 +519,14 @@ Capability model (important):
|
|
|
442
519
|
- 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
520
|
|
|
444
521
|
Usage rules:
|
|
445
|
-
1.
|
|
446
|
-
2. \`
|
|
447
|
-
3.
|
|
448
|
-
4. \`
|
|
449
|
-
5.
|
|
450
|
-
6.
|
|
451
|
-
7.
|
|
522
|
+
1. If a chrome_* tool says Chrome control is locked, ask the user to run \`/chrome authorize\` before retrying.
|
|
523
|
+
2. \`chrome_snapshot\` before clicking/typing; pass \`uid\` over \`selector\`.
|
|
524
|
+
3. \`includeSnapshot=true\` on click/type/fill to verify in one round trip.
|
|
525
|
+
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.
|
|
526
|
+
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).
|
|
527
|
+
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.
|
|
528
|
+
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.
|
|
529
|
+
8. Run /chrome doctor when in doubt about connectivity or capabilities.
|
|
452
530
|
</chrome-profile-bridge>`;
|
|
453
531
|
return { systemPrompt: event.systemPrompt + primer };
|
|
454
532
|
});
|
|
@@ -517,9 +595,9 @@ Usage rules:
|
|
|
517
595
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
518
596
|
};
|
|
519
597
|
|
|
520
|
-
//
|
|
521
|
-
const
|
|
522
|
-
on: "pi-chrome
|
|
598
|
+
// Run-in-background (Chrome focus) handler. No args = toggle. Explicit on/off/status.
|
|
599
|
+
const BACKGROUND_DESC: Record<string, string> = {
|
|
600
|
+
on: "pi-chrome runs in the background; Chrome won't pop up or steal focus.",
|
|
523
601
|
off: "Chrome pops to the front and switches tabs so you can watch what pi-chrome is doing.",
|
|
524
602
|
};
|
|
525
603
|
|
|
@@ -528,7 +606,7 @@ Usage rules:
|
|
|
528
606
|
const currentLabel = backgroundDefault ? "on" : "off";
|
|
529
607
|
|
|
530
608
|
if (arg === "status") {
|
|
531
|
-
ctx.ui.notify(`
|
|
609
|
+
ctx.ui.notify(`Run in background is ${currentLabel}. ${BACKGROUND_DESC[currentLabel]}`, "info");
|
|
532
610
|
return;
|
|
533
611
|
}
|
|
534
612
|
|
|
@@ -536,12 +614,48 @@ Usage rules:
|
|
|
536
614
|
else if (arg === "off" || arg === "false" || arg === "0") backgroundDefault = false;
|
|
537
615
|
else if (arg === "toggle" || arg === "") backgroundDefault = !backgroundDefault;
|
|
538
616
|
else {
|
|
539
|
-
ctx.ui.notify(`Unknown
|
|
617
|
+
ctx.ui.notify(`Unknown background setting '${arg}'. Pick one of: on | off | toggle | status.`, "warning");
|
|
540
618
|
return;
|
|
541
619
|
}
|
|
542
620
|
|
|
543
621
|
const nextLabel = backgroundDefault ? "on" : "off";
|
|
544
|
-
ctx.ui.notify(`
|
|
622
|
+
ctx.ui.notify(`Run in background → ${nextLabel}. ${BACKGROUND_DESC[nextLabel]}`, "info");
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const authorizeHandler = async (ctx: ExtensionContext, args: string) => {
|
|
626
|
+
const arg = (args || "").trim().toLowerCase() || "session";
|
|
627
|
+
if (arg === "status") {
|
|
628
|
+
ctx.ui.notify(`Chrome control auth: ${authSummary()}.`, "info");
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const options: Record<string, { label: string; until: number | "session"; calls?: number }> = {
|
|
632
|
+
once: { label: "one Chrome command", until: Date.now() + 10 * 60_000, calls: 1 },
|
|
633
|
+
"15m": { label: "15 minutes", until: Date.now() + 15 * 60_000 },
|
|
634
|
+
"1h": { label: "1 hour", until: Date.now() + 60 * 60_000 },
|
|
635
|
+
session: { label: "this Pi session", until: "session" },
|
|
636
|
+
};
|
|
637
|
+
const grant = options[arg];
|
|
638
|
+
if (!grant) {
|
|
639
|
+
ctx.ui.notify("Unknown authorize duration. Pick one of: once | 15m | 1h | session | status.", "warning");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const ok = await ctx.ui.confirm(
|
|
643
|
+
"Authorize pi-chrome control?",
|
|
644
|
+
`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.`,
|
|
645
|
+
);
|
|
646
|
+
if (!ok) {
|
|
647
|
+
ctx.ui.notify("Chrome control remains locked.", "info");
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
chromeAuthorizedUntil = grant.until;
|
|
651
|
+
chromeAuthorizedCalls = grant.calls;
|
|
652
|
+
ctx.ui.notify(`Chrome control authorized for ${grant.label}.`, "info");
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const revokeHandler = (ctx: ExtensionContext) => {
|
|
656
|
+
chromeAuthorizedUntil = undefined;
|
|
657
|
+
chromeAuthorizedCalls = undefined;
|
|
658
|
+
ctx.ui.notify("Chrome control locked. Run /chrome authorize to allow chrome_* tools again.", "info");
|
|
545
659
|
};
|
|
546
660
|
|
|
547
661
|
const onboardHandler = async (ctx: ExtensionContext) => {
|
|
@@ -579,7 +693,8 @@ Usage rules:
|
|
|
579
693
|
} catch {
|
|
580
694
|
parts.push(`✗ Chrome not responding`);
|
|
581
695
|
}
|
|
582
|
-
parts.push(`
|
|
696
|
+
parts.push(`auth: ${authSummary()}`);
|
|
697
|
+
parts.push(`background: ${backgroundDefault ? "on" : "off"}`);
|
|
583
698
|
return parts.join(" · ");
|
|
584
699
|
};
|
|
585
700
|
|
|
@@ -591,14 +706,14 @@ Usage rules:
|
|
|
591
706
|
// the last value also saves; Esc / 'q' closes. The description below changes with the
|
|
592
707
|
// current value so users always see what the active setting means.
|
|
593
708
|
const openSettingsDialog = async (ctx: ExtensionContext): Promise<void> => {
|
|
594
|
-
const
|
|
595
|
-
id: "
|
|
596
|
-
label: "
|
|
709
|
+
const backgroundItem: SettingItem = {
|
|
710
|
+
id: "background",
|
|
711
|
+
label: "Run in background",
|
|
597
712
|
currentValue: backgroundDefault ? "on" : "off",
|
|
598
713
|
values: ["on", "off"],
|
|
599
|
-
description:
|
|
714
|
+
description: BACKGROUND_DESC[backgroundDefault ? "on" : "off"] ?? "",
|
|
600
715
|
};
|
|
601
|
-
const items: SettingItem[] = [
|
|
716
|
+
const items: SettingItem[] = [backgroundItem];
|
|
602
717
|
|
|
603
718
|
await ctx.ui.custom<void>((_tui, theme, _kb, done) => {
|
|
604
719
|
const container = new Container();
|
|
@@ -611,10 +726,10 @@ Usage rules:
|
|
|
611
726
|
Math.min(items.length + 2, 8),
|
|
612
727
|
getSettingsListTheme(),
|
|
613
728
|
(id, newValue) => {
|
|
614
|
-
if (id === "
|
|
729
|
+
if (id === "background") {
|
|
615
730
|
backgroundDefault = newValue === "on";
|
|
616
|
-
|
|
617
|
-
|
|
731
|
+
backgroundItem.currentValue = newValue;
|
|
732
|
+
backgroundItem.description = BACKGROUND_DESC[newValue] ?? "";
|
|
618
733
|
list.invalidate();
|
|
619
734
|
}
|
|
620
735
|
},
|
|
@@ -632,7 +747,7 @@ Usage rules:
|
|
|
632
747
|
|
|
633
748
|
pi.registerCommand("chrome", {
|
|
634
749
|
description:
|
|
635
|
-
"All pi-chrome controls in one place.\n /chrome status — one-line snapshot of connection
|
|
750
|
+
"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
751
|
getArgumentCompletions: (prefix) => {
|
|
637
752
|
const raw = prefix;
|
|
638
753
|
const trimmedRight = raw.replace(/\s+$/, "");
|
|
@@ -649,17 +764,27 @@ Usage rules:
|
|
|
649
764
|
let candidates: Item[] = [];
|
|
650
765
|
if (path.length === 0) {
|
|
651
766
|
candidates = [
|
|
652
|
-
{ fullValue: "
|
|
767
|
+
{ fullValue: "authorize", label: "authorize", description: "Allow this Pi session to use chrome_* tools." },
|
|
768
|
+
{ fullValue: "revoke", label: "revoke", description: "Lock Chrome control for this Pi session." },
|
|
769
|
+
{ fullValue: "status", label: "status", description: "One-line summary: connection, auth, and background setting." },
|
|
653
770
|
{ fullValue: "doctor", label: "doctor", description: "Full health check. Tells you if Chrome is connected and what's wrong if it isn't." },
|
|
654
771
|
{ fullValue: "onboard", label: "onboard", description: "Install the Chrome companion extension (first-time setup)." },
|
|
655
|
-
{ fullValue: "
|
|
772
|
+
{ fullValue: "background", label: "background", description: "Run pi-chrome in the background without focusing Chrome?" },
|
|
773
|
+
];
|
|
774
|
+
} else if (path[0] === "authorize" && path.length === 1) {
|
|
775
|
+
candidates = [
|
|
776
|
+
{ fullValue: "authorize once", label: "once", description: "Authorize one Chrome command." },
|
|
777
|
+
{ fullValue: "authorize 15m", label: "15m", description: "Authorize Chrome control for 15 minutes." },
|
|
778
|
+
{ fullValue: "authorize 1h", label: "1h", description: "Authorize Chrome control for 1 hour." },
|
|
779
|
+
{ fullValue: "authorize session", label: "session", description: "Authorize Chrome control until this Pi session exits." },
|
|
780
|
+
{ fullValue: "authorize status", label: "status", description: "Show Chrome control authorization state." },
|
|
656
781
|
];
|
|
657
|
-
} else if (path[0] === "
|
|
782
|
+
} else if (path[0] === "background" && path.length === 1) {
|
|
658
783
|
candidates = [
|
|
659
|
-
{ fullValue: "
|
|
660
|
-
{ fullValue: "
|
|
661
|
-
{ fullValue: "
|
|
662
|
-
{ fullValue: "
|
|
784
|
+
{ fullValue: "background on", label: "on", description: "Run in background. Chrome stays in the background. Your editor keeps focus." },
|
|
785
|
+
{ fullValue: "background off", label: "off", description: "Bring Chrome to the front so you can watch (default)." },
|
|
786
|
+
{ fullValue: "background toggle", label: "toggle", description: "Flip whichever way it's currently set." },
|
|
787
|
+
{ fullValue: "background status", label: "status", description: "Show the current setting." },
|
|
663
788
|
];
|
|
664
789
|
}
|
|
665
790
|
if (candidates.length === 0) return null;
|
|
@@ -676,21 +801,22 @@ Usage rules:
|
|
|
676
801
|
const [head, ...rest] = tokens;
|
|
677
802
|
const subArgs = rest.join(" ");
|
|
678
803
|
switch (head) {
|
|
804
|
+
case "authorize": return authorizeHandler(ctx, subArgs);
|
|
805
|
+
case "revoke": return revokeHandler(ctx);
|
|
679
806
|
case "status": return statusHandler(ctx);
|
|
680
807
|
case "doctor": return doctorHandler(ctx);
|
|
681
808
|
case "onboard": return onboardHandler(ctx);
|
|
682
|
-
case "
|
|
683
|
-
case "background": // legacy alias
|
|
809
|
+
case "background":
|
|
684
810
|
return backgroundHandler(ctx, subArgs);
|
|
685
811
|
case "settings": {
|
|
686
812
|
// Legacy nested form: /chrome settings background ...
|
|
687
813
|
const [setting, ...settingArgs] = rest;
|
|
688
814
|
if (setting === "background") return backgroundHandler(ctx, settingArgs.join(" "));
|
|
689
|
-
ctx.ui.notify(`'/chrome settings' was removed. Use /chrome
|
|
815
|
+
ctx.ui.notify(`'/chrome settings' was removed. Use /chrome background directly.`, "warning");
|
|
690
816
|
return;
|
|
691
817
|
}
|
|
692
818
|
default:
|
|
693
|
-
ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome status | doctor | onboard |
|
|
819
|
+
ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome authorize | revoke | status | doctor | onboard | background.`, "warning");
|
|
694
820
|
}
|
|
695
821
|
},
|
|
696
822
|
});
|
|
@@ -710,7 +836,7 @@ Usage rules:
|
|
|
710
836
|
}),
|
|
711
837
|
async execute(_id, params, _signal, _onUpdate, ctx): Promise<ToolTextResult> {
|
|
712
838
|
if (params.url && bridge.connected) {
|
|
713
|
-
const result = await
|
|
839
|
+
const result = await authorizedBridgeSend("tab.new", { url: params.url }, DEFAULT_TIMEOUT_MS);
|
|
714
840
|
return { content: [{ type: "text", text: `Chrome bridge connected; opened ${params.url}` }], details: { status: bridge.status(), result } };
|
|
715
841
|
}
|
|
716
842
|
return {
|
|
@@ -745,7 +871,7 @@ Usage rules:
|
|
|
745
871
|
port: Type.Optional(Type.Number()),
|
|
746
872
|
}),
|
|
747
873
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
748
|
-
const result = await
|
|
874
|
+
const result = await authorizedBridgeSend(`tab.${params.action}`, params, DEFAULT_TIMEOUT_MS);
|
|
749
875
|
if (params.action === "list") {
|
|
750
876
|
const tabs = result as Array<{ id: number; title: string; url: string; active: boolean; windowId: number }>;
|
|
751
877
|
const text = tabs.map((tab) => `${tab.id}\t${tab.active ? "*" : " "}\t${tab.title || "(untitled)"}\t${tab.url}`).join("\n") || "No tabs.";
|
|
@@ -776,7 +902,7 @@ Usage rules:
|
|
|
776
902
|
port: Type.Optional(Type.Number()),
|
|
777
903
|
}),
|
|
778
904
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
779
|
-
const snapshot = await
|
|
905
|
+
const snapshot = await authorizedBridgeSend(
|
|
780
906
|
"page.snapshot",
|
|
781
907
|
withBackground({ ...params, maxElements: params.maxElements ?? MAX_ELEMENTS }),
|
|
782
908
|
DEFAULT_TIMEOUT_MS,
|
|
@@ -806,7 +932,7 @@ Usage rules:
|
|
|
806
932
|
port: Type.Optional(Type.Number()),
|
|
807
933
|
}),
|
|
808
934
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
809
|
-
const result = await
|
|
935
|
+
const result = await authorizedBridgeSend("page.navigate", withBackground(params), (params.timeoutMs ?? 15_000) + 2_000);
|
|
810
936
|
return { content: [{ type: "text", text: `Navigated to ${params.url}${params.initScript ? " (with initScript)" : ""}` }], details: { result: result as Json } };
|
|
811
937
|
},
|
|
812
938
|
});
|
|
@@ -830,7 +956,7 @@ Usage rules:
|
|
|
830
956
|
port: Type.Optional(Type.Number()),
|
|
831
957
|
}),
|
|
832
958
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
833
|
-
const value = await
|
|
959
|
+
const value = await authorizedBridgeSend("page.evaluate", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
834
960
|
const text = value === undefined
|
|
835
961
|
? "undefined"
|
|
836
962
|
: typeof value === "string"
|
|
@@ -863,7 +989,7 @@ Usage rules:
|
|
|
863
989
|
port: Type.Optional(Type.Number()),
|
|
864
990
|
}),
|
|
865
991
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
866
|
-
const raw = await
|
|
992
|
+
const raw = await authorizedBridgeSend("page.click", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
867
993
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
868
994
|
const summary = summarizeActionResult(result);
|
|
869
995
|
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
|
@@ -895,7 +1021,7 @@ Usage rules:
|
|
|
895
1021
|
port: Type.Optional(Type.Number()),
|
|
896
1022
|
}),
|
|
897
1023
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
898
|
-
const raw = await
|
|
1024
|
+
const raw = await authorizedBridgeSend("page.type", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
899
1025
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
900
1026
|
const summary = summarizeActionResult(result);
|
|
901
1027
|
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
|
@@ -927,7 +1053,7 @@ Usage rules:
|
|
|
927
1053
|
port: Type.Optional(Type.Number()),
|
|
928
1054
|
}),
|
|
929
1055
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
930
|
-
const raw = await
|
|
1056
|
+
const raw = await authorizedBridgeSend("page.fill", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
931
1057
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
932
1058
|
const summary = summarizeActionResult(result);
|
|
933
1059
|
const into = params.uid || params.selector ? ` into ${params.uid ?? params.selector}` : "";
|
|
@@ -962,7 +1088,7 @@ Usage rules:
|
|
|
962
1088
|
port: Type.Optional(Type.Number()),
|
|
963
1089
|
}),
|
|
964
1090
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
965
|
-
const raw = await
|
|
1091
|
+
const raw = await authorizedBridgeSend("page.key", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
966
1092
|
const result = (params.includeSnapshot ? (raw as { result: unknown }).result : raw) as Json;
|
|
967
1093
|
const summary = summarizeActionResult(result);
|
|
968
1094
|
const base = `Pressed ${params.key}.`;
|
|
@@ -987,7 +1113,7 @@ Usage rules:
|
|
|
987
1113
|
port: Type.Optional(Type.Number()),
|
|
988
1114
|
}),
|
|
989
1115
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
990
|
-
const result = await
|
|
1116
|
+
const result = await authorizedBridgeSend("page.waitFor", params, (params.timeoutMs ?? 10_000) + 2_000);
|
|
991
1117
|
return { content: [{ type: "text", text: `Observed ${params.kind}: ${params.value}` }], details: { result: result as Json } };
|
|
992
1118
|
},
|
|
993
1119
|
});
|
|
@@ -1008,7 +1134,7 @@ Usage rules:
|
|
|
1008
1134
|
port: Type.Optional(Type.Number()),
|
|
1009
1135
|
}),
|
|
1010
1136
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1011
|
-
const result = await
|
|
1137
|
+
const result = await authorizedBridgeSend("page.console.list", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1012
1138
|
return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } };
|
|
1013
1139
|
},
|
|
1014
1140
|
});
|
|
@@ -1030,7 +1156,7 @@ Usage rules:
|
|
|
1030
1156
|
port: Type.Optional(Type.Number()),
|
|
1031
1157
|
}),
|
|
1032
1158
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1033
|
-
const result = await
|
|
1159
|
+
const result = await authorizedBridgeSend("page.network.list", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1034
1160
|
return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } };
|
|
1035
1161
|
},
|
|
1036
1162
|
});
|
|
@@ -1050,7 +1176,7 @@ Usage rules:
|
|
|
1050
1176
|
port: Type.Optional(Type.Number()),
|
|
1051
1177
|
}),
|
|
1052
1178
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1053
|
-
const result = await
|
|
1179
|
+
const result = await authorizedBridgeSend("page.network.get", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1054
1180
|
return { content: [{ type: "text", text: truncateText(safeJson(result)) }], details: { result: result as Json } };
|
|
1055
1181
|
},
|
|
1056
1182
|
});
|
|
@@ -1080,7 +1206,7 @@ Usage rules:
|
|
|
1080
1206
|
const cwd = workspaceCwd(ctx);
|
|
1081
1207
|
const defaultPath = join(cwd, ".pi", "chrome-screenshots", `${new Date().toISOString().replace(/[:.]/g, "-")}.${format}`);
|
|
1082
1208
|
const outputPath = params.path ? resolve(cwd, params.path) : defaultPath;
|
|
1083
|
-
const result = (await
|
|
1209
|
+
const result = (await authorizedBridgeSend("page.screenshot", withBackground(params), params.fullPage ? 120_000 : DEFAULT_TIMEOUT_MS)) as {
|
|
1084
1210
|
dataUrl?: string;
|
|
1085
1211
|
tab?: unknown;
|
|
1086
1212
|
fullPage?: boolean;
|
|
@@ -1130,7 +1256,7 @@ Usage rules:
|
|
|
1130
1256
|
background: Type.Optional(Type.Boolean()),
|
|
1131
1257
|
}),
|
|
1132
1258
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1133
|
-
const result = await
|
|
1259
|
+
const result = await authorizedBridgeSend("page.hover", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1134
1260
|
return { content: [{ type: "text", text: `Hovered ${params.uid ?? params.selector ?? `${params.x},${params.y}`}` }], details: { result: result as Json } };
|
|
1135
1261
|
},
|
|
1136
1262
|
});
|
|
@@ -1156,7 +1282,7 @@ Usage rules:
|
|
|
1156
1282
|
background: Type.Optional(Type.Boolean()),
|
|
1157
1283
|
}),
|
|
1158
1284
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1159
|
-
const result = await
|
|
1285
|
+
const result = await authorizedBridgeSend("page.drag", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1160
1286
|
return { content: [{ type: "text", text: `Dragged from ${params.fromUid ?? params.fromSelector} to ${params.toUid ?? params.toSelector}` }], details: { result: result as Json } };
|
|
1161
1287
|
},
|
|
1162
1288
|
});
|
|
@@ -1178,7 +1304,7 @@ Usage rules:
|
|
|
1178
1304
|
background: Type.Optional(Type.Boolean()),
|
|
1179
1305
|
}),
|
|
1180
1306
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1181
|
-
const result = await
|
|
1307
|
+
const result = await authorizedBridgeSend("page.tap", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1182
1308
|
const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
|
|
1183
1309
|
return { content: [{ type: "text", text: `Tapped ${target} (touch)` }], details: { result: result as Json } };
|
|
1184
1310
|
},
|
|
@@ -1201,7 +1327,7 @@ Usage rules:
|
|
|
1201
1327
|
background: Type.Optional(Type.Boolean()),
|
|
1202
1328
|
}),
|
|
1203
1329
|
async execute(_id, params): Promise<ToolTextResult> {
|
|
1204
|
-
const result = await
|
|
1330
|
+
const result = await authorizedBridgeSend("page.scroll", withBackground(params), DEFAULT_TIMEOUT_MS);
|
|
1205
1331
|
return { content: [{ type: "text", text: `Scrolled dy=${params.deltaY ?? 0} dx=${params.deltaX ?? 0}` }], details: { result: result as Json } };
|
|
1206
1332
|
},
|
|
1207
1333
|
});
|
|
@@ -1223,7 +1349,7 @@ Usage rules:
|
|
|
1223
1349
|
async execute(_id, params, _signal, _onUpdate, ctx): Promise<ToolTextResult> {
|
|
1224
1350
|
const cwd = workspaceCwd(ctx);
|
|
1225
1351
|
const paths = params.paths.map((p) => resolve(cwd, p));
|
|
1226
|
-
const result = await
|
|
1352
|
+
const result = await authorizedBridgeSend("page.upload", withBackground({ ...params, paths }), DEFAULT_TIMEOUT_MS);
|
|
1227
1353
|
return { content: [{ type: "text", text: `Uploaded ${paths.length} file(s) to ${params.uid ?? params.selector}` }], details: { result: result as Json } };
|
|
1228
1354
|
},
|
|
1229
1355
|
});
|