pi-chrome 0.11.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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-background`, or pass `background: true` on a single tool call when you want quiet.
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-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.
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-doctor` reports the loaded extension version and warns when it drifts from the installed `pi-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-onboard
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-doctor
56
+ /chrome doctor
57
57
  ```
58
58
 
59
59
  Expected output:
@@ -65,24 +65,25 @@ pi-chrome v<version>
65
65
  ✓ Companion Chrome extension responding (ID: <chrome-extension-id>, ext v<version>)
66
66
  ```
67
67
 
68
- ## Trusted-input mode (CDP)
68
+ ## Click modes
69
69
 
70
- By default, `chrome_*` clicks and keystrokes are **synthetic** DOM events (`event.isTrusted === false`). They drive React/Vue/Angular state correctly but **do not** satisfy Chrome's user-activation gates: clipboard write, fullscreen, file picker, and autoplay all need a real user gesture.
70
+ pi-chrome can drive Chrome two ways:
71
71
 
72
- pi-chrome can optionally route input through `chrome.debugger` (CDP `Input.dispatchMouseEvent` / `Input.dispatchKeyEvent`) so each event arrives as `isTrusted=true`, satisfies user-activation, and bypasses site bot-detection that filters synthetic events. The tradeoff: Chrome pins a *"Pi Chrome Connector started debugging this browser"* banner to the top of any debugged tab.
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
+ - **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.
73
74
 
74
- Usage:
75
+ Pick a mode with `/chrome settings trusted`:
75
76
 
76
77
  ```text
77
- /chrome-trusted on # all chrome_* tools dispatch via CDP
78
- /chrome-trusted off # synthetic events only (default)
79
- /chrome-trusted auto # CDP only when a tool passes trusted=true
80
- /chrome-trusted status # show current mode and attached tabs
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
81
82
  ```
82
83
 
83
- For a single call, pass `trusted: true` on `chrome_click`, `chrome_type`, `chrome_fill`, `chrome_key`, `chrome_hover`, `chrome_drag`, or `chrome_scroll`. The per-call value always wins over the session toggle.
84
+ 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.
84
85
 
85
- First time you upgrade to a pi-chrome that uses trusted input, Chrome will prompt to re-approve the extension's new `debugger` permission do that once in `chrome://extensions`.
86
+ First time you update pi-chrome to a version that supports real-looking clicks, Chrome will ask you to re-approve the extension. Open `chrome://extensions` and accept the new permission once.
86
87
 
87
88
  ## Background mode
88
89
 
@@ -91,9 +92,9 @@ By default, `chrome_*` tools focus Chrome and activate the target tab so you can
91
92
  When you want quiet (planner / audit / worker sessions running alongside your editor), turn background mode on for the whole Pi session:
92
93
 
93
94
  ```text
94
- /chrome-background # toggle
95
- /chrome-background on # explicit
96
- /chrome-background off # explicit
95
+ /chrome settings background # toggle
96
+ /chrome settings background on # explicit
97
+ /chrome settings background off # explicit
97
98
  ```
98
99
 
99
100
  For a single tool call, the agent can pass `background: true` directly. The per-call value always wins over the session toggle.
@@ -136,9 +137,9 @@ Screenshots save under `.pi/chrome-screenshots/` by default, which composes nice
136
137
 
137
138
  ## Diagnostics
138
139
 
139
- - `/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).
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).
140
141
 
141
- 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`.
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`.
142
143
 
143
144
  ## Compose with
144
145
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Pi Chrome Connector",
4
- "version": "0.11.4",
4
+ "version": "0.12.0",
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/*"],
@@ -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-onboard, then load the bundled browser-extension folder in your normal Chrome profile.`,
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);
@@ -415,8 +415,8 @@ export default function (pi: ExtensionAPI): void {
415
415
  ctx.ui.setStatus("chrome", `Chrome bridge :${DEFAULT_PORT}`);
416
416
  ctx.ui.notify(
417
417
  status.mode === "client"
418
- ? `Chrome profile bridge connected to shared bridge at ${bridge.url}.`
419
- : `Chrome profile bridge listening at ${bridge.url}. Run /chrome-onboard to load the bundled browser extension in your normal Chrome profile.`,
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 onboard to install it.`,
420
420
  "info",
421
421
  );
422
422
  });
@@ -440,21 +440,20 @@ 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-background to silence the whole session.
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-doctor when in doubt about connectivity or capabilities.
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
- pi.registerCommand("chrome-doctor", {
451
- description:
452
- "Check Chrome bridge connectivity and capability tier. Probes the local bridge, the companion Chrome extension, MAIN-world evaluation, and CDP availability, and prints one-line fixes for common failures.",
453
- handler: async (_args, ctx) => {
454
- ctx.ui.notify("Performing Chrome bridge health check", "info");
450
+ // Shared handlers, dispatched by the unified /chrome command below.
451
+ const doctorHandler = async (ctx: ExtensionContext) => {
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();
457
- lines.push(`• Local bridge: mode=${status.mode}, url=${status.url}`);
455
+ const roleLabel = status.mode === "client" ? "sharing another pi session's connection" : "running the Chrome connection for this machine";
456
+ lines.push(`• This pi session is ${roleLabel}.`);
458
457
  let extensionAlive = false;
459
458
  let versionMismatch = false;
460
459
  try {
@@ -469,44 +468,44 @@ Usage rules:
469
468
  if (version.extensionVersion && version.extensionVersion !== PI_CHROME_VERSION) {
470
469
  versionMismatch = true;
471
470
  lines.push(
472
- `✗ EXTENSION VERSION MISMATCH: companion extension is v${version.extensionVersion}, but pi-chrome is v${PI_CHROME_VERSION}.`,
473
- ` All chrome_* tools will run with the OLD extension code until this is fixed.`,
474
- ` Fix: open chrome://extensions and click reload on "Pi Chrome Connector".`,
475
- ` (Future version drifts will self-heal: the extension now polls pi-chrome's expected version and reloads itself.)`,
471
+ `✗ The Chrome companion extension is on an old version (${version.extensionVersion}); this pi-chrome is ${PI_CHROME_VERSION}.`,
472
+ ` Every Chrome action will run with the old code until you reload the extension.`,
473
+ ` Fix: open chrome://extensions and click the refresh icon on 'Pi Chrome Connector'.`,
474
+ ` (After this one-time fix, future updates reload automatically.)`,
476
475
  );
477
476
  } else {
478
- lines.push(`✓ Companion Chrome extension responding (ID: ${version.extensionId ?? "?"}, ext v${version.extensionVersion ?? "?"}, latency ${latencyMs}ms)`);
477
+ lines.push(`✓ Chrome is connected (companion extension v${version.extensionVersion ?? "?"}, responded in ${latencyMs}ms).`);
479
478
  }
480
479
  } catch (error) {
481
480
  const message = (error as Error).message;
482
- lines.push(`✗ Companion Chrome extension not responding: ${message}`);
481
+ lines.push(`✗ Chrome isn't responding: ${message}`);
483
482
  if (message.includes("older pi-chrome without multi-session")) {
484
- lines.push(" Fix: restart the Pi session that owns the bridge (it was started on an older pi-chrome).");
483
+ lines.push(" Fix: quit and restart the pi session that first opened the Chrome connection (it was on an older pi-chrome).");
485
484
  } else {
486
- lines.push(" Fix: run /chrome-onboard, then load the bundled browser-extension folder in chrome://extensions and keep that Chrome window open.");
485
+ lines.push(" Fix: run /chrome onboard to install the Chrome companion extension, then keep that Chrome window open.");
487
486
  }
488
487
  }
489
488
 
490
489
  if (extensionAlive && !versionMismatch) {
491
- // MAIN-world evaluate probe.
490
+ // Sanity-check that pi-chrome can actually run code in the active tab.
492
491
  try {
493
492
  const value = await bridge.send("page.evaluate", { expression: "1+1", awaitPromise: true, foreground: false }, 10_000);
494
- if (value === 2) lines.push(`✓ chrome_evaluate("1+1") = 2`);
495
- else lines.push(`⚠ chrome_evaluate("1+1") returned ${JSON.stringify(value)} (expected 2). The current tab may have a restrictive CSP or be a chrome:// URL.`);
493
+ if (value === 2) lines.push(`✓ pi-chrome can run code in the active Chrome tab.`);
494
+ else lines.push(`⚠ pi-chrome ran code in the active tab but got an unexpected result (${JSON.stringify(value)}). The current tab may be locked-down (a Chrome internal page or a strict site).`);
496
495
  } catch (error) {
497
- lines.push(`✗ chrome_evaluate failed: ${(error as Error).message}`);
496
+ lines.push(`✗ pi-chrome can't run code in the active tab: ${(error as Error).message}`);
498
497
  }
499
498
 
500
- // Capability probe via MAIN-world helper.
499
+ // Surface obvious site-side automation flags so the user knows why a site might block pi.
501
500
  try {
502
501
  const probe = (await bridge.send("page.probe", { foreground: false }, 10_000)) as Record<string, unknown>;
503
- if (probe && probe.arithmetic === 2) lines.push(`✓ MAIN-world helper injection works (location=${hostnameOf(String(probe.location))})`);
504
- if (probe && probe.webdriver) lines.push(`⚠ navigator.webdriver=true on current tab site fingerprinting may flag automation.`);
502
+ if (probe && probe.arithmetic === 2) lines.push(`✓ The active tab is ${hostnameOf(String(probe.location))} and accepts pi-chrome's commands.`);
503
+ if (probe && probe.webdriver) lines.push(`⚠ Your Chrome is reporting itself as automated to websites. Some sites use this signal to block sign-ins or bot checks.`);
505
504
  } catch (error) {
506
- lines.push(`⚠ page.probe failed: ${(error as Error).message}`);
505
+ lines.push(`⚠ Couldn't inspect the active tab: ${(error as Error).message}`);
507
506
  }
508
507
  } else if (versionMismatch) {
509
- lines.push(`… Skipped MAIN-world capability checks because the loaded extension is stale.`);
508
+ lines.push(`… Skipped the remaining checks until you reload the Chrome extension.`);
510
509
  }
511
510
 
512
511
  // Real-input mode probe (plain English for the user).
@@ -524,7 +523,7 @@ Usage rules:
524
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."
525
524
  : status.mode === "on"
526
525
  ? " Every click and keystroke uses a real-looking event. The Chrome banner stays up on every tab pi-chrome touches."
527
- : " All clicks are quiet, no banner. Some sites (sign-ins, copy buttons, file pickers, paywalls) may silently ignore them. Switch to /chrome-trusted auto if a site isn’t responding.";
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.";
528
527
  const label = status.mode === "auto" ? "auto (smart upgrade)" : status.mode === "on" ? "on (always real-looking)" : "off (always quiet)";
529
528
  lines.push(`✓ Click mode: ${label}${banner}.${note}`);
530
529
  } else {
@@ -535,25 +534,10 @@ Usage rules:
535
534
  }
536
535
  }
537
536
 
538
- ctx.ui.notify(lines.join("\n"), "info");
539
- },
540
- });
537
+ ctx.ui.notify(lines.join("\n"), "info");
538
+ };
541
539
 
542
- pi.registerCommand("chrome-trusted", {
543
- description:
544
- "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.",
545
- getArgumentCompletions: (prefix) => {
546
- const items = [
547
- { value: "auto", label: "auto", description: "Default. Quiet clicks; upgrade to real ones only when a site rejects them." },
548
- { value: "off", label: "off", description: "Always quiet. No banner. Some sites won't accept the clicks." },
549
- { value: "on", label: "on", description: "Always real-looking clicks. Yellow banner stays up. Best for stubborn sites." },
550
- { value: "status", label: "status", description: "Show the current mode." },
551
- ];
552
- const lowered = prefix.toLowerCase();
553
- const matches = items.filter((item) => item.value.startsWith(lowered));
554
- return matches.length > 0 ? matches : null;
555
- },
556
- handler: async (args, ctx) => {
540
+ const trustedHandler = async (ctx: ExtensionContext, args: string) => {
557
541
  const rawArg = (args || "").trim().toLowerCase();
558
542
 
559
543
  // Resolve current status once for both branches (interactive picker + direct args).
@@ -611,7 +595,7 @@ Usage rules:
611
595
  }
612
596
 
613
597
  if (!["on", "off", "auto"].includes(target)) {
614
- ctx.ui.notify(`Unknown choice '${rawArg}'. Pick one of: auto | off | on | status, or just run /chrome-trusted with no argument.`, "warning");
598
+ ctx.ui.notify(`Unknown choice '${rawArg}'. Pick one of: auto | off | on | status, or run /chrome settings trusted with no argument.`, "warning");
615
599
  return;
616
600
  }
617
601
 
@@ -645,58 +629,129 @@ Usage rules:
645
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");
646
630
  }
647
631
  } catch (error) {
648
- ctx.ui.notify(`Couldn't switch mode: ${(error as Error).message}`, "warning");
649
- }
650
- },
651
- });
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
+ };
652
693
 
653
- pi.registerCommand("chrome-background", {
694
+ pi.registerCommand("chrome", {
654
695
  description:
655
- "Toggle silent/background mode for chrome_* tools. Background ON: chrome_* tools act silentlyyour editor/terminal keeps focus, Chrome does not pop up, your workflow is not interrupted. Background OFF (default): Chrome focuses and activates the target tab so you can watch the agent work, useful for demos, pair-driving, and debugging — tradeoff: Chrome pops up and steals focus. Pass `on` / `off` to set explicitly, or no argument to toggle.",
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.",
656
697
  getArgumentCompletions: (prefix) => {
657
- const items = [
658
- { value: "on", label: "on", description: "Run chrome_* actions silently without focusing Chrome" },
659
- { value: "off", label: "off", description: "Bring Chrome to the foreground for chrome_* actions (default)" },
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." },
660
706
  ];
661
- const lowered = prefix.toLowerCase();
662
- const matches = items.filter((item) => item.value.startsWith(lowered));
663
- return matches.length > 0 ? matches : null;
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;
664
732
  },
665
733
  handler: async (args, ctx) => {
666
- const arg = (args || "").trim().toLowerCase();
667
- if (arg === "on" || arg === "true" || arg === "1") backgroundDefault = true;
668
- else if (arg === "off" || arg === "false" || arg === "0") backgroundDefault = false;
669
- else backgroundDefault = !backgroundDefault;
670
- ctx.ui.notify(
671
- backgroundDefault
672
- ? "Chrome background mode ON. chrome_* tools will run silently. Your current app keeps focus."
673
- : "Chrome background mode OFF. chrome_* tools will focus Chrome and activate the target tab so you can watch the agent work.",
674
- "info",
675
- );
676
- },
677
- });
678
-
679
- pi.registerCommand("chrome-onboard", {
680
- description: "Guide Chrome extension setup for the existing-profile bridge",
681
- handler: async (_args, ctx) => {
682
- const extensionPath = browserExtensionPath();
683
- const proceed = await ctx.ui.confirm(
684
- "Chrome bridge setup",
685
- `This will open chrome://extensions and reveal the extension folder in Finder.\n\nAfter the windows open: enable Developer mode → Load unpacked → select:\n${extensionPath}\n\nPress Enter to continue, or Esc to cancel.`,
686
- );
687
- if (!proceed) {
688
- ctx.ui.notify("Chrome bridge setup cancelled", "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, []);
689
745
  return;
690
746
  }
691
- if (process.platform === "darwin") {
692
- await pi.exec("open", ["-a", "Google Chrome", "chrome://extensions"], { cwd: workspaceCwd(ctx), timeout: 5_000 }).catch(() => undefined);
693
- await pi.exec("open", ["-R", extensionPath], { cwd: workspaceCwd(ctx), timeout: 5_000 }).catch(() => undefined);
694
- await pi.exec("sh", ["-lc", `printf %s ${JSON.stringify(extensionPath)} | pbcopy`], { cwd: workspaceCwd(ctx), timeout: 5_000 }).catch(() => undefined);
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");
695
754
  }
696
- ctx.ui.notify(
697
- "Chrome bridge setup opened. The extension path has been copied to your clipboard. After loading it, run /chrome-doctor.",
698
- "info",
699
- );
700
755
  },
701
756
  });
702
757
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.11.4",
3
+ "version": "0.12.0",
4
4
  "description": "Drive your existing logged-in Chrome from Pi — no re-login, no throwaway profile, watch the agent work in real time (or toggle quiet background mode).",
5
5
  "keywords": [
6
6
  "pi-package",