pi-chrome 0.15.23 → 0.15.25

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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable user-facing changes to `pi-chrome`.
4
4
 
5
+ ## 0.15.25 — 2026-05-16
6
+
7
+ - **Reload after older installs.** `/reload` now recovers from stale singleton state left by pi-chrome 0.15.19 and earlier instead of skipping the freshly loaded extension.
8
+ - **Test suite coverage.** Added gate buckets plus strict-CSP fallback, dynamic wait readiness, and explicit tab lifecycle challenges.
9
+
10
+ ## 0.15.24 — 2026-05-16
11
+
12
+ - **Unload Chrome tools on lock.** `chrome_*` tools now deactivate when `/chrome revoke` runs or timed authorization expires, keeping the prompt/tool list small after Chrome control locks again.
13
+
5
14
  ## 0.15.23 — 2026-05-16
6
15
 
7
16
  - **Attribution.** The 0.15.22 features below are pulled from Dani Bednarski's fork (`DaniBedz/pi-chrome`). Thank you, Dani.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Pi Chrome Connector",
4
- "version": "0.15.23",
4
+ "version": "0.15.25",
5
5
  "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -441,6 +441,28 @@ class ChromeProfileBridge {
441
441
  const tabActionValues = ["list", "new", "activate", "close", "version"] as const;
442
442
  const imageFormatValues = ["png", "jpeg"] as const;
443
443
  const waitForValues = ["selector", "expression"] as const;
444
+ const CHROME_TOOL_NAMES = [
445
+ "chrome_launch",
446
+ "chrome_tab",
447
+ "chrome_snapshot",
448
+ "chrome_navigate",
449
+ "chrome_evaluate",
450
+ "chrome_click",
451
+ "chrome_type",
452
+ "chrome_fill",
453
+ "chrome_key",
454
+ "chrome_wait_for",
455
+ "chrome_list_console_messages",
456
+ "chrome_list_network_requests",
457
+ "chrome_get_network_request",
458
+ "chrome_screenshot",
459
+ "chrome_hover",
460
+ "chrome_drag",
461
+ "chrome_tap",
462
+ "chrome_scroll",
463
+ "chrome_upload_file",
464
+ ] as const;
465
+ const CHROME_TOOL_NAME_SET = new Set<string>(CHROME_TOOL_NAMES);
444
466
 
445
467
  function StringEnum<T extends readonly [string, ...string[]]>(values: T) {
446
468
  return Type.Union(values.map((value) => Type.Literal(value)) as [ReturnType<typeof Type.Literal>, ...ReturnType<typeof Type.Literal>[]]);
@@ -448,22 +470,50 @@ function StringEnum<T extends readonly [string, ...string[]]>(values: T) {
448
470
 
449
471
  export default function (pi: ExtensionAPI): void {
450
472
  const instanceToken = Symbol("pi-chrome-instance");
473
+ const currentRoot = extensionRoot();
451
474
  const globalState = globalThis as typeof globalThis & {
452
- [PI_CHROME_GLOBAL_KEY]?: { version: string; root: string; token: symbol };
475
+ [PI_CHROME_GLOBAL_KEY]?: { version: string; root: string; token?: symbol };
453
476
  };
454
477
  const alreadyLoaded = globalState[PI_CHROME_GLOBAL_KEY];
455
- if (alreadyLoaded) {
478
+ if (alreadyLoaded?.token || (alreadyLoaded && alreadyLoaded.root !== currentRoot)) {
456
479
  console.warn(
457
- `pi-chrome already loaded from ${alreadyLoaded.root} (v${alreadyLoaded.version}); skipping duplicate from ${extensionRoot()}.`,
480
+ `pi-chrome already loaded from ${alreadyLoaded.root} (v${alreadyLoaded.version}); skipping duplicate from ${currentRoot}.`,
458
481
  );
459
482
  return;
460
483
  }
461
- globalState[PI_CHROME_GLOBAL_KEY] = { version: PI_CHROME_VERSION, root: extensionRoot(), token: instanceToken };
484
+ // pi-chrome <=0.15.19 set the singleton flag but did not clear it on reload.
485
+ // If the stale flag points at this same extension root, replace it instead of
486
+ // skipping the freshly reloaded extension.
487
+ globalState[PI_CHROME_GLOBAL_KEY] = { version: PI_CHROME_VERSION, root: currentRoot, token: instanceToken };
462
488
 
463
489
  const bridge = new ChromeProfileBridge(DEFAULT_HOST, DEFAULT_PORT);
464
490
  let backgroundDefault = false;
465
491
  let chromeAuthorizedUntil: number | "indefinite" | undefined;
466
492
  let chromeToolsRegistered = false;
493
+ let authExpiryTimer: NodeJS.Timeout | undefined;
494
+
495
+ const clearAuthExpiryTimer = (): void => {
496
+ if (!authExpiryTimer) return;
497
+ clearTimeout(authExpiryTimer);
498
+ authExpiryTimer = undefined;
499
+ };
500
+
501
+ const activeToolNamesWithoutChrome = (): string[] => pi.getActiveTools().filter((name) => !CHROME_TOOL_NAME_SET.has(name));
502
+
503
+ const activateChromeTools = (): void => {
504
+ registerChromeTools(pi);
505
+ pi.setActiveTools([...new Set([...pi.getActiveTools(), ...CHROME_TOOL_NAMES])]);
506
+ };
507
+
508
+ const deactivateChromeTools = (): void => {
509
+ pi.setActiveTools(activeToolNamesWithoutChrome());
510
+ };
511
+
512
+ const lockChromeControl = (): void => {
513
+ clearAuthExpiryTimer();
514
+ chromeAuthorizedUntil = undefined;
515
+ deactivateChromeTools();
516
+ };
467
517
 
468
518
  const authSummary = (): string => {
469
519
  if (chromeAuthorizedUntil === "indefinite") return "authorized indefinitely";
@@ -477,7 +527,7 @@ export default function (pi: ExtensionAPI): void {
477
527
  const chromeControlAuthorized = (): boolean => {
478
528
  if (chromeAuthorizedUntil === "indefinite") return true;
479
529
  if (typeof chromeAuthorizedUntil === "number" && chromeAuthorizedUntil > Date.now()) return true;
480
- chromeAuthorizedUntil = undefined;
530
+ if (chromeAuthorizedUntil !== undefined) lockChromeControl();
481
531
  return false;
482
532
  };
483
533
 
@@ -495,6 +545,21 @@ export default function (pi: ExtensionAPI): void {
495
545
  }
496
546
  };
497
547
 
548
+ const scheduleAuthExpiry = (ctx: ExtensionContext, until: number | "indefinite"): void => {
549
+ clearAuthExpiryTimer();
550
+ if (until === "indefinite") return;
551
+ authExpiryTimer = setTimeout(() => {
552
+ if (chromeAuthorizedUntil !== until) return;
553
+ try {
554
+ lockChromeControl();
555
+ ctx.ui.notify("Chrome control authorization expired. Run /chrome authorize to allow chrome_* tools again.", "info");
556
+ updateChromeStatus(ctx);
557
+ } catch (error) {
558
+ console.warn(`Failed to expire pi-chrome authorization cleanly: ${(error as Error).message}`);
559
+ }
560
+ }, Math.max(0, until - Date.now()));
561
+ };
562
+
498
563
  const authorizedBridgeSend = (action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise<unknown> => {
499
564
  requireChromeControlAuthorized();
500
565
  return bridge.send(action, params, timeoutMs, signal);
@@ -520,6 +585,7 @@ export default function (pi: ExtensionAPI): void {
520
585
  });
521
586
 
522
587
  pi.on("session_shutdown", () => {
588
+ clearAuthExpiryTimer();
523
589
  bridge.stop();
524
590
  if (globalState[PI_CHROME_GLOBAL_KEY]?.token === instanceToken) {
525
591
  delete globalState[PI_CHROME_GLOBAL_KEY];
@@ -653,8 +719,9 @@ Usage rules:
653
719
  ctx.ui.notify("Chrome control remains locked.", "info");
654
720
  return;
655
721
  }
656
- registerChromeTools(pi);
657
722
  chromeAuthorizedUntil = until;
723
+ activateChromeTools();
724
+ scheduleAuthExpiry(ctx, until);
658
725
  ctx.ui.notify(`Chrome control authorized for ${label}.`, "info");
659
726
  updateChromeStatus(ctx);
660
727
  };
@@ -677,7 +744,7 @@ Usage rules:
677
744
  };
678
745
 
679
746
  const revokeHandler = (ctx: ExtensionContext) => {
680
- chromeAuthorizedUntil = undefined;
747
+ lockChromeControl();
681
748
  ctx.ui.notify("Chrome control locked. Run /chrome authorize to allow chrome_* tools again.", "info");
682
749
  updateChromeStatus(ctx);
683
750
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.15.23",
3
+ "version": "0.15.25",
4
4
  "scripts": {
5
5
  "version": "node scripts/sync-manifest-version.js",
6
6
  "prepublishOnly": "node scripts/sync-manifest-version.js"