pi-chrome 0.15.20 → 0.15.23

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,25 @@
2
2
 
3
3
  All notable user-facing changes to `pi-chrome`.
4
4
 
5
+ ## 0.15.23 — 2026-05-16
6
+
7
+ - **Attribution.** The 0.15.22 features below are pulled from Dani Bednarski's fork (`DaniBedz/pi-chrome`). Thank you, Dani.
8
+
9
+ ## 0.15.22 — 2026-05-16
10
+
11
+ Features in this release are pulled from Dani Bednarski's fork (`DaniBedz/pi-chrome`). Thank you, Dani.
12
+
13
+ - **Earlier page-load capture.** Companion extension now injects console/network instrumentation at `document_start`, so initial React render errors and early API calls show up in `chrome_list_console_messages` / `chrome_list_network_requests`.
14
+ - **Quieter locked state.** Startup no longer shows a persistent Chrome bridge notification/status item before authorization; status bar appears only when Chrome control is authorized.
15
+ - **Lazy tool registration.** `chrome_*` tools and primer are registered only after `/chrome authorize`, reducing prompt/tool overhead while Chrome control is locked.
16
+
17
+ ## 0.15.21 — 2026-05-16
18
+
19
+ ### Reverted 0.16.x and 0.17.x lines
20
+
21
+ - Versions 0.16.0 through 0.17.2 were published to npm and subsequently unpublished. 0.17.3 was prepared locally but never published. The work introduced in those versions — mandatory pairing, signed-envelope auth, standalone bridge daemon, idempotent onboard, etc. — is reachable only via git tags (`v0.16.0` … `v0.17.3`) and is not in the current main branch.
22
+ - This release is **tree-equivalent to 0.15.20** with a version-only bump so future patch releases can ship cleanly.
23
+
5
24
  ## 0.15.20 — 2026-05-15
6
25
 
7
26
  - **Interruptible `chrome_*` tools.** All `chrome_*` tools now honor the agent harness `AbortSignal`, so pressing Esc aborts in-flight bridge calls (including the long-polling `chrome_wait_for`) immediately instead of waiting out the full `timeoutMs`.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Pi Chrome Connector",
4
- "version": "0.15.20",
4
+ "version": "0.15.23",
5
5
  "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -966,6 +966,24 @@ if (chrome.webNavigation && chrome.webNavigation.onCommitted) {
966
966
  });
967
967
  }
968
968
 
969
+ // Always inject early console/network capture at document_start on every navigation.
970
+ // Catches console messages, errors, and network requests that fire during page load,
971
+ // before chrome_snapshot or chrome_evaluate install the instrumentation normally.
972
+ // The function installEarlyCapture sets __piChromeWrapped flags so the post-hoc
973
+ // installPiChromeInstrumentation() call is idempotent.
974
+ if (chrome.webNavigation && chrome.webNavigation.onCommitted) {
975
+ chrome.webNavigation.onCommitted.addListener((details) => {
976
+ if (details.frameId !== 0) return;
977
+ chrome.scripting.executeScript({
978
+ target: { tabId: details.tabId, frameIds: [0] },
979
+ world: "MAIN",
980
+ injectImmediately: true,
981
+ func: installEarlyCapture,
982
+ args: [],
983
+ }).catch(() => undefined);
984
+ });
985
+ }
986
+
969
987
  async function bringToFront(tab) {
970
988
  await chrome.windows.update(tab.windowId, { focused: true });
971
989
  await chrome.tabs.update(tab.id, { active: true });
@@ -1310,6 +1328,141 @@ function installPiChromeInstrumentation() {
1310
1328
  }
1311
1329
  }
1312
1330
 
1331
+ // Early-capture version of installPiChromeInstrumentation, designed to be injected
1332
+ // at document_start via webNavigation.onCommitted. Wraps console, fetch, and XHR
1333
+ // before the page's own JavaScript runs, so page-load errors are captured.
1334
+ // Sets __piChromeWrapped flags so the post-hoc installPiChromeInstrumentation()
1335
+ // sees them and skips (idempotent).
1336
+ // NOTE: This function is self-contained — it does NOT close over any outer scope
1337
+ // because it gets serialized by chrome.scripting.executeScript({func: ...}).
1338
+ function installEarlyCapture() {
1339
+ if (window.__piChromeEarlyCaptureInstalled) return;
1340
+ window.__piChromeEarlyCaptureInstalled = true;
1341
+ var state = window.__PI_CHROME_STATE__;
1342
+ if (!state) {
1343
+ state = {
1344
+ nextElementUid: 1,
1345
+ elements: {},
1346
+ console: [],
1347
+ network: [],
1348
+ nextRequestId: 1,
1349
+ instrumentationInstalled: false,
1350
+ };
1351
+ window.__PI_CHROME_STATE__ = state;
1352
+ }
1353
+ function pushConsole(level, args) {
1354
+ state.console.push({
1355
+ id: state.console.length + 1,
1356
+ level: level,
1357
+ timestamp: Date.now(),
1358
+ url: location.href,
1359
+ args: Array.from(args).map(function(arg) {
1360
+ try {
1361
+ if (typeof arg === "string") return arg;
1362
+ if (arg instanceof Error) return { name: arg.name, message: arg.message, stack: arg.stack };
1363
+ return JSON.parse(JSON.stringify(arg));
1364
+ } catch (e) {
1365
+ return String(arg);
1366
+ }
1367
+ }),
1368
+ });
1369
+ if (state.console.length > 500) state.console.splice(0, state.console.length - 500);
1370
+ }
1371
+ for (var i = 0; i < 5; i++) {
1372
+ var levels = ["debug", "log", "info", "warn", "error"];
1373
+ var level = levels[i];
1374
+ var original = console[level];
1375
+ if (typeof original !== "function" || original.__piChromeWrapped) continue;
1376
+ var wrapped = function(lvl, orig) {
1377
+ return function() {
1378
+ pushConsole(lvl, arguments);
1379
+ return orig.apply(this, arguments);
1380
+ };
1381
+ }(level, original);
1382
+ wrapped.__piChromeWrapped = true;
1383
+ console[level] = wrapped;
1384
+ }
1385
+ window.addEventListener("error", function(event) {
1386
+ pushConsole("pageerror", [event.message, event.filename + ":" + event.lineno + ":" + event.colno]);
1387
+ });
1388
+ window.addEventListener("unhandledrejection", function(event) {
1389
+ pushConsole("unhandledrejection", [event.reason]);
1390
+ });
1391
+ var trimBody = function(text) {
1392
+ return typeof text === "string" && text.length > 200000 ? text.slice(0, 200000) + "\n[truncated " + (text.length - 200000) + " chars]" : text;
1393
+ };
1394
+ var record = function(entry) {
1395
+ state.network.push(entry);
1396
+ if (state.network.length > 1000) state.network.splice(0, state.network.length - 1000);
1397
+ return entry;
1398
+ };
1399
+ if (window.fetch && !window.fetch.__piChromeWrapped) {
1400
+ var originalFetch = window.fetch.bind(window);
1401
+ var wrappedFetch = async function() {
1402
+ var args = [];
1403
+ for (var k = 0; k < arguments.length; k++) args.push(arguments[k]);
1404
+ var id = "req-" + state.nextRequestId++;
1405
+ var startedAt = Date.now();
1406
+ var input = args[0];
1407
+ var init = args[1] || {};
1408
+ var url = typeof input === "string" ? input : (input ? input.url : "");
1409
+ var method = (init.method || (input ? input.method : null) || "GET").toUpperCase();
1410
+ var entry = record({ id: id, type: "fetch", method: method, url: String(url || ""), startedAt: startedAt, pageUrl: location.href, status: "pending" });
1411
+ try {
1412
+ var response = await originalFetch.apply(window, args);
1413
+ entry.status = response.status;
1414
+ entry.statusText = response.statusText;
1415
+ entry.ok = response.ok;
1416
+ entry.responseUrl = response.url;
1417
+ entry.durationMs = Date.now() - startedAt;
1418
+ entry.responseHeaders = Array.from(response.headers.entries());
1419
+ response.clone().text().then(function(text) {
1420
+ entry.responseBody = trimBody(text);
1421
+ entry.responseBodyTruncated = typeof text === "string" && text.length > 200000;
1422
+ }).catch(function(error) { entry.responseBodyError = error ? error.message : String(error); });
1423
+ return response;
1424
+ } catch (error) {
1425
+ entry.error = error ? error.message : String(error);
1426
+ entry.durationMs = Date.now() - startedAt;
1427
+ throw error;
1428
+ }
1429
+ };
1430
+ wrappedFetch.__piChromeWrapped = true;
1431
+ window.fetch = wrappedFetch;
1432
+ }
1433
+ if (window.XMLHttpRequest && !XMLHttpRequest.prototype.open.__piChromeWrapped) {
1434
+ var originalOpen = XMLHttpRequest.prototype.open;
1435
+ var originalSend = XMLHttpRequest.prototype.send;
1436
+ XMLHttpRequest.prototype.open = function(method, url) {
1437
+ this.__piChromeRequest = { method: String(method || "GET").toUpperCase(), url: String(url || "") };
1438
+ return originalOpen.apply(this, arguments);
1439
+ };
1440
+ XMLHttpRequest.prototype.open.__piChromeWrapped = true;
1441
+ XMLHttpRequest.prototype.send = function(body) {
1442
+ var id = "req-" + state.nextRequestId++;
1443
+ var startedAt = Date.now();
1444
+ var info = this.__piChromeRequest || {};
1445
+ var entry = record({ id: id, type: "xhr", method: info.method || "GET", url: info.url || "", startedAt: startedAt, pageUrl: location.href, status: "pending" });
1446
+ this.addEventListener("loadend", function() {
1447
+ entry.status = this.status;
1448
+ entry.statusText = this.statusText;
1449
+ entry.responseUrl = this.responseURL;
1450
+ entry.durationMs = Date.now() - startedAt;
1451
+ try { entry.responseHeadersText = this.getAllResponseHeaders(); } catch (e) {}
1452
+ try {
1453
+ if (typeof this.responseText === "string") {
1454
+ entry.responseBody = trimBody(this.responseText);
1455
+ entry.responseBodyTruncated = this.responseText.length > 200000;
1456
+ }
1457
+ } catch (error) { entry.responseBodyError = error ? error.message : String(error); }
1458
+ });
1459
+ this.addEventListener("error", function() { entry.error = "XMLHttpRequest error"; entry.durationMs = Date.now() - startedAt; });
1460
+ return originalSend.apply(this, arguments);
1461
+ };
1462
+ }
1463
+ state.instrumentationInstalled = true;
1464
+ }
1465
+
1313
1466
  function snapshotPage(maxElements, containingText, roleFilter, nearUid) {
1314
1467
  installPiChromeInstrumentation();
1315
1468
  const unique = (selector) => {
@@ -463,6 +463,7 @@ export default function (pi: ExtensionAPI): void {
463
463
  const bridge = new ChromeProfileBridge(DEFAULT_HOST, DEFAULT_PORT);
464
464
  let backgroundDefault = false;
465
465
  let chromeAuthorizedUntil: number | "indefinite" | undefined;
466
+ let chromeToolsRegistered = false;
466
467
 
467
468
  const authSummary = (): string => {
468
469
  if (chromeAuthorizedUntil === "indefinite") return "authorized indefinitely";
@@ -486,6 +487,14 @@ export default function (pi: ExtensionAPI): void {
486
487
  }
487
488
  };
488
489
 
490
+ const updateChromeStatus = (ctx: ExtensionContext): void => {
491
+ if (chromeControlAuthorized()) {
492
+ ctx.ui.setStatus("chrome", ctx.ui.theme.fg("success", "●") + " Chrome Bridge");
493
+ } else {
494
+ ctx.ui.setStatus("chrome", undefined);
495
+ }
496
+ };
497
+
489
498
  const authorizedBridgeSend = (action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise<unknown> => {
490
499
  requireChromeControlAuthorized();
491
500
  return bridge.send(action, params, timeoutMs, signal);
@@ -507,14 +516,7 @@ export default function (pi: ExtensionAPI): void {
507
516
 
508
517
  pi.on("session_start", async (_event, ctx) => {
509
518
  await bridge.start();
510
- const status = bridge.status();
511
- ctx.ui.setStatus("chrome", `Chrome bridge :${DEFAULT_PORT}`);
512
- ctx.ui.notify(
513
- status.mode === "client"
514
- ? `pi-chrome connected (sharing the Chrome connection an earlier pi session opened). Run /chrome authorize before using chrome_* tools.`
515
- : `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.`,
516
- "info",
517
- );
519
+ updateChromeStatus(ctx);
518
520
  });
519
521
 
520
522
  pi.on("session_shutdown", () => {
@@ -525,6 +527,9 @@ export default function (pi: ExtensionAPI): void {
525
527
  });
526
528
 
527
529
  pi.on("before_agent_start", (event) => {
530
+ if (!chromeToolsRegistered || !chromeControlAuthorized()) {
531
+ return { systemPrompt: event.systemPrompt };
532
+ }
528
533
  const primer = `
529
534
  <chrome-profile-bridge>
530
535
  Chrome control is available through the chrome_* tools via a companion Chrome extension installed in the user's normal Chrome profile. Tools target the existing signed-in profile: no remote-debug port, no throwaway profile.
@@ -648,8 +653,10 @@ Usage rules:
648
653
  ctx.ui.notify("Chrome control remains locked.", "info");
649
654
  return;
650
655
  }
656
+ registerChromeTools(pi);
651
657
  chromeAuthorizedUntil = until;
652
658
  ctx.ui.notify(`Chrome control authorized for ${label}.`, "info");
659
+ updateChromeStatus(ctx);
653
660
  };
654
661
 
655
662
  const parseAuthorizeArg = (arg: string): { label: string; until: number | "indefinite" } | undefined => {
@@ -672,6 +679,7 @@ Usage rules:
672
679
  const revokeHandler = (ctx: ExtensionContext) => {
673
680
  chromeAuthorizedUntil = undefined;
674
681
  ctx.ui.notify("Chrome control locked. Run /chrome authorize to allow chrome_* tools again.", "info");
682
+ updateChromeStatus(ctx);
675
683
  };
676
684
 
677
685
  const onboardHandler = async (ctx: ExtensionContext) => {
@@ -848,6 +856,10 @@ Usage rules:
848
856
  },
849
857
  });
850
858
 
859
+ function registerChromeTools(pi: ExtensionAPI): void {
860
+ if (chromeToolsRegistered) return;
861
+ chromeToolsRegistered = true;
862
+
851
863
  pi.registerTool({
852
864
  name: "chrome_launch",
853
865
  label: "Chrome Bridge Setup",
@@ -1381,4 +1393,6 @@ Usage rules:
1381
1393
  return { content: [{ type: "text", text: `Uploaded ${paths.length} file(s) to ${params.uid ?? params.selector}` }], details: { result: result as Json } };
1382
1394
  },
1383
1395
  });
1396
+ }
1397
+
1384
1398
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.15.20",
3
+ "version": "0.15.23",
4
4
  "scripts": {
5
5
  "version": "node scripts/sync-manifest-version.js",
6
6
  "prepublishOnly": "node scripts/sync-manifest-version.js"