chrome-relay 0.7.1 → 0.7.2

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/dist/cli.js CHANGED
@@ -445,6 +445,9 @@ function parseChromeSnapshotArgs(input) {
445
445
  const diff = optBool(obj, "diff", TOOL_NAMES.SNAPSHOT);
446
446
  if (diff !== void 0)
447
447
  out.diff = diff;
448
+ const elide = optBool(obj, "elide", TOOL_NAMES.SNAPSHOT);
449
+ if (elide !== void 0)
450
+ out.elide = elide;
448
451
  return out;
449
452
  }
450
453
  function parseChromeScreenshotArgs(input) {
@@ -585,16 +588,19 @@ function parseChromeNetworkArgs(input) {
585
588
  if (action === "har") {
586
589
  const withBodies = optBool(obj, "withBodies");
587
590
  const bestEffortBodies = optBool(obj, "bestEffortBodies");
591
+ const rawHeadersHar = optBool(obj, "rawHeaders");
588
592
  return {
589
593
  ...target,
590
594
  action: "har",
591
595
  ...withBodies !== void 0 ? { withBodies } : {},
592
596
  ...bestEffortBodies !== void 0 ? { bestEffortBodies } : {},
597
+ ...rawHeadersHar !== void 0 ? { rawHeaders: rawHeadersHar } : {},
593
598
  ...parseFilter(obj)
594
599
  };
595
600
  }
596
601
  if (action === "read") {
597
- return { ...target, action: "read", ...parseFilter(obj) };
602
+ const rawHeaders = optBool(obj, "rawHeaders");
603
+ return { ...target, action: "read", ...rawHeaders !== void 0 ? { rawHeaders } : {}, ...parseFilter(obj) };
598
604
  }
599
605
  throw new RelayError({
600
606
  code: "invalid_arguments",
@@ -1419,7 +1425,7 @@ var init_dist = __esm({
1419
1425
  import { Command } from "commander";
1420
1426
 
1421
1427
  // src/index.ts
1422
- var CHROME_RELAY_VERSION = true ? "0.7.1" : "0.0.0-dev";
1428
+ var CHROME_RELAY_VERSION = true ? "0.7.2" : "0.0.0-dev";
1423
1429
 
1424
1430
  // src/commands/shared.ts
1425
1431
  init_dist();
@@ -1836,15 +1842,20 @@ async function runDoctor() {
1836
1842
  console.log(`Tip: run "chrome-relay install" to refresh manifests for every detected browser.`);
1837
1843
  }
1838
1844
  let serverReachable = false;
1845
+ let connected = {};
1839
1846
  try {
1840
1847
  const response = await fetch(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/ping`);
1841
1848
  serverReachable = response.ok;
1849
+ if (response.ok) connected = await response.json();
1842
1850
  } catch {
1843
1851
  serverReachable = false;
1844
1852
  }
1845
1853
  console.log(`Allowed extension IDs: ${formatKnownExtensionIds()}`);
1846
1854
  console.log(`Local bridge reachable: ${serverReachable ? "yes" : "no"}`);
1847
- if (!serverReachable) {
1855
+ if (serverReachable) {
1856
+ const flavor = connected.extensionId === CHROME_WEB_STORE_EXTENSION_ID ? "Chrome Web Store" : connected.extensionId === LOCAL_UNPACKED_EXTENSION_ID ? "local unpacked (dev)" : connected.extensionId === LEGACY_DEV_EXTENSION_ID ? "legacy dev" : connected.extensionId ? "unknown id" : "id not reported (pre-0.8 host still running \u2014 restart Chrome or run chrome-relay update)";
1857
+ console.log(`Connected extension: ${connected.extensionVersion ?? "?"} \u2014 ${flavor}${connected.extensionId ? ` (${connected.extensionId})` : ""}`);
1858
+ } else {
1848
1859
  console.log(`Tip: load the extension in one of the detected browsers so it can launch the native host.`);
1849
1860
  }
1850
1861
  return allHealthy;
@@ -1856,6 +1867,11 @@ async function runDoctor() {
1856
1867
 
1857
1868
  // src/release-notes.ts
1858
1869
  var RELEASE_NOTES = {
1870
+ "0.7.2": [
1871
+ "Fix (Windows): `chrome-relay update` now resolves the installed binary with `where` (Windows has no `which`), picks the spawnable `.cmd`/`.exe` over the extensionless npm shim, and re-execs it through a shell with the path quoted. Pre-0.7.2 the `which` call always failed on Windows, so update skipped the post-install manifest refresh and re-fired the cli-outdated nudge \u2014 the same loop fixed on macOS/Linux in 0.5.21, now closed on Windows too.",
1872
+ "Fix (Windows): `screencast stop --gif/--mp4` detected ffmpeg with `which`, which doesn't exist on Windows, so the stitch step failed with `external_dependency_missing` even when ffmpeg was on PATH. Now uses `where` on win32. No change to the macOS/Linux path.",
1873
+ "No extension change. Both fixes are CLI-only; existing 0.7.x extensions work unchanged."
1874
+ ],
1859
1875
  "0.7.1": [
1860
1876
  "Fix: `wait` failed with `invalid_arguments: got 0 conditions` when invoked through the CLI. Root cause: the CLI transmitted the PARSED tool args over the bridge, and chrome_wait's parser transforms its shape ({selector} -> {condition}), so the extension re-parsed a shape it doesn't accept. The CLI now validates locally but transmits raw args \u2014 parsers run on the same raw shape at both ends, as designed. No extension change needed; 0.7.0 extensions work."
1861
1877
  ],
@@ -2051,6 +2067,23 @@ function listReleaseNotesSince(since) {
2051
2067
  }
2052
2068
 
2053
2069
  // src/commands/install-update.ts
2070
+ function whichBinary(spawnSync2, name) {
2071
+ const tool = process.platform === "win32" ? "where" : "which";
2072
+ const res = spawnSync2(tool, [name]);
2073
+ if (res.status !== 0) return null;
2074
+ const lines = (res.stdout?.toString() ?? "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
2075
+ if (lines.length === 0) return null;
2076
+ if (process.platform === "win32") {
2077
+ return lines.find((l) => /\.(cmd|bat|exe)$/i.test(l)) ?? lines[0];
2078
+ }
2079
+ return lines[0];
2080
+ }
2081
+ function spawnCli(spawnSync2, bin, args, opts = {}) {
2082
+ if (process.platform === "win32") {
2083
+ return spawnSync2(`"${bin}"`, args, { ...opts, shell: true });
2084
+ }
2085
+ return spawnSync2(bin, args, opts);
2086
+ }
2054
2087
  function registerInstallUpdate(program) {
2055
2088
  program.command("install").description("Install and register the local Chrome Relay host.").action(async () => {
2056
2089
  await runInstall();
@@ -2093,22 +2126,21 @@ function registerInstallUpdate(program) {
2093
2126
  process.stdout.write(JSON.stringify(out, null, 2) + "\n");
2094
2127
  process.exit(1);
2095
2128
  }
2096
- const which = spawnSync2("which", ["chrome-relay"]);
2097
- const newBin = which.stdout?.toString().trim();
2098
- if (which.status === 0 && newBin) {
2099
- const versionOut = spawnSync2(newBin, ["--version"]);
2129
+ const newBin = whichBinary(spawnSync2, "chrome-relay");
2130
+ if (newBin) {
2131
+ const versionOut = spawnCli(spawnSync2, newBin, ["--version"]);
2100
2132
  const newVersion = (versionOut.stdout?.toString() ?? "").trim();
2101
2133
  out.binary.path = newBin;
2102
2134
  if (newVersion && newVersion !== fromVersion) {
2103
2135
  out.updatedTo = newVersion;
2104
- const install2 = spawnSync2(newBin, ["install"], { stdio: "inherit" });
2136
+ const install2 = spawnCli(spawnSync2, newBin, ["install"], { stdio: "inherit" });
2105
2137
  if (install2.status !== 0) {
2106
2138
  out.warnings.push({
2107
2139
  code: "install_refresh_failed",
2108
2140
  message: `Update installed the new package but \`${newBin} install\` exited ${install2.status}. Run it manually to refresh the native host manifest.`
2109
2141
  });
2110
2142
  }
2111
- const rn = spawnSync2(newBin, ["release-notes", "--since", fromVersion]);
2143
+ const rn = spawnCli(spawnSync2, newBin, ["release-notes", "--since", fromVersion]);
2112
2144
  try {
2113
2145
  const parsed = JSON.parse(rn.stdout?.toString() ?? "");
2114
2146
  if (Array.isArray(parsed.changes)) {
@@ -2130,7 +2162,7 @@ function registerInstallUpdate(program) {
2130
2162
  } else {
2131
2163
  out.warnings.push({
2132
2164
  code: "update_not_verified",
2133
- message: `Install completed but \`which chrome-relay\` did not return a path. Could not verify the active binary changed.`
2165
+ message: `Install completed but \`${process.platform === "win32" ? "where" : "which"} chrome-relay\` did not return a path. Could not verify the active binary changed.`
2134
2166
  });
2135
2167
  }
2136
2168
  }
@@ -2161,7 +2193,7 @@ function registerNavigation(ctx) {
2161
2193
  await run("get_windows_and_tabs", {});
2162
2194
  });
2163
2195
  tabOpt(
2164
- program.command("navigate <url>").description("Navigate a tab to a URL. Use --tab <id> to target an existing tab.").option("--new", "open in a new tab").option("--active", "activate the tab after navigating (default: background \u2014 no focus theft)").addHelpText(
2196
+ program.command("navigate <url>").description("Navigate a tab to a URL. Use --tab <id> to target an existing tab.").option("--new", "open in a new tab").option("--active", "activate the tab after navigating (default: background, no focus theft)").addHelpText(
2165
2197
  "after",
2166
2198
  `
2167
2199
 
@@ -2171,7 +2203,7 @@ Examples:
2171
2203
  chrome-relay navigate "https://chrome-relay.kushalsm.com" --new # open in a new background tab
2172
2204
  chrome-relay navigate "https://chrome-relay.kushalsm.com" --new --active # open new tab AND show it to the user
2173
2205
 
2174
- By default chrome-relay never steals focus \u2014 navigated tabs (new or
2206
+ By default chrome-relay never steals focus. Navigated tabs (new or
2175
2207
  existing) stay in whatever state they're in. Pass --active when you
2176
2208
  actually want the user looking at the page.
2177
2209
  `
@@ -2220,7 +2252,7 @@ Examples:
2220
2252
  chrome-relay click 'button[aria-label="Save"]'
2221
2253
  chrome-relay click --tab 123 --x 1327 --y 771
2222
2254
 
2223
- Prefer @refs from \`chrome-relay snapshot\` \u2014 they carry their own tab and
2255
+ Prefer @refs from \`chrome-relay snapshot\`. They carry their own tab and
2224
2256
  survive DOM churn (backendNodeId + role/name heal). CSS selectors for
2225
2257
  elements you know statically. Coordinates for canvas/SVG chart internals
2226
2258
  where no DOM handle exists. See docs/clicking-strategies.md.
@@ -2273,10 +2305,10 @@ Examples:
2273
2305
  chrome-relay type "appended into already-focused element"
2274
2306
 
2275
2307
  When to pick which:
2276
- fill \u2014 plain <input>, <textarea>, <select>, React-controlled inputs (atomic write).
2277
- type \u2014 contenteditable, Draft.js, Lexical, ProseMirror (trusted text commit).
2278
- keys \u2014 Enter, Tab, Esc, arrows, modifier chords (single key press).
2279
- js \u2014 anything else.
2308
+ fill: plain <input>, <textarea>, <select>, React-controlled inputs (atomic write).
2309
+ type: contenteditable, Draft.js, Lexical, ProseMirror (trusted text commit).
2310
+ keys: Enter, Tab, Esc, arrows, modifier chords (single key press).
2311
+ js: anything else.
2280
2312
  `
2281
2313
  )
2282
2314
  ).action(async (text, opts) => {
@@ -2333,7 +2365,7 @@ import { writeFileSync } from "fs";
2333
2365
  import { structuredPatch } from "diff";
2334
2366
  function printSnapshotDiff(current, prevText) {
2335
2367
  if (prevText === null) {
2336
- process.stderr.write("[chrome-relay] no previous snapshot for this tab \u2014 showing full output.\n");
2368
+ process.stderr.write("[chrome-relay] no previous snapshot for this tab. Showing full output.\n");
2337
2369
  process.stdout.write(current + "\n");
2338
2370
  return;
2339
2371
  }
@@ -2375,17 +2407,17 @@ function exitWithError(error) {
2375
2407
  function registerCapture(ctx) {
2376
2408
  const { program, withBase, run } = ctx;
2377
2409
  tabOpt(
2378
- program.command("snapshot").description("Page snapshot with actionable @refs \u2014 accessibility tree + cursor-interactive sweep, compact text.").option("-i, --interactive", "only ref-bearing elements (buttons, links, inputs, named content, clickables)").option("-d, --depth <n>", "truncate the tree at this depth", (v) => Number(v)).option("-s, --scope <css>", "restrict to the subtree of the first CSS match").option("-u, --urls", "include link hrefs as url= attrs").option("--diff", "print only what changed since the previous snapshot of this tab (~100 tokens instead of a re-read)").option("--json", "structured output: { title, url, tabId, nodes, refs }").addHelpText(
2410
+ program.command("snapshot").description("Page snapshot with actionable @refs. Accessibility tree plus cursor-interactive sweep, compact text.").option("-i, --interactive", "only ref-bearing elements (buttons, links, inputs, named content, clickables)").option("-d, --depth <n>", "truncate the tree at this depth", (v) => Number(v)).option("-s, --scope <css>", "restrict to the subtree of the first CSS match").option("-u, --urls", "include link hrefs as url= attrs").option("--diff", "print only what changed since the previous snapshot of this tab (~100 tokens instead of a re-read)").option("--no-elide", "print every row of long identical-shape runs (default keeps 10 + a count marker)").option("--json", "structured output: { title, url, tabId, nodes, refs }").addHelpText(
2379
2411
  "after",
2380
2412
  `
2381
2413
 
2382
2414
  Examples:
2383
2415
  chrome-relay snapshot -i # see the page, get @refs
2384
- chrome-relay click @e12 # act on a ref \u2014 no --tab needed
2416
+ chrome-relay click @e12 # act on a ref, no --tab needed
2385
2417
  chrome-relay snapshot -i -s "#main" # scope to a subtree
2386
2418
  chrome-relay snapshot --json # machine-readable envelope
2387
2419
 
2388
- The core loop: snapshot -i \u2192 click/fill @eN \u2192 snapshot -i again after the
2420
+ The core loop: snapshot -i -> click/fill @eN -> snapshot -i again after the
2389
2421
  page changes. Refs carry their own tab and heal across DOM churn
2390
2422
  (backendNodeId fast path + role/name re-find); a dead ref returns
2391
2423
  error.code = stale_ref, which means: re-run snapshot.
@@ -2398,6 +2430,7 @@ error.code = stale_ref, which means: re-run snapshot.
2398
2430
  if (opts.scope) extras.scope = opts.scope;
2399
2431
  if (opts.urls) extras.urls = true;
2400
2432
  if (opts.diff) extras.diff = true;
2433
+ if (opts.elide === false) extras.elide = false;
2401
2434
  try {
2402
2435
  const result = await callTool("chrome_snapshot", withBase(opts, extras));
2403
2436
  if (opts.diff && !opts.json) {
@@ -2411,7 +2444,7 @@ error.code = stale_ref, which means: re-run snapshot.
2411
2444
  }
2412
2445
  });
2413
2446
  tabOpt(
2414
- program.command("screenshot").description("Capture a screenshot of any tab without activating it.").option("--full", "capture beyond the viewport (full page)").option("--bbox <rect>", "capture a region: 'x,y,width,height' (pixels)").option("--selector <css>", "capture the bounding box of a CSS selector").option("--padding <px>", "pixels of padding around --selector region", (v) => Number(v)).option("--max-edge <px>", "downscale so longer edge \u2264 this many pixels (no default; opt-in)", (v) => Number(v)).option("-o, --out <path>", "save image to path (base64 PNG decoded)").addHelpText(
2447
+ program.command("screenshot").description("Capture a screenshot of any tab without activating it.").option("--full", "capture beyond the viewport (full page)").option("--bbox <rect>", "capture a region: 'x,y,width,height' (pixels)").option("--selector <css>", "capture the bounding box of a CSS selector").option("--padding <px>", "pixels of padding around --selector region", (v) => Number(v)).option("--max-edge <px>", "downscale so longer edge is at most this many pixels (no default; opt-in)", (v) => Number(v)).option("-o, --out <path>", "save image to path (base64 PNG decoded)").addHelpText(
2415
2448
  "after",
2416
2449
  `
2417
2450
 
@@ -2453,7 +2486,7 @@ full-tab screenshot when an agent only needs to see one component.
2453
2486
  }
2454
2487
  });
2455
2488
  tabOpt(
2456
- program.command("read").description("[deprecated \u2014 use `snapshot`] Alias for the unified snapshot.").option("-i, --interactive", "only ref-bearing elements").option("--json", "structured output")
2489
+ program.command("read").description("[deprecated: use `snapshot`] Alias for the unified snapshot.").option("-i, --interactive", "only ref-bearing elements").option("--json", "structured output")
2457
2490
  ).action(async (opts) => {
2458
2491
  process.stderr.write("[chrome-relay] deprecated: `read` is now an alias for `snapshot` (new output format). Use `chrome-relay snapshot`.\n");
2459
2492
  const extras = {};
@@ -2466,7 +2499,7 @@ full-tab screenshot when an agent only needs to see one component.
2466
2499
  }
2467
2500
  });
2468
2501
  tabOpt(
2469
- program.command("ax").description("[deprecated \u2014 use `snapshot`] Alias for the unified snapshot.").option("-i, --interactive-only", "only ref-bearing elements").option("--root <role>", "(ignored \u2014 use `snapshot --scope <css>`)").option("--include-subframes", "(ignored \u2014 snapshot is top-frame only)").option("--json", "structured output")
2502
+ program.command("ax").description("[deprecated: use `snapshot`] Alias for the unified snapshot.").option("-i, --interactive-only", "only ref-bearing elements").option("--root <role>", "(ignored: use `snapshot --scope <css>`)").option("--include-subframes", "(ignored: snapshot is top-frame only)").option("--json", "structured output")
2470
2503
  ).action(async (opts) => {
2471
2504
  process.stderr.write("[chrome-relay] deprecated: `ax` is now an alias for `snapshot` (new output format, one ref space). Use `chrome-relay snapshot`.\n");
2472
2505
  const extras = {};
@@ -2479,7 +2512,7 @@ full-tab screenshot when an agent only needs to see one component.
2479
2512
  }
2480
2513
  });
2481
2514
  tabOpt(
2482
- program.command("click-ax").description("[deprecated \u2014 use `click @eN`] Click by raw backendDOMNodeId (from `snapshot --json` refs).").requiredOption("--node <id>", "backendDOMNodeId from `chrome-relay ax`", (v) => Number(v)).addHelpText(
2515
+ program.command("click-ax").description("[deprecated: use `click @eN`] Click by raw backendDOMNodeId (from `snapshot --json` refs).").requiredOption("--node <id>", "backendDOMNodeId from `chrome-relay ax`", (v) => Number(v)).addHelpText(
2483
2516
  "after",
2484
2517
  `
2485
2518
 
@@ -2585,10 +2618,10 @@ Notes:
2585
2618
  if (opts.gif || opts.mp4) {
2586
2619
  const fps = typeof opts.fps === "number" ? opts.fps : 15;
2587
2620
  const { spawnSync: spawnSync2 } = await import("child_process");
2588
- const which = spawnSync2("which", ["ffmpeg"]);
2621
+ const which = spawnSync2(process.platform === "win32" ? "where" : "which", ["ffmpeg"]);
2589
2622
  if (which.status !== 0) {
2590
2623
  if (opts.allowMissingFfmpeg) {
2591
- process.stderr.write("[chrome-relay] ffmpeg not on PATH \u2014 skipping --gif/--mp4 (allow-missing-ffmpeg).\n");
2624
+ process.stderr.write("[chrome-relay] ffmpeg not on PATH. Skipping --gif/--mp4 (allow-missing-ffmpeg).\n");
2592
2625
  return;
2593
2626
  }
2594
2627
  const { RelayError: RelayError2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
@@ -2654,7 +2687,7 @@ Notes:
2654
2687
  init_dist();
2655
2688
  var CONSOLE_BUFFER_MAX_KB = Math.round(CONSOLE_BUFFER_MAX_BYTES / 1024);
2656
2689
  function netFilterOpts(cmd) {
2657
- return cmd.option("--filter <substr>", "url substring filter").option("--status <bucket>", "ok | redirect | client_error | server_error | failed").option("--method <verb>", "exact method, e.g. POST").option("--limit <n>", "cap response length", (v) => Number(v));
2690
+ return cmd.option("--filter <substr>", "url substring filter").option("--status <bucket>", "ok | redirect | client_error | server_error | failed").option("--method <verb>", "exact method, e.g. POST").option("--limit <n>", "cap response length", (v) => Number(v)).option("--raw-headers", "include sensitive header values (cookies, auth, tokens). REDACTED by default so output is safe to paste");
2658
2691
  }
2659
2692
  function netFilterArgs(opts) {
2660
2693
  const a = {};
@@ -2662,6 +2695,7 @@ function netFilterArgs(opts) {
2662
2695
  if (opts.status) a.status = opts.status;
2663
2696
  if (opts.method) a.method = opts.method;
2664
2697
  if (typeof opts.limit === "number") a.limit = opts.limit;
2698
+ if (opts.rawHeaders) a.rawHeaders = true;
2665
2699
  return a;
2666
2700
  }
2667
2701
  function registerSessions(ctx) {
@@ -2802,8 +2836,8 @@ Privacy:
2802
2836
  share with the agent invoking chrome-relay.
2803
2837
 
2804
2838
  Notes:
2805
- Bodies are NOT eagerly buffered \u2014 Chrome GCs response bodies ~30s after
2806
- the request finishes. Use \`--body <id>\` or \`har --with-bodies\` promptly.
2839
+ Bodies are NOT eagerly buffered. Chrome GCs response bodies ~30s after
2840
+ the request finishes. Use \`network body <requestId>\` or \`network har --with-bodies\` promptly.
2807
2841
  WebSocket frames and SSE streams are out of scope.
2808
2842
  `
2809
2843
  ).action(async (opts) => {
@@ -2815,7 +2849,7 @@ Notes:
2815
2849
  await run("chrome_network", withBase(opts, netFilterArgs(opts)));
2816
2850
  });
2817
2851
  tabOpt(
2818
- network.command("body <requestId>").description("Fetch the response body for one request (lazy; may fail if GC'd).").option("--head <bytes>", "truncate to first N bytes", (v) => Number(v)).option("--full", "return the full body \u2014 default truncates to 8 KB")
2852
+ network.command("body <requestId>").description("Fetch the response body for one request (lazy; may fail if GC'd).").option("--head <bytes>", "truncate to first N bytes", (v) => Number(v)).option("--full", "return the full body. Default truncates to 8 KB")
2819
2853
  ).action(async (requestId, opts) => {
2820
2854
  const extras = { action: "body", requestId };
2821
2855
  if (opts.full) extras.full = true;
@@ -2823,7 +2857,7 @@ Notes:
2823
2857
  await run("chrome_network", withBase(opts, extras));
2824
2858
  });
2825
2859
  tabOpt(netFilterOpts(
2826
- network.command("har").description("Emit HAR-compatible JSON for the captured entries.").option("--with-bodies", "fetch response bodies before emitting; strict by default \u2014 fails if any body cannot be fetched").option("--best-effort-bodies", "with --with-bodies: keep the HAR even when some bodies are missing/errored (legacy behavior); per-entry _chrome_relay.bodyState/bodyError records what failed")
2860
+ network.command("har").description("Emit HAR-compatible JSON for the captured entries.").option("--with-bodies", "fetch response bodies before emitting; strict by default, fails if any body cannot be fetched").option("--best-effort-bodies", "with --with-bodies: keep the HAR even when some bodies are missing/errored (legacy behavior); per-entry _chrome_relay.bodyState/bodyError records what failed")
2827
2861
  )).action(async (opts) => {
2828
2862
  const extras = { ...netFilterArgs(opts), action: "har" };
2829
2863
  if (opts.withBodies) extras.withBodies = true;
@@ -2871,11 +2905,11 @@ Notes:
2871
2905
  init_dist();
2872
2906
  import { readFileSync } from "fs";
2873
2907
  function coreSkillText() {
2874
- if (true) return '# Chrome Relay\n\nDrives the user\'s real Chrome through a Chrome extension + local native host. Prefer it when logged-in browser state (auth cookies, sessions, installed extensions) matters.\n\n## Setup\n\n1. [Chrome extension](https://chromewebstore.google.com/detail/chrome-relay/cpdiapbifblhlcpnmlmfpgfjlacebokb)\n2. CLI:\n ```sh\n pnpm add -g chrome-relay\n chrome-relay install\n chrome-relay doctor\n ```\n\nVerify CLI \u2265 0.7.0 \u2014 wait/get/batch/`snapshot --diff` landed there (0.6.0 brought the snapshot/@ref loop; \u2265 0.5.20 fixed a silent click bug on Radix/React-Aria UIs):\n```sh\nchrome-relay --version\n```\n\n## The core loop\n\n```sh\nchrome-relay tabs # find or create a tab\nchrome-relay navigate "https://kushalsm.com" --new # background tab by default\nchrome-relay snapshot --tab 1234 -i # see the page: actionable elements get @refs\nchrome-relay click @e12 # act on refs \u2014 no --tab, no selector\nchrome-relay fill @e14 "hello"\nchrome-relay wait --text "Saved" --tab 1234 # block until the page reacts\nchrome-relay snapshot --tab 1234 --diff # print only what changed (~100 tokens)\n```\n\nSnapshot output is compact indented text (~1\u201315 KB for most pages) \u2014 read it directly, no jq needed:\n\n```\n- link "Hacker News" [ref=e4]\n- textbox "Search" [ref=e41]: current value\n- checkbox "Remember me" [checked, ref=e42]\n- clickable "Open card" [ref=e88] \u2190 cursor-pointer div the AX tree missed\n```\n\n**Refs carry their own tab.** `click @e12` acts on the tab that produced e12, never the active tab \u2014 safe while the user keeps browsing. A contradicting `--tab` errors with `target_conflict`.\n\n**Ref lifetime.** Refs survive same-page DOM churn (cached backendNodeId, healed by role+name re-find when nodes are replaced) but die on real navigation. A dead ref returns `error.code = stale_ref` \u2192 re-run `snapshot`.\n\n**Interception.** Ref clicks hit-test the point first: if an overlay / sticky header / modal owns it, you get `error.code = click_intercepted` naming the interceptor \u2014 dismiss it or scroll, then retry. The click was NOT delivered. `fill`/`type` skip this check (covered inputs are still writable).\n\n## Tool surface\n\n| Command | What it does |\n|---|---|\n| `tabs` | List windows + tabs with their `tabId`s |\n| `navigate <url>` | Open in current tab. `--new` opens in a **background** tab (default). `--active` brings it to foreground. `--tab <id>` retargets an existing tab. |\n| `snapshot --tab <id> -i` | Page snapshot with actionable `@refs` \u2014 accessibility tree + cursor-interactive sweep, one ref space, compact text. `-d N` depth cap, `-s <css>` scope to subtree, `-u` include hrefs, `--diff` print only changes since the last snapshot, `--json` structured envelope with the refs map. |\n| `wait <css\\|@ref>` / `wait --text` / `--url <glob>` / `--load networkidle` / `--fn <js>` | Block until a condition holds (one per call, default 10s, max 25s). `wait 1500` just sleeps. On timeout the error includes current page state. |\n| `get text\\|value\\|attr\\|count\\|title\\|url <target>` | One value, plain to stdout \u2014 no full snapshot. `get text @e12`, `get attr @e7 href`, `get count ".row"`. |\n| `batch \'[{"name":"chrome_...","args":{...}}, ...]\'` | N tool calls in ONE round-trip, sequential, bail-on-error by default. Use wire tool names. |\n| `skills get core` | Print this playbook, version-matched to the installed binary. |\n| `click <@ref \\| selector> --tab <id>` | Trusted hover + press + release at element center (`pointerType: "mouse"`). Refs need no `--tab`. |\n| `click --x N --y N --tab <id>` | Coordinate-mode click \u2014 for canvas/SVG chart internals with no DOM handle. |\n| `hover <@ref \\| selector \\| --x --y>` | Pointer move only \u2014 fires `:hover` styles. |\n| `fill <@ref \\| selector> <value>` | Atomic value write into `<input>`/`<textarea>`/`<select>`. Bypasses React\'s value tracker. Refs reach inside shadow DOM (selectors can\'t). |\n| `type <text> [-s <@ref \\| selector>]` | CDP `Input.insertText`. Use for contenteditable / Draft.js / Lexical / ProseMirror. **Appends** at caret; clear the input first if it had a value. |\n| `keys <chord> --tab <id>` | Single key or chord: `Enter`, `Tab`, `Escape`, `Cmd+K`, `Shift+ArrowDown`. |\n| `js <code> --tab <id>` | `Runtime.evaluate` in MAIN world. Use `return` for the value. Top-level `await` works. |\n| `screenshot --tab <id> -o <path>` | PNG. `--full` captures beyond viewport. `--max-edge N` resizes. |\n| `screencast --tab <id> -o <path>` | Record a tab via CDP (paint-driven). Requires an active tab. |\n| `network --tab <id>` | HTTP request/response ring buffer, last 200 per tab. `network read --request-id <id>` for bodies. |\n| `console --tab <id>` | `console.log/warn/error` + page exceptions, last 200. |\n| `viewport` | Emulate device viewport, DPR, mobile flag, touch, UA. |\n| `workspace` / `group` | Manage named windows / tab-groups so multiple agents can drive separate windows. |\n| `switch <tabId>` / `close <tabIds...>` | Activate or close tabs |\n| `self-reload` | Restart the extension\'s service worker after a rebuild |\n| `release-notes --since <ver>` / `update` | Queryable changelog; agent-readable JSON. |\n| `call <tool> [json]` | Raw pass-through for any internal tool. |\n| `read` / `ax` / `click-ax` | **Deprecated** \u2014 aliases for `snapshot` / `click @ref`. Will be removed; don\'t use in new work. |\n\n## Picking the right text tool\n\n| Target element | Tool |\n|---|---|\n| `<input>`, `<textarea>`, `<select>` (including React-controlled, shadow DOM) | `fill @ref` |\n| `[contenteditable]`, `role="textbox"`, Draft.js / Lexical / ProseMirror, X compose, LinkedIn DM, new Reddit composer | `type` |\n| Submit, navigate menus, modifier shortcuts | `keys` |\n| Combobox / autocomplete option selection | `type` into filter \u2192 `keys ArrowDown` \u2192 `keys Enter` ([why](references/patterns.md)) |\n| Framework-internal pokes, scraping, custom widgets | `js` |\n\n## Element addressing \u2014 the fallback ladder\n\n1. **`@ref` from `snapshot -i`** \u2014 default. Covers buttons/links/inputs, named content, cursor-pointer div-soup (the sweep), and shadow DOM.\n2. **CSS selector** \u2014 when you know the selector statically and don\'t need a snapshot.\n3. **`js` probe \u2192 coordinate click** \u2014 canvas internals and SVG chart segments (anonymous `<path>` elements have no DOM handle anywhere):\n ```sh\n chrome-relay js --tab 1234 "const r = document.querySelector(\'svg path\').getBoundingClientRect(); return {x: r.x + r.width/2, y: r.y + r.height/2}"\n chrome-relay click --tab 1234 --x 312 --y 218\n ```\n\n## Don\'t poll \u2014 wait\n\nA snapshot after every action wastes turns. The cheap loop on a changing page:\n\n```sh\nchrome-relay click @e12\nchrome-relay wait --text "Saved" --tab 1234 # or wait <selector> / --url / --load\nchrome-relay snapshot --tab 1234 --diff # only the changes, refs included\n```\n\n## Top gotchas\n\n1. **`type` appends** \u2014 it inserts at the caret. If the input had a value (autosaved draft, default text), clear it first via `js` or `keys` (Cmd+A then Backspace).\n2. **Refs die on navigation** \u2014 `stale_ref` means the page changed under you; re-snapshot. Don\'t retry the same ref.\n3. **Coords go stale fast** \u2014 read `getBoundingClientRect`, scroll/reflow, then click \u2192 you hit the wrong element. For autocomplete popups especially, use keyboard nav, not coord clicks.\n4. **Click "succeeded" but nothing happened** \u2014 first diagnostic: `document.elementFromPoint(x, y)`. If it returns a wrapper or form background, your coords are wrong. If it returns the right element but state didn\'t change, you\'re likely on chrome-relay <0.5.20 \u2014 upgrade.\n\nMore recipes: [references/patterns.md](references/patterns.md)\nFailure modes: [references/troubleshooting.md](references/troubleshooting.md)\n\n## Operational guidance\n\n- **Don\'t give up early.** A failing click is information, not a stop signal. Attach a document-level listener with `capture:true` and watch what fires:\n ```sh\n chrome-relay js --tab 1234 "\n [\'pointerdown\',\'mousedown\',\'click\'].forEach(t =>\n document.addEventListener(t, e => console.log(t, e.target.tagName, e.target.className), {capture:true})\n );\n return \'listening\'\n "\n # do the action, then:\n chrome-relay console --tab 1234\n ```\n- **Don\'t echo secrets.** When extracting tokens / API keys via `js`, write the result directly to a file. Never `echo $TOKEN` or interpolate into shell strings \u2014 it ends up in scrollback, logs, and tool transcripts.\n- **Capture before irreversible actions** (form submit, send message, account change). Save the screenshot path.\n\n## Guardrails\n\n- Errors are structured: branch on `relayError.code` (`stale_ref`, `click_intercepted`, `element_not_found`, `target_conflict`, `timeout`), not on message text.\n- If a flag is unclear, `chrome-relay <command> --help` is authoritative \u2014 these docs lag.';
2908
+ if (true) return '# Chrome Relay\n\nDrives the user\'s real Chrome through a Chrome extension + local native host. Prefer it when logged-in browser state (auth cookies, sessions, installed extensions) matters.\n\n## Setup\n\n1. [Chrome extension](https://chromewebstore.google.com/detail/chrome-relay/cpdiapbifblhlcpnmlmfpgfjlacebokb)\n2. CLI:\n ```sh\n pnpm add -g chrome-relay\n chrome-relay install\n chrome-relay doctor\n ```\n\nVerify CLI >= 0.7.0. wait/get/batch/`snapshot --diff` landed there. 0.6.0 brought the snapshot/@ref loop. >= 0.5.20 fixed a silent click bug on Radix/React-Aria UIs:\n```sh\nchrome-relay --version\n```\n\n## The core loop\n\n```sh\nchrome-relay tabs # find or create a tab\nchrome-relay navigate "https://kushalsm.com" --new # background tab by default\nchrome-relay snapshot --tab 1234 -i # see the page: actionable elements get @refs\nchrome-relay click @e12 # act on refs, no --tab, no selector\nchrome-relay fill @e14 "hello"\nchrome-relay wait --text "Saved" --tab 1234 # block until the page reacts\nchrome-relay snapshot --tab 1234 --diff # print only what changed (~100 tokens)\n```\n\nSnapshot output is compact indented text, usually 1 to 15 KB for most pages. Read it directly, no jq needed:\n\n```\n- link "Hacker News" [ref=e4]\n- textbox "Search" [ref=e41]: current value\n- checkbox "Remember me" [checked, ref=e42]\n- clickable "Open card" [ref=e88] # cursor-pointer div the AX tree missed\n```\n\n**Refs carry their own tab.** `click @e12` acts on the tab that produced e12, never the active tab. Safe while the user keeps browsing. A contradicting `--tab` errors with `target_conflict`.\n\n**Ref lifetime.** Refs survive same-page DOM churn (cached backendNodeId, healed by role+name re-find when nodes are replaced) but die on real navigation. A dead ref returns `error.code = stale_ref`. Re-run `snapshot`.\n\n**Interception.** Ref clicks hit-test the point first. If an overlay / sticky header / modal owns it, you get `error.code = click_intercepted` naming the interceptor. Dismiss it or scroll, then retry. The click was NOT delivered. `fill`/`type` skip this check (covered inputs are still writable).\n\n## Tool surface\n\n| Command | What it does |\n|---|---|\n| `tabs` | List windows + tabs with their `tabId`s |\n| `navigate <url>` | Open in current tab. `--new` opens in a **background** tab (default). `--active` brings it to foreground. `--tab <id>` retargets an existing tab. |\n| `snapshot --tab <id> -i` | Page snapshot with actionable `@refs`: accessibility tree plus cursor-interactive sweep, one ref space, compact text. `-d N` depth cap, `-s <css>` scope to subtree, `-u` include hrefs, `--diff` print only changes since the last snapshot, `--json` structured envelope with the refs map. |\n| `wait <css\\|@ref>` / `wait --text` / `--url <glob>` / `--load networkidle` / `--fn <js>` | Block until a condition holds (one per call, default 10s, max 25s). `wait 1500` just sleeps. On timeout the error includes current page state. |\n| `get text\\|value\\|attr\\|count\\|title\\|url <target>` | One value, plain to stdout. No full snapshot. `get text @e12`, `get attr @e7 href`, `get count ".row"`. |\n| `batch \'[{"name":"chrome_...","args":{...}}, ...]\'` | N tool calls in ONE round-trip, sequential, bail-on-error by default. Use wire tool names. |\n| `skills get core` | Print this playbook, version-matched to the installed binary. |\n| `click <@ref \\| selector> --tab <id>` | Trusted hover + press + release at element center (`pointerType: "mouse"`). Refs need no `--tab`. |\n| `click --x N --y N --tab <id>` | Coordinate-mode click for canvas/SVG chart internals with no DOM handle. |\n| `hover <@ref \\| selector \\| --x --y>` | Pointer move only. Fires `:hover` styles. |\n| `fill <@ref \\| selector> <value>` | Atomic value write into `<input>`/`<textarea>`/`<select>`. Bypasses React\'s value tracker. Refs reach inside shadow DOM (selectors can\'t). |\n| `type <text> [-s <@ref \\| selector>]` | CDP `Input.insertText`. Use for contenteditable / Draft.js / Lexical / ProseMirror. **Appends** at caret; clear the input first if it had a value. |\n| `keys <chord> --tab <id>` | Single key or chord: `Enter`, `Tab`, `Escape`, `Cmd+K`, `Shift+ArrowDown`. |\n| `js <code> --tab <id>` | `Runtime.evaluate` in MAIN world. Use `return` for the value. Top-level `await` works. |\n| `screenshot --tab <id> -o <path>` | PNG. `--full` captures beyond viewport. `--max-edge N` resizes. |\n| `screencast --tab <id> -o <path>` | Record a tab via CDP (paint-driven). Requires an active tab. |\n| `network --tab <id>` | HTTP request/response ring buffer, last 200 per tab. `network body <requestId>` fetches a body while Chrome still has it. `network har --with-bodies` exports a HAR with bodies. |\n| `console --tab <id>` | `console.log/warn/error` + page exceptions, last 200. |\n| `viewport` | Emulate device viewport, DPR, mobile flag, touch, UA. |\n| `workspace` / `group` | Manage named windows / tab-groups so multiple agents can drive separate windows. |\n| `switch <tabId>` / `close <tabIds...>` | Activate or close tabs |\n| `self-reload` | Restart the extension\'s service worker after a rebuild |\n| `release-notes --since <ver>` / `update` | Queryable changelog; agent-readable JSON. |\n| `call <tool> [json]` | Raw pass-through for any internal tool. |\n| `read` / `ax` / `click-ax` | **Deprecated**. Aliases for `snapshot` / `click @ref`. Will be removed; don\'t use in new work. |\n\n## Picking the right text tool\n\n| Target element | Tool |\n|---|---|\n| `<input>`, `<textarea>`, `<select>` (including React-controlled, shadow DOM) | `fill @ref` |\n| `[contenteditable]`, `role="textbox"`, Draft.js / Lexical / ProseMirror, X compose, LinkedIn DM, new Reddit composer | `type` |\n| Submit, navigate menus, modifier shortcuts | `keys` |\n| Combobox / autocomplete option selection | `type` into filter, then `keys ArrowDown`, then `keys Enter` ([why](references/patterns.md)) |\n| Framework-internal pokes, scraping, custom widgets | `js` |\n\n## Element addressing: the fallback ladder\n\n1. **`@ref` from `snapshot -i`**: default. Covers buttons/links/inputs, named content, cursor-pointer div-soup (the sweep), and shadow DOM.\n2. **CSS selector**: when you know the selector statically and don\'t need a snapshot.\n3. **`js` probe, then coordinate click**: canvas internals and SVG chart segments (anonymous `<path>` elements have no DOM handle anywhere):\n ```sh\n chrome-relay js --tab 1234 "const r = document.querySelector(\'svg path\').getBoundingClientRect(); return {x: r.x + r.width/2, y: r.y + r.height/2}"\n chrome-relay click --tab 1234 --x 312 --y 218\n ```\n\n## Don\'t poll. Wait.\n\nA snapshot after every action wastes turns. The cheap loop on a changing page:\n\n```sh\nchrome-relay click @e12\nchrome-relay wait --text "Saved" --tab 1234 # or wait <selector> / --url / --load\nchrome-relay snapshot --tab 1234 --diff # only the changes, refs included\n```\n\n## Top gotchas\n\n0. **`snapshot -i` is for ACTING, not fact extraction.** It prints ref-bearing elements only. Non-interactive values (dashboard metrics, paragraph text, chart labels) drop out. Measured live: a Cloudflare Pages metrics page lost all its numbers under `-i`. To READ facts, use full `snapshot`, `get text <target>`, or a `js` projection.\n1. **`type` appends.** It inserts at the caret. If the input had a value (autosaved draft, default text), clear it first via `js` or `keys` (Cmd+A then Backspace).\n2. **Refs die on navigation.** `stale_ref` means the page changed under you; re-snapshot. Don\'t retry the same ref.\n3. **Coords go stale fast.** Read `getBoundingClientRect`, scroll/reflow, then click, and you hit the wrong element. For autocomplete popups especially, use keyboard nav, not coord clicks.\n4. **Click "succeeded" but nothing happened.** First diagnostic: `document.elementFromPoint(x, y)`. If it returns a wrapper or form background, your coords are wrong. If it returns the right element but state didn\'t change, you\'re likely on chrome-relay <0.5.20. Upgrade.\n\nMore recipes: [references/patterns.md](references/patterns.md)\nFailure modes: [references/troubleshooting.md](references/troubleshooting.md)\n\n## Operational guidance\n\n- **Don\'t give up early.** A failing click is information, not a stop signal. Attach a document-level listener with `capture:true` and watch what fires:\n ```sh\n chrome-relay js --tab 1234 "\n [\'pointerdown\',\'mousedown\',\'click\'].forEach(t =>\n document.addEventListener(t, e => console.log(t, e.target.tagName, e.target.className), {capture:true})\n );\n return \'listening\'\n "\n # do the action, then:\n chrome-relay console --tab 1234\n ```\n- **Don\'t echo secrets.** When extracting tokens / API keys via `js`, write the result directly to a file. Never `echo $TOKEN` or interpolate into shell strings. It ends up in scrollback, logs, and tool transcripts.\n- **Redact `network` output.** Request/response headers carry cookies, auth/CSRF tokens, account and project IDs. Never paste raw `chrome-relay network` output into chat, docs, issues, or commits. Filter to the fields you need (url, status, timings) or redact headers first.\n- **Capture before irreversible actions** (form submit, send message, account change). Save the screenshot path.\n\n## Guardrails\n\n- Errors are structured: branch on `relayError.code` (`stale_ref`, `click_intercepted`, `element_not_found`, `target_conflict`, `timeout`), not on message text.\n- If a flag is unclear, `chrome-relay <command> --help` is authoritative. These docs lag.';
2875
2909
  try {
2876
2910
  return readFileSync(new URL("../../../../skills/chrome-relay/SKILL.md", import.meta.url), "utf8").replace(/^---\n[\s\S]*?\n---\n/, "").replace(/<!--[\s\S]*?-->\n*/, "").trim();
2877
2911
  } catch {
2878
- return "core skill unavailable in this build \u2014 see https://chrome-relay.kushalsm.com/skill.md";
2912
+ return "core skill unavailable in this build. See https://chrome-relay.kushalsm.com/skill.md";
2879
2913
  }
2880
2914
  }
2881
2915
  function exitWithError2(error) {
@@ -2928,7 +2962,7 @@ need a follow-up probe.
2928
2962
  if (typeof opts.timeout === "number") extras.timeoutMs = opts.timeout;
2929
2963
  await run(TOOL_NAMES.WAIT, withBase(opts, extras));
2930
2964
  });
2931
- const get = program.command("get").description("One value, plain to stdout \u2014 no full snapshot.").addHelpText(
2965
+ const get = program.command("get").description("One value, plain to stdout. No full snapshot.").addHelpText(
2932
2966
  "after",
2933
2967
  `
2934
2968
 
@@ -2991,7 +3025,7 @@ Examples:
2991
3025
 
2992
3026
  One HTTP POST, one native-messaging message, sequential execution in the
2993
3027
  extension. Tool names are the wire names (chrome_navigate, chrome_snapshot,
2994
- chrome_click_element, ... \u2014 see \`chrome-relay call --help\`). Amortizes CLI
3028
+ chrome_click_element, ...; see \`chrome-relay call --help\`). Amortizes CLI
2995
3029
  startup across N actions. Nested batches are rejected.
2996
3030
  `
2997
3031
  ).action(async (json, opts) => {
@@ -3038,7 +3072,7 @@ startup across N actions. Nested batches are rejected.
3038
3072
  exitWithError2(error);
3039
3073
  }
3040
3074
  });
3041
- const skills = program.command("skills [verb] [name]").description("Agent playbooks shipped inside the CLI \u2014 always version-matched to the binary.").addHelpText(
3075
+ const skills = program.command("skills [verb] [name]").description("Agent playbooks shipped inside the CLI. Always version-matched to the binary.").addHelpText(
3042
3076
  "after",
3043
3077
  `
3044
3078
 
@@ -3046,13 +3080,13 @@ Examples:
3046
3080
  chrome-relay skills # list available skills
3047
3081
  chrome-relay skills get core # print the core usage guide
3048
3082
 
3049
- The same guide is hosted at https://chrome-relay.kushalsm.com/skill.md \u2014
3050
- the binary copy is authoritative for the version you have installed.
3083
+ The same guide is hosted at https://chrome-relay.kushalsm.com/skill.md.
3084
+ The binary copy is authoritative for the version you have installed.
3051
3085
  `
3052
3086
  );
3053
3087
  skills.action(async (verb, name) => {
3054
3088
  if (!verb || verb === "list") {
3055
- process.stdout.write("core \u2014 the Chrome Relay agent playbook (snapshot/@ref loop, text-tool table, gotchas)\n");
3089
+ process.stdout.write("core: the Chrome Relay agent playbook (snapshot/@ref loop, text-tool table, gotchas)\n");
3056
3090
  return;
3057
3091
  }
3058
3092
  if (verb === "get" && (name === "core" || name === void 0)) {
@@ -3068,7 +3102,7 @@ the binary copy is authoritative for the version you have installed.
3068
3102
  // src/program.ts
3069
3103
  function buildProgram() {
3070
3104
  const program = new Command();
3071
- program.name("chrome-relay").description("Your agent drives the Chrome you're signed into \u2014 reads pages, clicks buttons, fills forms from any shell.").version(CHROME_RELAY_VERSION).showHelpAfterError().option("--workspace <name>", "target the active tab in a named workspace window (works at top level too)").option("--group <name>", "target the active tab in a named tab-group (works at top level too)").enablePositionalOptions().addHelpText(
3105
+ program.name("chrome-relay").description("Your agent drives the Chrome you're signed into. Reads pages, clicks buttons, fills forms from any shell.").version(CHROME_RELAY_VERSION).showHelpAfterError().option("--workspace <name>", "target the active tab in a named workspace window (works at top level too)").option("--group <name>", "target the active tab in a named tab-group (works at top level too)").enablePositionalOptions().addHelpText(
3072
3106
  "after",
3073
3107
  `
3074
3108
 
@@ -3076,7 +3110,7 @@ The core loop:
3076
3110
  chrome-relay tabs
3077
3111
  chrome-relay navigate "https://chrome-relay.kushalsm.com" --new # background tab
3078
3112
  chrome-relay snapshot --tab <tabId> -i # actionable elements get @refs
3079
- chrome-relay click @e12 # act on a ref \u2014 no --tab needed
3113
+ chrome-relay click @e12 # act on a ref, no --tab needed
3080
3114
  chrome-relay fill @e14 "value"
3081
3115
  chrome-relay snapshot --tab <tabId> -i # re-look after the page changes
3082
3116
 
@@ -3090,8 +3124,8 @@ Also:
3090
3124
 
3091
3125
  Notes:
3092
3126
  Refs come from snapshot and carry their own tab. Tools attach via CDP and
3093
- run on backgrounded tabs without stealing focus. Errors are structured \u2014
3094
- branch on relayError.code (stale_ref means: re-run snapshot).
3127
+ run on backgrounded tabs without stealing focus. Errors are structured.
3128
+ Branch on relayError.code (stale_ref means: re-run snapshot).
3095
3129
  `
3096
3130
  );
3097
3131
  const baseArgs = makeBaseArgs(program);
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- var CHROME_RELAY_VERSION = true ? "0.7.1" : "0.0.0-dev";
2
+ var CHROME_RELAY_VERSION = true ? "0.7.2" : "0.0.0-dev";
3
3
  export {
4
4
  CHROME_RELAY_VERSION
5
5
  };
@@ -56,7 +56,7 @@ function toBridgeError(unknownErr, fallbackTool) {
56
56
  }
57
57
 
58
58
  // src/index.ts
59
- var CHROME_RELAY_VERSION = true ? "0.7.1" : "0.0.0-dev";
59
+ var CHROME_RELAY_VERSION = true ? "0.7.2" : "0.0.0-dev";
60
60
 
61
61
  // src/release-notes.ts
62
62
  function compareSemver(a, b) {
@@ -104,7 +104,8 @@ var RelayHttpServer = class {
104
104
  ok: true,
105
105
  port: this.port,
106
106
  cliVersion: CHROME_RELAY_VERSION,
107
- extensionVersion: this.bridge.getExtensionVersion() ?? null
107
+ extensionVersion: this.bridge.getExtensionVersion() ?? null,
108
+ extensionId: this.bridge.getExtensionId() ?? null
108
109
  }));
109
110
  this.app.post("/call", async (request, reply) => {
110
111
  if (request.headers.origin) {
@@ -154,12 +155,17 @@ var ExtensionBridge = class {
154
155
  pending = /* @__PURE__ */ new Map();
155
156
  readyWaiters = /* @__PURE__ */ new Set();
156
157
  ready = false;
157
- // Extension version captured from `bridge.ready`. Read by the HTTP server
158
- // to compute the cli-outdated notice on each tool call.
158
+ // Extension version + id captured from `bridge.ready`. Version feeds the
159
+ // cli-outdated notice; the id tells doctor WHICH extension owns the bridge
160
+ // (store vs unpacked dev — they race for the port when both are loaded).
159
161
  extensionVersion;
162
+ extensionId;
160
163
  getExtensionVersion() {
161
164
  return this.extensionVersion;
162
165
  }
166
+ getExtensionId() {
167
+ return this.extensionId;
168
+ }
163
169
  handleMessage(message) {
164
170
  if (message.type === "bridge.ready") {
165
171
  this.handleReady(message);
@@ -182,6 +188,7 @@ var ExtensionBridge = class {
182
188
  handleReady(message) {
183
189
  this.ready = true;
184
190
  this.extensionVersion = message.payload?.version;
191
+ this.extensionId = message.payload?.extensionId;
185
192
  for (const notify of this.readyWaiters) {
186
193
  notify();
187
194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-relay",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,7 +16,7 @@
16
16
  "typecheck": "tsc -p tsconfig.json --noEmit",
17
17
  "test": "vitest run"
18
18
  },
19
- "description": "Your agent drives the Chrome you're signed into read pages, click buttons, fill forms from any shell. No robot browser, no cookie export, no focus stealing.",
19
+ "description": "Your agent drives the Chrome you're signed into. Read pages, click buttons, fill forms from any shell. No robot browser, no cookie export, no focus stealing.",
20
20
  "keywords": [
21
21
  "chrome",
22
22
  "browser",