pi-chrome 0.11.5 → 0.12.1
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
|
@@ -11,9 +11,9 @@ Multiple Pi sessions can use Chrome at the same time. The first Pi session start
|
|
|
11
11
|
## Why try it?
|
|
12
12
|
|
|
13
13
|
- **Uses your existing Chrome profile** — works with the Chrome windows/tabs you are already using, including logged-in GitHub, admin dashboards, local apps, and internal tools.
|
|
14
|
-
- **Watch your authenticated Chrome work** — by default, `chrome_*` tool calls focus Chrome and activate the target tab so you can see the agent inspect, navigate, click, and type in real time. Switch to silent/background mode for the whole session with `/chrome
|
|
14
|
+
- **Watch your authenticated Chrome work** — by default, `chrome_*` tool calls focus Chrome and activate the target tab so you can see the agent inspect, navigate, click, and type in real time. Switch to silent/background mode for the whole session with `/chrome settings background`, or pass `background: true` on a single tool call when you want quiet.
|
|
15
15
|
- **Full browser automation toolkit for Pi** — list/create/activate/close tabs, snapshot pages with usable CSS selectors, navigate, evaluate JavaScript, click, type, press keys, wait for page state, and capture screenshots.
|
|
16
|
-
- **Built-in setup and agent guidance** — `/chrome
|
|
16
|
+
- **Built-in setup and agent guidance** — `/chrome onboard` walks users through installing the companion extension, `/chrome doctor` checks connectivity and version drift, screenshots save to disk, and the prompt primer tells agents to inspect with `chrome_snapshot` before acting and avoid destructive actions unless explicitly requested.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -29,14 +29,14 @@ pi install ./pi-chrome
|
|
|
29
29
|
|
|
30
30
|
### Why an unpacked Chrome extension?
|
|
31
31
|
|
|
32
|
-
`pi-chrome` cannot ship through the Chrome Web Store: a Web Store extension is not allowed to talk to a local bridge controlled by another tool. Instead it ships as a small, MIT-licensed unpacked extension in `extensions/chrome-profile-bridge/browser-extension/` — read the source before loading. `/chrome
|
|
32
|
+
`pi-chrome` cannot ship through the Chrome Web Store: a Web Store extension is not allowed to talk to a local bridge controlled by another tool. Instead it ships as a small, MIT-licensed unpacked extension in `extensions/chrome-profile-bridge/browser-extension/` — read the source before loading. `/chrome doctor` reports the loaded extension version and warns when it drifts from the installed `pi-chrome`.
|
|
33
33
|
|
|
34
34
|
## First-time setup
|
|
35
35
|
|
|
36
36
|
In Pi, run:
|
|
37
37
|
|
|
38
38
|
```text
|
|
39
|
-
/chrome
|
|
39
|
+
/chrome onboard
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Pi first shows setup instructions and waits for confirmation. Press Enter to continue. On macOS it will:
|
|
@@ -53,7 +53,7 @@ Then in Chrome:
|
|
|
53
53
|
4. Return to Pi and run:
|
|
54
54
|
|
|
55
55
|
```text
|
|
56
|
-
/chrome
|
|
56
|
+
/chrome doctor
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
Expected output:
|
|
@@ -72,13 +72,13 @@ pi-chrome can drive Chrome two ways:
|
|
|
72
72
|
- **Quiet clicks** — fast and unobtrusive. They work on most sites, but some pages (sign-in flows, copy-to-clipboard buttons, file pickers, autoplay videos, fullscreen, paywalls) ignore them because they don't look like a real human action.
|
|
73
73
|
- **Real-looking clicks** — indistinguishable from a person clicking. They unlock the cases above, but Chrome shows a *"Pi Chrome Connector started debugging this browser"* banner at the top of every tab pi-chrome touches while it's working.
|
|
74
74
|
|
|
75
|
-
Pick a mode with `/chrome
|
|
75
|
+
Pick a mode with `/chrome settings trusted`:
|
|
76
76
|
|
|
77
77
|
```text
|
|
78
|
-
/chrome
|
|
79
|
-
/chrome
|
|
80
|
-
/chrome
|
|
81
|
-
/chrome
|
|
78
|
+
/chrome settings trusted auto # default; quiet by default, real-looking only when needed
|
|
79
|
+
/chrome settings trusted off # always quiet, no banner ever
|
|
80
|
+
/chrome settings trusted on # always real-looking, banner stays up the whole session
|
|
81
|
+
/chrome settings trusted status # show the current mode
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
For a one-off call, pass `trusted: true` (or `false`) on `chrome_click`, `chrome_type`, `chrome_fill`, `chrome_key`, `chrome_hover`, `chrome_drag`, or `chrome_scroll`. The per-call value wins over the global mode.
|
|
@@ -92,9 +92,9 @@ By default, `chrome_*` tools focus Chrome and activate the target tab so you can
|
|
|
92
92
|
When you want quiet (planner / audit / worker sessions running alongside your editor), turn background mode on for the whole Pi session:
|
|
93
93
|
|
|
94
94
|
```text
|
|
95
|
-
/chrome
|
|
96
|
-
/chrome
|
|
97
|
-
/chrome
|
|
95
|
+
/chrome settings background # toggle
|
|
96
|
+
/chrome settings background on # explicit
|
|
97
|
+
/chrome settings background off # explicit
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
For a single tool call, the agent can pass `background: true` directly. The per-call value always wins over the session toggle.
|
|
@@ -137,9 +137,9 @@ Screenshots save under `.pi/chrome-screenshots/` by default, which composes nice
|
|
|
137
137
|
|
|
138
138
|
## Diagnostics
|
|
139
139
|
|
|
140
|
-
- `/chrome
|
|
140
|
+
- `/chrome doctor` — single command that checks connectivity and reports the loaded Chrome extension ID + version, plus a one-line fix for common setup failures (extension not loaded, bridge owner stale after `pi update`, version mismatch between pi-chrome and the loaded Chrome extension).
|
|
141
141
|
|
|
142
|
-
If the Chrome extension you have loaded is older than `pi-chrome` on disk, `/chrome
|
|
142
|
+
If the Chrome extension you have loaded is older than `pi-chrome` on disk, `/chrome doctor` will tell you to reload it from `chrome://extensions`.
|
|
143
143
|
|
|
144
144
|
## Compose with
|
|
145
145
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Pi Chrome Connector",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.1",
|
|
5
5
|
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
|
|
6
6
|
"permissions": ["tabs", "scripting", "storage", "activeTab", "alarms", "webNavigation", "debugger"],
|
|
7
7
|
"host_permissions": ["<all_urls>", "http://127.0.0.1:17318/*"],
|
|
@@ -67,7 +67,22 @@ async function attachDebugger(tabId) {
|
|
|
67
67
|
entry.detachAt = Date.now() + TRUSTED_IDLE_DETACH_MS;
|
|
68
68
|
return entry;
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
try {
|
|
71
|
+
await chrome.debugger.attach({ tabId }, CDP_VERSION);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Chrome occasionally rejects attach with "Cannot access a chrome-extension:// URL of
|
|
74
|
+
// different extension" right after a navigation, even when the target tab's URL is a
|
|
75
|
+
// normal page. Wait a tick, verify the tab is on a non-privileged URL, and retry once.
|
|
76
|
+
const msg = String(error?.message || error);
|
|
77
|
+
const transient = /Cannot access a chrome-extension|Cannot access contents of|No tab with id|Debugger is not attached|Another debugger|Target closed/i.test(msg);
|
|
78
|
+
if (!transient) throw error;
|
|
79
|
+
await sleep(180);
|
|
80
|
+
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
81
|
+
if (!tab || (tab.url || "").startsWith("chrome://") || (tab.url || "").startsWith("chrome-extension://")) {
|
|
82
|
+
throw new Error(`Chrome can't attach the debugger to this tab (${tab?.url ?? "unknown"}). Open a normal http(s) tab and try again.`);
|
|
83
|
+
}
|
|
84
|
+
await chrome.debugger.attach({ tabId }, CDP_VERSION);
|
|
85
|
+
}
|
|
71
86
|
// Seed pointer in a plausible "just left the address bar" location.
|
|
72
87
|
const entry = { detachAt: Date.now() + TRUSTED_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 } };
|
|
73
88
|
attachedTabs.set(tabId, entry);
|
|
@@ -256,9 +271,28 @@ async function trustedClick(params) {
|
|
|
256
271
|
const resolved = await resolveTargetInTab(tab.id, params);
|
|
257
272
|
const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
|
|
258
273
|
await cdpMoveTo(tab.id, point.x, point.y);
|
|
259
|
-
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse" });
|
|
274
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse", force: 0.5 });
|
|
260
275
|
await sleep(rng(45, 140));
|
|
261
276
|
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" });
|
|
277
|
+
// Reset :focus-visible if the click landed on a focusable element. CDP-driven pointer
|
|
278
|
+
// focus can leave :focus-visible=true in Chromium, which trips heuristics that expect
|
|
279
|
+
// pointer focus to suppress the focus ring (synthetic clicks naturally land on false).
|
|
280
|
+
if (params.selector || params.uid) {
|
|
281
|
+
await chrome.scripting.executeScript({
|
|
282
|
+
target: { tabId: tab.id, frameIds: [0] },
|
|
283
|
+
world: "MAIN",
|
|
284
|
+
func: (sel, uid) => {
|
|
285
|
+
const state = window.__PI_CHROME_STATE__;
|
|
286
|
+
let el = null;
|
|
287
|
+
if (uid && state && state.elements && state.elements[uid]) el = state.elements[uid];
|
|
288
|
+
else if (sel) el = document.querySelector(sel);
|
|
289
|
+
if (el && typeof el.focus === "function" && el === document.activeElement) {
|
|
290
|
+
try { el.blur(); el.focus({ preventScroll: true, focusVisible: false }); } catch {}
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
args: [params.selector ?? null, params.uid ?? null],
|
|
294
|
+
}).catch(() => undefined);
|
|
295
|
+
}
|
|
262
296
|
return { trusted: true, x: point.x, y: point.y, tag: resolved.tag };
|
|
263
297
|
}
|
|
264
298
|
|
|
@@ -320,7 +354,7 @@ async function trustedType(params) {
|
|
|
320
354
|
const resolved = await resolveTargetInTab(tab.id, params);
|
|
321
355
|
const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
|
|
322
356
|
await cdpMoveTo(tab.id, point.x, point.y);
|
|
323
|
-
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse" });
|
|
357
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse", force: 0.5 });
|
|
324
358
|
await sleep(rng(45, 110));
|
|
325
359
|
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" });
|
|
326
360
|
await sleep(rng(50, 120));
|
|
@@ -344,7 +378,7 @@ async function trustedFill(params) {
|
|
|
344
378
|
await cdpMoveTo(tab.id, point.x, point.y);
|
|
345
379
|
// Triple-click selects all in input fields.
|
|
346
380
|
for (let i = 1; i <= 3; i++) {
|
|
347
|
-
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: i, pointerType: "mouse" });
|
|
381
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: i, pointerType: "mouse", force: 0.5 });
|
|
348
382
|
await sleep(rng(20, 60));
|
|
349
383
|
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: i, pointerType: "mouse" });
|
|
350
384
|
await sleep(rng(20, 60));
|
|
@@ -367,21 +401,36 @@ async function trustedScroll(params) {
|
|
|
367
401
|
const x = resolved.rect ? resolved.rect.left + Math.min(resolved.rect.width, 800) / 2 : resolved.x;
|
|
368
402
|
const y = resolved.rect ? resolved.rect.top + Math.min(resolved.rect.height, 600) / 2 : resolved.y;
|
|
369
403
|
const totalY = params.deltaY || 0, totalX = params.deltaX || 0;
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
const MAX_STEP = 25;
|
|
404
|
+
// Profile mimics a trackpad flick: short ramp-up (~15% of events), then geometric decay
|
|
405
|
+
// with a ~12% drop per event. Gives momentum tail tests something to find, and the small
|
|
406
|
+
// tail deltas (a handful of <20px events) put IntersectionObserver thresholds in range.
|
|
374
407
|
const peak = Math.max(Math.abs(totalY), Math.abs(totalX));
|
|
375
|
-
//
|
|
376
|
-
const
|
|
377
|
-
const n = Math.max(6, Math.min(200, params.steps || Math.max(minN, 12)));
|
|
378
|
-
// Front-loaded but smooth weights: w[i] = 1 + 0.5 * (1 - i/(n-1)) so the first event has
|
|
379
|
-
// weight 1.5, the last has 1.0, average ~1.25; redistribution stays predictable.
|
|
408
|
+
// Aim peak event ~22px so cumulative wheel approach to target seeds low-ratio IO samples.
|
|
409
|
+
const PEAK_TARGET = 22;
|
|
380
410
|
const w = [];
|
|
381
|
-
for
|
|
382
|
-
|
|
383
|
-
|
|
411
|
+
// Build weights for an arbitrary n, then iterate to find an n where peak * (w_peak/sum) <= PEAK_TARGET.
|
|
412
|
+
function build(n) {
|
|
413
|
+
const arr = [];
|
|
414
|
+
const peakIdx = Math.max(1, Math.floor(n * 0.15));
|
|
415
|
+
for (let i = 0; i < n; i++) {
|
|
416
|
+
if (i <= peakIdx) arr.push(0.5 + 0.5 * (i / peakIdx)); // 0.5 → 1.0
|
|
417
|
+
else arr.push(Math.pow(0.88, i - peakIdx)); // ~12% drop per step
|
|
418
|
+
}
|
|
419
|
+
return arr;
|
|
420
|
+
}
|
|
421
|
+
let n = Math.max(12, params.steps || 24);
|
|
422
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
423
|
+
const arr = build(n);
|
|
424
|
+
const s = arr.reduce((a, b) => a + b, 0);
|
|
425
|
+
const peakStep = peak * (Math.max(...arr) / s);
|
|
426
|
+
if (peakStep <= PEAK_TARGET || n >= 240) {
|
|
427
|
+
w.length = 0;
|
|
428
|
+
w.push(...arr);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
n = Math.ceil(n * 1.4);
|
|
384
432
|
}
|
|
433
|
+
if (w.length === 0) w.push(...build(n));
|
|
385
434
|
const sumW = w.reduce((a, b) => a + b, 0);
|
|
386
435
|
for (let i = 0; i < n; i++) {
|
|
387
436
|
const dy = totalY * (w[i] / sumW), dx = totalX * (w[i] / sumW);
|
|
@@ -419,7 +468,7 @@ async function trustedDrag(params) {
|
|
|
419
468
|
const fp = from.rect ? pickInsideRect(from.rect) : { x: from.x, y: from.y };
|
|
420
469
|
const tp = to.rect ? pickInsideRect(to.rect) : { x: to.x, y: to.y };
|
|
421
470
|
await cdpMoveTo(tab.id, fp.x, fp.y);
|
|
422
|
-
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: fp.x, y: fp.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse" });
|
|
471
|
+
await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: fp.x, y: fp.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse", force: 0.5 });
|
|
423
472
|
await sleep(rng(60, 140));
|
|
424
473
|
const steps = params.steps || 20;
|
|
425
474
|
for (let i = 1; i <= steps; i++) {
|
|
@@ -233,7 +233,7 @@ class ChromeProfileBridge {
|
|
|
233
233
|
this.queue = this.queue.filter((queued) => queued.id !== id);
|
|
234
234
|
rejectCommand(
|
|
235
235
|
new Error(
|
|
236
|
-
`Timed out waiting for Chrome extension after ${timeoutMs}ms. Run /chrome
|
|
236
|
+
`Timed out waiting for Chrome extension after ${timeoutMs}ms. Run /chrome onboard, then load the bundled browser-extension folder in your normal Chrome profile.`,
|
|
237
237
|
),
|
|
238
238
|
);
|
|
239
239
|
}, timeoutMs);
|
|
@@ -416,7 +416,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
416
416
|
ctx.ui.notify(
|
|
417
417
|
status.mode === "client"
|
|
418
418
|
? `pi-chrome connected (sharing the Chrome connection an earlier pi session opened).`
|
|
419
|
-
: `pi-chrome is ready and waiting for the Chrome companion to connect. Run /chrome
|
|
419
|
+
: `pi-chrome is ready and waiting for the Chrome companion to connect. Run /chrome onboard to install it.`,
|
|
420
420
|
"info",
|
|
421
421
|
);
|
|
422
422
|
});
|
|
@@ -440,17 +440,15 @@ Usage rules:
|
|
|
440
440
|
2. \`includeSnapshot=true\` on click/type/fill to verify in one round trip.
|
|
441
441
|
3. 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.
|
|
442
442
|
4. \`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).
|
|
443
|
-
5. By default chrome_* tools focus Chrome so the user can watch; pass \`background=true\` or run /chrome
|
|
443
|
+
5. By default chrome_* tools focus Chrome so the user can watch; pass \`background=true\` or run /chrome settings background to silence the whole session.
|
|
444
444
|
6. If you hit an autoplay/clipboard/file-picker gate, tell the user; this bridge cannot satisfy it.
|
|
445
|
-
7. Run /chrome
|
|
445
|
+
7. Run /chrome doctor when in doubt about connectivity or capabilities.
|
|
446
446
|
</chrome-profile-bridge>`;
|
|
447
447
|
return { systemPrompt: event.systemPrompt + primer };
|
|
448
448
|
});
|
|
449
449
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
"Run a quick health check on pi-chrome. Shows whether Chrome is connected, whether the companion extension is up to date, which click mode is active, and how to fix anything that's wrong.",
|
|
453
|
-
handler: async (_args, ctx) => {
|
|
450
|
+
// Shared handlers, dispatched by the unified /chrome command below.
|
|
451
|
+
const doctorHandler = async (ctx: ExtensionContext) => {
|
|
454
452
|
ctx.ui.notify("Checking pi-chrome…", "info");
|
|
455
453
|
const lines: string[] = [`pi-chrome v${PI_CHROME_VERSION}`];
|
|
456
454
|
const status = bridge.status();
|
|
@@ -484,7 +482,7 @@ Usage rules:
|
|
|
484
482
|
if (message.includes("older pi-chrome without multi-session")) {
|
|
485
483
|
lines.push(" Fix: quit and restart the pi session that first opened the Chrome connection (it was on an older pi-chrome).");
|
|
486
484
|
} else {
|
|
487
|
-
lines.push(" Fix: run /chrome
|
|
485
|
+
lines.push(" Fix: run /chrome onboard to install the Chrome companion extension, then keep that Chrome window open.");
|
|
488
486
|
}
|
|
489
487
|
}
|
|
490
488
|
|
|
@@ -525,7 +523,7 @@ Usage rules:
|
|
|
525
523
|
? " Clicks/keys are quiet by default; if a site rejects a quiet click, pi-chrome retries it once with a real-looking click. The Chrome banner shows only when that retry happens."
|
|
526
524
|
: status.mode === "on"
|
|
527
525
|
? " Every click and keystroke uses a real-looking event. The Chrome banner stays up on every tab pi-chrome touches."
|
|
528
|
-
: " All clicks are quiet, no banner. Some sites (sign-ins, copy buttons, file pickers, paywalls) may silently ignore them. Switch to /chrome
|
|
526
|
+
: " All clicks are quiet, no banner. Some sites (sign-ins, copy buttons, file pickers, paywalls) may silently ignore them. Switch to /chrome settings trusted auto if a site isn’t responding.";
|
|
529
527
|
const label = status.mode === "auto" ? "auto (smart upgrade)" : status.mode === "on" ? "on (always real-looking)" : "off (always quiet)";
|
|
530
528
|
lines.push(`✓ Click mode: ${label}${banner}.${note}`);
|
|
531
529
|
} else {
|
|
@@ -536,25 +534,10 @@ Usage rules:
|
|
|
536
534
|
}
|
|
537
535
|
}
|
|
538
536
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
});
|
|
537
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
538
|
+
};
|
|
542
539
|
|
|
543
|
-
|
|
544
|
-
description:
|
|
545
|
-
"Choose how realistically pi-chrome should drive Chrome. Real-looking clicks/keys unlock things like copy-to-clipboard buttons, file pickers, and sign-in pages, but show a banner at the top of every Chrome window saying it's being driven by pi-chrome. Three modes:\n auto (default) — quiet by default; auto-upgrade when a site blocks the quiet click.\n off — always quiet, no banner; some sites won't accept these clicks.\n on — always real-looking, banner stays up the whole session.\n status — show the current mode.",
|
|
546
|
-
getArgumentCompletions: (prefix) => {
|
|
547
|
-
const items = [
|
|
548
|
-
{ value: "auto", label: "auto", description: "Default. Quiet clicks; upgrade to real ones only when a site rejects them." },
|
|
549
|
-
{ value: "off", label: "off", description: "Always quiet. No banner. Some sites won't accept the clicks." },
|
|
550
|
-
{ value: "on", label: "on", description: "Always real-looking clicks. Yellow banner stays up. Best for stubborn sites." },
|
|
551
|
-
{ value: "status", label: "status", description: "Show the current mode." },
|
|
552
|
-
];
|
|
553
|
-
const lowered = prefix.toLowerCase();
|
|
554
|
-
const matches = items.filter((item) => item.value.startsWith(lowered));
|
|
555
|
-
return matches.length > 0 ? matches : null;
|
|
556
|
-
},
|
|
557
|
-
handler: async (args, ctx) => {
|
|
540
|
+
const trustedHandler = async (ctx: ExtensionContext, args: string) => {
|
|
558
541
|
const rawArg = (args || "").trim().toLowerCase();
|
|
559
542
|
|
|
560
543
|
// Resolve current status once for both branches (interactive picker + direct args).
|
|
@@ -612,7 +595,7 @@ Usage rules:
|
|
|
612
595
|
}
|
|
613
596
|
|
|
614
597
|
if (!["on", "off", "auto"].includes(target)) {
|
|
615
|
-
ctx.ui.notify(`Unknown choice '${rawArg}'. Pick one of: auto | off | on | status, or
|
|
598
|
+
ctx.ui.notify(`Unknown choice '${rawArg}'. Pick one of: auto | off | on | status, or run /chrome settings trusted with no argument.`, "warning");
|
|
616
599
|
return;
|
|
617
600
|
}
|
|
618
601
|
|
|
@@ -646,58 +629,129 @@ Usage rules:
|
|
|
646
629
|
ctx.ui.notify("Auto. Clicks stay quiet by default; pi-chrome will only switch to real-looking clicks when a site rejects a quiet one. The Chrome banner will appear only when that retry happens.", "info");
|
|
647
630
|
}
|
|
648
631
|
} catch (error) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
632
|
+
ctx.ui.notify(`Couldn't switch mode: ${(error as Error).message}`, "warning");
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const backgroundHandler = async (ctx: ExtensionContext, args: string) => {
|
|
637
|
+
const arg = (args || "").trim().toLowerCase();
|
|
638
|
+
if (arg === "on" || arg === "true" || arg === "1") backgroundDefault = true;
|
|
639
|
+
else if (arg === "off" || arg === "false" || arg === "0") backgroundDefault = false;
|
|
640
|
+
else backgroundDefault = !backgroundDefault;
|
|
641
|
+
ctx.ui.notify(
|
|
642
|
+
backgroundDefault
|
|
643
|
+
? "Quiet mode on. pi-chrome will work in the background; Chrome won't steal focus."
|
|
644
|
+
: "Watch mode on. Chrome will pop to the front and switch tabs so you can see what pi-chrome is doing.",
|
|
645
|
+
"info",
|
|
646
|
+
);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const onboardHandler = async (ctx: ExtensionContext) => {
|
|
650
|
+
const extensionPath = browserExtensionPath();
|
|
651
|
+
const proceed = await ctx.ui.confirm(
|
|
652
|
+
"Install the pi-chrome Chrome extension?",
|
|
653
|
+
`This opens Chrome's extensions page and reveals the folder pi-chrome needs you to load.\n\nWhen the windows open, in Chrome:\n 1. Turn on 'Developer mode' (top-right toggle).\n 2. Click 'Load unpacked' and choose the folder that just opened in Finder, or paste this path:\n ${extensionPath}\n\nPress Enter to continue, or Esc to cancel.`,
|
|
654
|
+
);
|
|
655
|
+
if (!proceed) {
|
|
656
|
+
ctx.ui.notify("Cancelled. You can run /chrome onboard again whenever you're ready.", "info");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (process.platform === "darwin") {
|
|
660
|
+
await pi.exec("open", ["-a", "Google Chrome", "chrome://extensions"], { cwd: workspaceCwd(ctx), timeout: 5_000 }).catch(() => undefined);
|
|
661
|
+
await pi.exec("open", ["-R", extensionPath], { cwd: workspaceCwd(ctx), timeout: 5_000 }).catch(() => undefined);
|
|
662
|
+
await pi.exec("sh", ["-lc", `printf %s ${JSON.stringify(extensionPath)} | pbcopy`], { cwd: workspaceCwd(ctx), timeout: 5_000 }).catch(() => undefined);
|
|
663
|
+
}
|
|
664
|
+
ctx.ui.notify(
|
|
665
|
+
"Chrome and Finder should be open. The extension folder path is on your clipboard. After you click 'Load unpacked' and pick it, run /chrome doctor to confirm everything is connected.",
|
|
666
|
+
"info",
|
|
667
|
+
);
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const settingsHandler = async (ctx: ExtensionContext, rest: string[]) => {
|
|
671
|
+
if (rest.length === 0) {
|
|
672
|
+
const picked = await ctx.ui.select(
|
|
673
|
+
"pi-chrome settings — what would you like to change?",
|
|
674
|
+
[
|
|
675
|
+
"background — should Chrome pop to the front when pi-chrome acts, or work silently?",
|
|
676
|
+
"trusted — how realistic should pi-chrome's clicks and keystrokes be?",
|
|
677
|
+
],
|
|
678
|
+
);
|
|
679
|
+
if (!picked) return;
|
|
680
|
+
if (picked.startsWith("background")) return backgroundHandler(ctx, "");
|
|
681
|
+
if (picked.startsWith("trusted")) return trustedHandler(ctx, "");
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const [head, ...sub] = rest;
|
|
685
|
+
const subArgs = sub.join(" ");
|
|
686
|
+
switch (head) {
|
|
687
|
+
case "background": return backgroundHandler(ctx, subArgs);
|
|
688
|
+
case "trusted": return trustedHandler(ctx, subArgs);
|
|
689
|
+
default:
|
|
690
|
+
ctx.ui.notify(`Unknown setting '${head}'. Try: /chrome settings background | trusted.`, "warning");
|
|
691
|
+
}
|
|
692
|
+
};
|
|
653
693
|
|
|
654
|
-
pi.registerCommand("chrome
|
|
694
|
+
pi.registerCommand("chrome", {
|
|
655
695
|
description:
|
|
656
|
-
"
|
|
696
|
+
"All pi-chrome controls in one place. Subcommands:\n /chrome doctor — quick health check.\n /chrome onboard — install the Chrome companion extension.\n /chrome settings — change how pi-chrome behaves (background mode, click realism).\nRun with no arguments for an interactive picker.",
|
|
657
697
|
getArgumentCompletions: (prefix) => {
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
698
|
+
const rawTokens = prefix.split(/\s+/);
|
|
699
|
+
const last = (rawTokens[rawTokens.length - 1] ?? "").toLowerCase();
|
|
700
|
+
const path = rawTokens.slice(0, -1).filter(Boolean).map((t) => t.toLowerCase());
|
|
701
|
+
|
|
702
|
+
const TOP = [
|
|
703
|
+
{ value: "doctor", description: "Quick health check. Tells you if Chrome is connected and what's wrong if it isn't." },
|
|
704
|
+
{ value: "onboard", description: "Install the Chrome companion extension (first-time setup)." },
|
|
705
|
+
{ value: "settings", description: "Change pi-chrome behaviour: background mode, click realism." },
|
|
661
706
|
];
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
707
|
+
const SETTINGS = [
|
|
708
|
+
{ value: "background", description: "Should Chrome pop to the front when pi-chrome acts, or work silently?" },
|
|
709
|
+
{ value: "trusted", description: "How realistic should pi-chrome's clicks and keystrokes be?" },
|
|
710
|
+
];
|
|
711
|
+
const BG = [
|
|
712
|
+
{ value: "on", description: "Work silently. Chrome stays in the background. Your editor keeps focus." },
|
|
713
|
+
{ value: "off", description: "Bring Chrome to the front so you can watch (default)." },
|
|
714
|
+
];
|
|
715
|
+
const TRUSTED = [
|
|
716
|
+
{ value: "auto", description: "Default. Quiet clicks; upgrade to real ones only when a site rejects them." },
|
|
717
|
+
{ value: "off", description: "Always quiet. No banner. Some sites won't accept the clicks." },
|
|
718
|
+
{ value: "on", description: "Always real-looking clicks. Banner stays up. Best for stubborn sites." },
|
|
719
|
+
{ value: "status", description: "Show the current click mode." },
|
|
720
|
+
];
|
|
721
|
+
|
|
722
|
+
let pool: Array<{ value: string; description: string }> | null = null;
|
|
723
|
+
if (path.length === 0) pool = TOP;
|
|
724
|
+
else if (path[0] === "settings" && path.length === 1) pool = SETTINGS;
|
|
725
|
+
else if (path[0] === "settings" && path[1] === "background" && path.length === 2) pool = BG;
|
|
726
|
+
else if (path[0] === "settings" && path[1] === "trusted" && path.length === 2) pool = TRUSTED;
|
|
727
|
+
if (!pool) return null;
|
|
728
|
+
|
|
729
|
+
const items = pool.map((p) => ({ value: p.value, label: p.value, description: p.description }));
|
|
730
|
+
const filtered = items.filter((i) => i.value.startsWith(last));
|
|
731
|
+
return filtered.length > 0 ? filtered : null;
|
|
665
732
|
},
|
|
666
733
|
handler: async (args, ctx) => {
|
|
667
|
-
const
|
|
668
|
-
if (
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
"
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
pi.registerCommand("chrome-onboard", {
|
|
681
|
-
description: "Walk through installing the Chrome companion extension that pi-chrome needs to control your browser.",
|
|
682
|
-
handler: async (_args, ctx) => {
|
|
683
|
-
const extensionPath = browserExtensionPath();
|
|
684
|
-
const proceed = await ctx.ui.confirm(
|
|
685
|
-
"Install the pi-chrome Chrome extension?",
|
|
686
|
-
`This opens Chrome's extensions page and reveals the folder pi-chrome needs you to load.\n\nWhen the windows open, in Chrome:\n 1. Turn on 'Developer mode' (top-right toggle).\n 2. Click 'Load unpacked' and choose the folder that just opened in Finder, or paste this path:\n ${extensionPath}\n\nPress Enter to continue, or Esc to cancel.`,
|
|
687
|
-
);
|
|
688
|
-
if (!proceed) {
|
|
689
|
-
ctx.ui.notify("Cancelled. You can run /chrome-onboard again whenever you're ready.", "info");
|
|
734
|
+
const tokens = (args || "").trim().split(/\s+/).filter(Boolean);
|
|
735
|
+
if (tokens.length === 0) {
|
|
736
|
+
const picked = await ctx.ui.select("pi-chrome — what would you like to do?", [
|
|
737
|
+
"doctor — quick health check; tells you what's wrong if Chrome isn't responding",
|
|
738
|
+
"onboard — install the Chrome companion extension (first-time setup)",
|
|
739
|
+
"settings — change pi-chrome behaviour (background mode, click realism)",
|
|
740
|
+
]);
|
|
741
|
+
if (!picked) return;
|
|
742
|
+
if (picked.startsWith("doctor")) return doctorHandler(ctx);
|
|
743
|
+
if (picked.startsWith("onboard")) return onboardHandler(ctx);
|
|
744
|
+
if (picked.startsWith("settings")) return settingsHandler(ctx, []);
|
|
690
745
|
return;
|
|
691
746
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
747
|
+
const [head, ...rest] = tokens;
|
|
748
|
+
switch (head) {
|
|
749
|
+
case "doctor": return doctorHandler(ctx);
|
|
750
|
+
case "onboard": return onboardHandler(ctx);
|
|
751
|
+
case "settings": return settingsHandler(ctx, rest);
|
|
752
|
+
default:
|
|
753
|
+
ctx.ui.notify(`Unknown subcommand '${head}'. Try: /chrome doctor | onboard | settings.`, "warning");
|
|
696
754
|
}
|
|
697
|
-
ctx.ui.notify(
|
|
698
|
-
"Chrome and Finder should be open. The extension folder path is on your clipboard. After you click 'Load unpacked' and pick it, run /chrome-doctor to confirm everything is connected.",
|
|
699
|
-
"info",
|
|
700
|
-
);
|
|
701
755
|
},
|
|
702
756
|
});
|
|
703
757
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-chrome",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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",
|