chrome-relay 0.7.0 → 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.0" : "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();
@@ -1527,8 +1533,8 @@ function emitTargetOverride(kind, from, to) {
1527
1533
  }
1528
1534
  async function runToolImpl(name, args) {
1529
1535
  try {
1530
- const parsedArgs = isToolName(name) ? parseToolArgs(name, args) : args;
1531
- const result = await callTool(name, parsedArgs);
1536
+ if (isToolName(name)) parseToolArgs(name, args);
1537
+ const result = await callTool(name, args);
1532
1538
  if (typeof result === "string") {
1533
1539
  process.stdout.write(result + "\n");
1534
1540
  } else {
@@ -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,14 @@ 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
+ ],
1875
+ "0.7.1": [
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."
1877
+ ],
1859
1878
  "0.7.0": [
1860
1879
  "`wait` \u2014 block until a condition holds: `wait <css|@ref>` (exists and visible), `wait --text <s>`, `wait --url <glob>`, `wait --load load|domcontentloaded|networkidle`, `wait --fn <js>`, or `wait <ms>` for a plain sleep. One condition per call; default 10s, capped 25s (under the transport timeout, so waits always resolve in their own round-trip). Timeout errors include the page's current state so no follow-up probe is needed.",
1861
1880
  "`snapshot --diff` \u2014 print only what changed since this tab's previous snapshot (unified hunks + an additions/removals count; ~100 tokens instead of a full re-read). The full snapshot is still taken and the ref map still refreshes \u2014 refs in the diff are current and clickable.",
@@ -2048,6 +2067,23 @@ function listReleaseNotesSince(since) {
2048
2067
  }
2049
2068
 
2050
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
+ }
2051
2087
  function registerInstallUpdate(program) {
2052
2088
  program.command("install").description("Install and register the local Chrome Relay host.").action(async () => {
2053
2089
  await runInstall();
@@ -2090,22 +2126,21 @@ function registerInstallUpdate(program) {
2090
2126
  process.stdout.write(JSON.stringify(out, null, 2) + "\n");
2091
2127
  process.exit(1);
2092
2128
  }
2093
- const which = spawnSync2("which", ["chrome-relay"]);
2094
- const newBin = which.stdout?.toString().trim();
2095
- if (which.status === 0 && newBin) {
2096
- const versionOut = spawnSync2(newBin, ["--version"]);
2129
+ const newBin = whichBinary(spawnSync2, "chrome-relay");
2130
+ if (newBin) {
2131
+ const versionOut = spawnCli(spawnSync2, newBin, ["--version"]);
2097
2132
  const newVersion = (versionOut.stdout?.toString() ?? "").trim();
2098
2133
  out.binary.path = newBin;
2099
2134
  if (newVersion && newVersion !== fromVersion) {
2100
2135
  out.updatedTo = newVersion;
2101
- const install2 = spawnSync2(newBin, ["install"], { stdio: "inherit" });
2136
+ const install2 = spawnCli(spawnSync2, newBin, ["install"], { stdio: "inherit" });
2102
2137
  if (install2.status !== 0) {
2103
2138
  out.warnings.push({
2104
2139
  code: "install_refresh_failed",
2105
2140
  message: `Update installed the new package but \`${newBin} install\` exited ${install2.status}. Run it manually to refresh the native host manifest.`
2106
2141
  });
2107
2142
  }
2108
- const rn = spawnSync2(newBin, ["release-notes", "--since", fromVersion]);
2143
+ const rn = spawnCli(spawnSync2, newBin, ["release-notes", "--since", fromVersion]);
2109
2144
  try {
2110
2145
  const parsed = JSON.parse(rn.stdout?.toString() ?? "");
2111
2146
  if (Array.isArray(parsed.changes)) {
@@ -2127,7 +2162,7 @@ function registerInstallUpdate(program) {
2127
2162
  } else {
2128
2163
  out.warnings.push({
2129
2164
  code: "update_not_verified",
2130
- 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.`
2131
2166
  });
2132
2167
  }
2133
2168
  }
@@ -2158,7 +2193,7 @@ function registerNavigation(ctx) {
2158
2193
  await run("get_windows_and_tabs", {});
2159
2194
  });
2160
2195
  tabOpt(
2161
- 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(
2162
2197
  "after",
2163
2198
  `
2164
2199
 
@@ -2168,7 +2203,7 @@ Examples:
2168
2203
  chrome-relay navigate "https://chrome-relay.kushalsm.com" --new # open in a new background tab
2169
2204
  chrome-relay navigate "https://chrome-relay.kushalsm.com" --new --active # open new tab AND show it to the user
2170
2205
 
2171
- By default chrome-relay never steals focus \u2014 navigated tabs (new or
2206
+ By default chrome-relay never steals focus. Navigated tabs (new or
2172
2207
  existing) stay in whatever state they're in. Pass --active when you
2173
2208
  actually want the user looking at the page.
2174
2209
  `
@@ -2217,7 +2252,7 @@ Examples:
2217
2252
  chrome-relay click 'button[aria-label="Save"]'
2218
2253
  chrome-relay click --tab 123 --x 1327 --y 771
2219
2254
 
2220
- 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
2221
2256
  survive DOM churn (backendNodeId + role/name heal). CSS selectors for
2222
2257
  elements you know statically. Coordinates for canvas/SVG chart internals
2223
2258
  where no DOM handle exists. See docs/clicking-strategies.md.
@@ -2270,10 +2305,10 @@ Examples:
2270
2305
  chrome-relay type "appended into already-focused element"
2271
2306
 
2272
2307
  When to pick which:
2273
- fill \u2014 plain <input>, <textarea>, <select>, React-controlled inputs (atomic write).
2274
- type \u2014 contenteditable, Draft.js, Lexical, ProseMirror (trusted text commit).
2275
- keys \u2014 Enter, Tab, Esc, arrows, modifier chords (single key press).
2276
- 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.
2277
2312
  `
2278
2313
  )
2279
2314
  ).action(async (text, opts) => {
@@ -2330,7 +2365,7 @@ import { writeFileSync } from "fs";
2330
2365
  import { structuredPatch } from "diff";
2331
2366
  function printSnapshotDiff(current, prevText) {
2332
2367
  if (prevText === null) {
2333
- 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");
2334
2369
  process.stdout.write(current + "\n");
2335
2370
  return;
2336
2371
  }
@@ -2372,17 +2407,17 @@ function exitWithError(error) {
2372
2407
  function registerCapture(ctx) {
2373
2408
  const { program, withBase, run } = ctx;
2374
2409
  tabOpt(
2375
- 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(
2376
2411
  "after",
2377
2412
  `
2378
2413
 
2379
2414
  Examples:
2380
2415
  chrome-relay snapshot -i # see the page, get @refs
2381
- chrome-relay click @e12 # act on a ref \u2014 no --tab needed
2416
+ chrome-relay click @e12 # act on a ref, no --tab needed
2382
2417
  chrome-relay snapshot -i -s "#main" # scope to a subtree
2383
2418
  chrome-relay snapshot --json # machine-readable envelope
2384
2419
 
2385
- 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
2386
2421
  page changes. Refs carry their own tab and heal across DOM churn
2387
2422
  (backendNodeId fast path + role/name re-find); a dead ref returns
2388
2423
  error.code = stale_ref, which means: re-run snapshot.
@@ -2395,6 +2430,7 @@ error.code = stale_ref, which means: re-run snapshot.
2395
2430
  if (opts.scope) extras.scope = opts.scope;
2396
2431
  if (opts.urls) extras.urls = true;
2397
2432
  if (opts.diff) extras.diff = true;
2433
+ if (opts.elide === false) extras.elide = false;
2398
2434
  try {
2399
2435
  const result = await callTool("chrome_snapshot", withBase(opts, extras));
2400
2436
  if (opts.diff && !opts.json) {
@@ -2408,7 +2444,7 @@ error.code = stale_ref, which means: re-run snapshot.
2408
2444
  }
2409
2445
  });
2410
2446
  tabOpt(
2411
- 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(
2412
2448
  "after",
2413
2449
  `
2414
2450
 
@@ -2450,7 +2486,7 @@ full-tab screenshot when an agent only needs to see one component.
2450
2486
  }
2451
2487
  });
2452
2488
  tabOpt(
2453
- 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")
2454
2490
  ).action(async (opts) => {
2455
2491
  process.stderr.write("[chrome-relay] deprecated: `read` is now an alias for `snapshot` (new output format). Use `chrome-relay snapshot`.\n");
2456
2492
  const extras = {};
@@ -2463,7 +2499,7 @@ full-tab screenshot when an agent only needs to see one component.
2463
2499
  }
2464
2500
  });
2465
2501
  tabOpt(
2466
- 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")
2467
2503
  ).action(async (opts) => {
2468
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");
2469
2505
  const extras = {};
@@ -2476,7 +2512,7 @@ full-tab screenshot when an agent only needs to see one component.
2476
2512
  }
2477
2513
  });
2478
2514
  tabOpt(
2479
- 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(
2480
2516
  "after",
2481
2517
  `
2482
2518
 
@@ -2582,10 +2618,10 @@ Notes:
2582
2618
  if (opts.gif || opts.mp4) {
2583
2619
  const fps = typeof opts.fps === "number" ? opts.fps : 15;
2584
2620
  const { spawnSync: spawnSync2 } = await import("child_process");
2585
- const which = spawnSync2("which", ["ffmpeg"]);
2621
+ const which = spawnSync2(process.platform === "win32" ? "where" : "which", ["ffmpeg"]);
2586
2622
  if (which.status !== 0) {
2587
2623
  if (opts.allowMissingFfmpeg) {
2588
- 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");
2589
2625
  return;
2590
2626
  }
2591
2627
  const { RelayError: RelayError2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
@@ -2651,7 +2687,7 @@ Notes:
2651
2687
  init_dist();
2652
2688
  var CONSOLE_BUFFER_MAX_KB = Math.round(CONSOLE_BUFFER_MAX_BYTES / 1024);
2653
2689
  function netFilterOpts(cmd) {
2654
- 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");
2655
2691
  }
2656
2692
  function netFilterArgs(opts) {
2657
2693
  const a = {};
@@ -2659,6 +2695,7 @@ function netFilterArgs(opts) {
2659
2695
  if (opts.status) a.status = opts.status;
2660
2696
  if (opts.method) a.method = opts.method;
2661
2697
  if (typeof opts.limit === "number") a.limit = opts.limit;
2698
+ if (opts.rawHeaders) a.rawHeaders = true;
2662
2699
  return a;
2663
2700
  }
2664
2701
  function registerSessions(ctx) {
@@ -2799,8 +2836,8 @@ Privacy:
2799
2836
  share with the agent invoking chrome-relay.
2800
2837
 
2801
2838
  Notes:
2802
- Bodies are NOT eagerly buffered \u2014 Chrome GCs response bodies ~30s after
2803
- 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.
2804
2841
  WebSocket frames and SSE streams are out of scope.
2805
2842
  `
2806
2843
  ).action(async (opts) => {
@@ -2812,7 +2849,7 @@ Notes:
2812
2849
  await run("chrome_network", withBase(opts, netFilterArgs(opts)));
2813
2850
  });
2814
2851
  tabOpt(
2815
- 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")
2816
2853
  ).action(async (requestId, opts) => {
2817
2854
  const extras = { action: "body", requestId };
2818
2855
  if (opts.full) extras.full = true;
@@ -2820,7 +2857,7 @@ Notes:
2820
2857
  await run("chrome_network", withBase(opts, extras));
2821
2858
  });
2822
2859
  tabOpt(netFilterOpts(
2823
- 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")
2824
2861
  )).action(async (opts) => {
2825
2862
  const extras = { ...netFilterArgs(opts), action: "har" };
2826
2863
  if (opts.withBodies) extras.withBodies = true;
@@ -2868,11 +2905,11 @@ Notes:
2868
2905
  init_dist();
2869
2906
  import { readFileSync } from "fs";
2870
2907
  function coreSkillText() {
2871
- 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.';
2872
2909
  try {
2873
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();
2874
2911
  } catch {
2875
- 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";
2876
2913
  }
2877
2914
  }
2878
2915
  function exitWithError2(error) {
@@ -2925,7 +2962,7 @@ need a follow-up probe.
2925
2962
  if (typeof opts.timeout === "number") extras.timeoutMs = opts.timeout;
2926
2963
  await run(TOOL_NAMES.WAIT, withBase(opts, extras));
2927
2964
  });
2928
- 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(
2929
2966
  "after",
2930
2967
  `
2931
2968
 
@@ -2988,7 +3025,7 @@ Examples:
2988
3025
 
2989
3026
  One HTTP POST, one native-messaging message, sequential execution in the
2990
3027
  extension. Tool names are the wire names (chrome_navigate, chrome_snapshot,
2991
- chrome_click_element, ... \u2014 see \`chrome-relay call --help\`). Amortizes CLI
3028
+ chrome_click_element, ...; see \`chrome-relay call --help\`). Amortizes CLI
2992
3029
  startup across N actions. Nested batches are rejected.
2993
3030
  `
2994
3031
  ).action(async (json, opts) => {
@@ -3035,7 +3072,7 @@ startup across N actions. Nested batches are rejected.
3035
3072
  exitWithError2(error);
3036
3073
  }
3037
3074
  });
3038
- 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(
3039
3076
  "after",
3040
3077
  `
3041
3078
 
@@ -3043,13 +3080,13 @@ Examples:
3043
3080
  chrome-relay skills # list available skills
3044
3081
  chrome-relay skills get core # print the core usage guide
3045
3082
 
3046
- The same guide is hosted at https://chrome-relay.kushalsm.com/skill.md \u2014
3047
- 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.
3048
3085
  `
3049
3086
  );
3050
3087
  skills.action(async (verb, name) => {
3051
3088
  if (!verb || verb === "list") {
3052
- 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");
3053
3090
  return;
3054
3091
  }
3055
3092
  if (verb === "get" && (name === "core" || name === void 0)) {
@@ -3065,7 +3102,7 @@ the binary copy is authoritative for the version you have installed.
3065
3102
  // src/program.ts
3066
3103
  function buildProgram() {
3067
3104
  const program = new Command();
3068
- 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(
3069
3106
  "after",
3070
3107
  `
3071
3108
 
@@ -3073,7 +3110,7 @@ The core loop:
3073
3110
  chrome-relay tabs
3074
3111
  chrome-relay navigate "https://chrome-relay.kushalsm.com" --new # background tab
3075
3112
  chrome-relay snapshot --tab <tabId> -i # actionable elements get @refs
3076
- chrome-relay click @e12 # act on a ref \u2014 no --tab needed
3113
+ chrome-relay click @e12 # act on a ref, no --tab needed
3077
3114
  chrome-relay fill @e14 "value"
3078
3115
  chrome-relay snapshot --tab <tabId> -i # re-look after the page changes
3079
3116
 
@@ -3087,8 +3124,8 @@ Also:
3087
3124
 
3088
3125
  Notes:
3089
3126
  Refs come from snapshot and carry their own tab. Tools attach via CDP and
3090
- run on backgrounded tabs without stealing focus. Errors are structured \u2014
3091
- 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).
3092
3129
  `
3093
3130
  );
3094
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.0" : "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.0" : "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.0",
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",