chrome-relay 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -442,6 +442,9 @@ function parseChromeSnapshotArgs(input) {
442
442
  const urls = optBool(obj, "urls", TOOL_NAMES.SNAPSHOT);
443
443
  if (urls !== void 0)
444
444
  out.urls = urls;
445
+ const diff = optBool(obj, "diff", TOOL_NAMES.SNAPSHOT);
446
+ if (diff !== void 0)
447
+ out.diff = diff;
445
448
  return out;
446
449
  }
447
450
  function parseChromeScreenshotArgs(input) {
@@ -910,6 +913,170 @@ var init_multi = __esm({
910
913
  }
911
914
  });
912
915
 
916
+ // ../protocol/dist/limits.js
917
+ var DEFAULT_TOOL_CALL_TIMEOUT_MS, DEFAULT_PING_TIMEOUT_MS, DEFAULT_READY_TIMEOUT_MS, DEFAULT_EVAL_TIMEOUT_MS, DEFAULT_BODY_PREVIEW_BYTES, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS, WAIT_POLL_INTERVAL_MS, NETWORKIDLE_QUIET_MS, MAX_BATCH_COMMANDS, MAX_BATCH_BYTES, NETWORK_BUFFER_MAX_ENTRIES, NETWORK_BUFFER_MAX_BYTES, CONSOLE_BUFFER_MAX_ENTRIES, CONSOLE_BUFFER_MAX_BYTES, CONSOLE_ENTRY_TEXT_MAX_CHARS, CONSOLE_ENTRY_STACK_MAX_CHARS;
918
+ var init_limits = __esm({
919
+ "../protocol/dist/limits.js"() {
920
+ "use strict";
921
+ DEFAULT_TOOL_CALL_TIMEOUT_MS = 3e4;
922
+ DEFAULT_PING_TIMEOUT_MS = 2e3;
923
+ DEFAULT_READY_TIMEOUT_MS = 15e3;
924
+ DEFAULT_EVAL_TIMEOUT_MS = 15e3;
925
+ DEFAULT_BODY_PREVIEW_BYTES = 8 * 1024;
926
+ DEFAULT_WAIT_TIMEOUT_MS = 1e4;
927
+ MAX_WAIT_TIMEOUT_MS = 25e3;
928
+ WAIT_POLL_INTERVAL_MS = 100;
929
+ NETWORKIDLE_QUIET_MS = 500;
930
+ MAX_BATCH_COMMANDS = 50;
931
+ MAX_BATCH_BYTES = 9e5;
932
+ NETWORK_BUFFER_MAX_ENTRIES = 200;
933
+ NETWORK_BUFFER_MAX_BYTES = 512 * 1024;
934
+ CONSOLE_BUFFER_MAX_ENTRIES = 200;
935
+ CONSOLE_BUFFER_MAX_BYTES = 256 * 1024;
936
+ CONSOLE_ENTRY_TEXT_MAX_CHARS = 1e3;
937
+ CONSOLE_ENTRY_STACK_MAX_CHARS = 1e3;
938
+ }
939
+ });
940
+
941
+ // ../protocol/dist/args/loop.js
942
+ function parseChromeWaitArgs(input) {
943
+ const obj = asObject(input, TOOL_NAMES.WAIT);
944
+ const target = parseTargetArgs(obj, TOOL_NAMES.WAIT);
945
+ const candidates = [];
946
+ const selector = optString(obj, "selector", TOOL_NAMES.WAIT);
947
+ if (selector)
948
+ candidates.push({ kind: "selector", selector });
949
+ const ref = optString(obj, "ref", TOOL_NAMES.WAIT);
950
+ if (ref)
951
+ candidates.push({ kind: "ref", ref });
952
+ const text = optString(obj, "text", TOOL_NAMES.WAIT);
953
+ if (text)
954
+ candidates.push({ kind: "text", text });
955
+ const urlGlob = optString(obj, "urlGlob", TOOL_NAMES.WAIT);
956
+ if (urlGlob)
957
+ candidates.push({ kind: "url", urlGlob });
958
+ const load = optString(obj, "load", TOOL_NAMES.WAIT);
959
+ if (load) {
960
+ if (!LOAD_STATES.has(load)) {
961
+ throw new RelayError({
962
+ code: "invalid_arguments",
963
+ message: `${TOOL_NAMES.WAIT}: \`load\` must be one of load | domcontentloaded | networkidle (got ${load}).`,
964
+ tool: TOOL_NAMES.WAIT,
965
+ phase: "parse_arguments",
966
+ details: { received: load },
967
+ retryable: false
968
+ });
969
+ }
970
+ candidates.push({ kind: "load", state: load });
971
+ }
972
+ const fn = optString(obj, "fn", TOOL_NAMES.WAIT);
973
+ if (fn)
974
+ candidates.push({ kind: "fn", fn });
975
+ if (candidates.length !== 1) {
976
+ throw new RelayError({
977
+ code: "invalid_arguments",
978
+ message: `${TOOL_NAMES.WAIT}: pass exactly one condition (selector | ref | text | urlGlob | load | fn); got ${candidates.length}.`,
979
+ tool: TOOL_NAMES.WAIT,
980
+ phase: "parse_arguments",
981
+ details: { received: candidates.map((c) => c.kind) },
982
+ retryable: false
983
+ });
984
+ }
985
+ const timeoutRaw = optPositiveNumber(obj, "timeoutMs", TOOL_NAMES.WAIT) ?? DEFAULT_WAIT_TIMEOUT_MS;
986
+ return {
987
+ ...target,
988
+ condition: candidates[0],
989
+ timeoutMs: Math.min(timeoutRaw, MAX_WAIT_TIMEOUT_MS)
990
+ };
991
+ }
992
+ function parseChromeBatchArgs(input) {
993
+ const obj = asObject(input, TOOL_NAMES.BATCH);
994
+ if (!Array.isArray(obj.commands) || obj.commands.length === 0) {
995
+ throw new RelayError({
996
+ code: "invalid_arguments",
997
+ message: `${TOOL_NAMES.BATCH}: \`commands\` must be a non-empty array of { name, args? }.`,
998
+ tool: TOOL_NAMES.BATCH,
999
+ phase: "parse_arguments",
1000
+ details: { received: obj.commands },
1001
+ retryable: false
1002
+ });
1003
+ }
1004
+ if (obj.commands.length > MAX_BATCH_COMMANDS) {
1005
+ throw new RelayError({
1006
+ code: "invalid_arguments",
1007
+ message: `${TOOL_NAMES.BATCH}: at most ${MAX_BATCH_COMMANDS} commands per batch (got ${obj.commands.length}).`,
1008
+ tool: TOOL_NAMES.BATCH,
1009
+ phase: "parse_arguments",
1010
+ details: { received: obj.commands.length },
1011
+ retryable: false
1012
+ });
1013
+ }
1014
+ const commands = obj.commands.map((c, i) => {
1015
+ const cmd = asObject(c, TOOL_NAMES.BATCH);
1016
+ const name = requireString(cmd, "name", TOOL_NAMES.BATCH);
1017
+ if (name === TOOL_NAMES.BATCH) {
1018
+ throw new RelayError({
1019
+ code: "invalid_arguments",
1020
+ message: `${TOOL_NAMES.BATCH}: command ${i} nests a batch inside a batch \u2014 not supported.`,
1021
+ tool: TOOL_NAMES.BATCH,
1022
+ phase: "parse_arguments",
1023
+ details: { index: i },
1024
+ retryable: false
1025
+ });
1026
+ }
1027
+ return { name, args: cmd.args ?? {} };
1028
+ });
1029
+ return { commands, bail: optBool(obj, "bail", TOOL_NAMES.BATCH) ?? true };
1030
+ }
1031
+ function parseChromeGetArgs(input) {
1032
+ const obj = asObject(input, TOOL_NAMES.GET);
1033
+ const target = parseTargetArgs(obj, TOOL_NAMES.GET);
1034
+ const what = requireString(obj, "what", TOOL_NAMES.GET);
1035
+ if (!GET_WHATS.has(what)) {
1036
+ throw new RelayError({
1037
+ code: "invalid_arguments",
1038
+ message: `${TOOL_NAMES.GET}: \`what\` must be one of text | value | attr | count | title | url (got ${what}).`,
1039
+ tool: TOOL_NAMES.GET,
1040
+ phase: "parse_arguments",
1041
+ details: { received: what },
1042
+ retryable: false
1043
+ });
1044
+ }
1045
+ if (what === "title" || what === "url") {
1046
+ return { ...target, what };
1047
+ }
1048
+ if (what === "count") {
1049
+ return { ...target, what, selector: requireString(obj, "selector", TOOL_NAMES.GET) };
1050
+ }
1051
+ const selector = optString(obj, "selector", TOOL_NAMES.GET);
1052
+ const ref = optString(obj, "ref", TOOL_NAMES.GET);
1053
+ if ((selector ? 1 : 0) + (ref ? 1 : 0) !== 1) {
1054
+ throw new RelayError({
1055
+ code: "invalid_arguments",
1056
+ message: `${TOOL_NAMES.GET} ${what}: pass exactly one of \`selector\` or \`ref\`.`,
1057
+ tool: TOOL_NAMES.GET,
1058
+ phase: "parse_arguments",
1059
+ details: { received: { selector, ref } },
1060
+ retryable: false
1061
+ });
1062
+ }
1063
+ if (what === "attr") {
1064
+ return { ...target, what, attrName: requireString(obj, "attrName", TOOL_NAMES.GET), selector, ref };
1065
+ }
1066
+ return { ...target, what, selector, ref };
1067
+ }
1068
+ var LOAD_STATES, GET_WHATS;
1069
+ var init_loop = __esm({
1070
+ "../protocol/dist/args/loop.js"() {
1071
+ "use strict";
1072
+ init_limits();
1073
+ init_dist();
1074
+ init_shared();
1075
+ LOAD_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
1076
+ GET_WHATS = /* @__PURE__ */ new Set(["text", "value", "attr", "count", "title", "url"]);
1077
+ }
1078
+ });
1079
+
913
1080
  // ../protocol/dist/args/index.js
914
1081
  function parseToolArgs(name, input) {
915
1082
  switch (name) {
@@ -957,6 +1124,12 @@ function parseToolArgs(name, input) {
957
1124
  return parseChromeScreencastArgs(input);
958
1125
  case "chrome_snapshot":
959
1126
  return parseChromeSnapshotArgs(input);
1127
+ case "chrome_wait":
1128
+ return parseChromeWaitArgs(input);
1129
+ case "chrome_batch":
1130
+ return parseChromeBatchArgs(input);
1131
+ case "chrome_get":
1132
+ return parseChromeGetArgs(input);
960
1133
  }
961
1134
  const exhaustive = name;
962
1135
  return exhaustive;
@@ -970,33 +1143,16 @@ var init_args = __esm({
970
1143
  init_network();
971
1144
  init_simple();
972
1145
  init_multi();
1146
+ init_loop();
973
1147
  init_navigate();
974
1148
  init_hover();
975
1149
  init_network();
976
1150
  init_simple();
1151
+ init_loop();
977
1152
  init_multi();
978
1153
  }
979
1154
  });
980
1155
 
981
- // ../protocol/dist/limits.js
982
- var DEFAULT_TOOL_CALL_TIMEOUT_MS, DEFAULT_PING_TIMEOUT_MS, DEFAULT_READY_TIMEOUT_MS, DEFAULT_EVAL_TIMEOUT_MS, DEFAULT_BODY_PREVIEW_BYTES, NETWORK_BUFFER_MAX_ENTRIES, NETWORK_BUFFER_MAX_BYTES, CONSOLE_BUFFER_MAX_ENTRIES, CONSOLE_BUFFER_MAX_BYTES, CONSOLE_ENTRY_TEXT_MAX_CHARS, CONSOLE_ENTRY_STACK_MAX_CHARS;
983
- var init_limits = __esm({
984
- "../protocol/dist/limits.js"() {
985
- "use strict";
986
- DEFAULT_TOOL_CALL_TIMEOUT_MS = 3e4;
987
- DEFAULT_PING_TIMEOUT_MS = 2e3;
988
- DEFAULT_READY_TIMEOUT_MS = 15e3;
989
- DEFAULT_EVAL_TIMEOUT_MS = 15e3;
990
- DEFAULT_BODY_PREVIEW_BYTES = 8 * 1024;
991
- NETWORK_BUFFER_MAX_ENTRIES = 200;
992
- NETWORK_BUFFER_MAX_BYTES = 512 * 1024;
993
- CONSOLE_BUFFER_MAX_ENTRIES = 200;
994
- CONSOLE_BUFFER_MAX_BYTES = 256 * 1024;
995
- CONSOLE_ENTRY_TEXT_MAX_CHARS = 1e3;
996
- CONSOLE_ENTRY_STACK_MAX_CHARS = 1e3;
997
- }
998
- });
999
-
1000
1156
  // ../protocol/dist/snapshot.js
1001
1157
  function parseRefToken(input) {
1002
1158
  const m = REF_TOKEN.exec(input.trim());
@@ -1085,13 +1241,19 @@ __export(dist_exports, {
1085
1241
  DEFAULT_PING_TIMEOUT_MS: () => DEFAULT_PING_TIMEOUT_MS,
1086
1242
  DEFAULT_READY_TIMEOUT_MS: () => DEFAULT_READY_TIMEOUT_MS,
1087
1243
  DEFAULT_TOOL_CALL_TIMEOUT_MS: () => DEFAULT_TOOL_CALL_TIMEOUT_MS,
1244
+ DEFAULT_WAIT_TIMEOUT_MS: () => DEFAULT_WAIT_TIMEOUT_MS,
1088
1245
  LEGACY_DEV_EXTENSION_ID: () => LEGACY_DEV_EXTENSION_ID,
1089
1246
  LOCAL_UNPACKED_EXTENSION_ID: () => LOCAL_UNPACKED_EXTENSION_ID,
1247
+ MAX_BATCH_BYTES: () => MAX_BATCH_BYTES,
1248
+ MAX_BATCH_COMMANDS: () => MAX_BATCH_COMMANDS,
1249
+ MAX_WAIT_TIMEOUT_MS: () => MAX_WAIT_TIMEOUT_MS,
1090
1250
  NATIVE_HOST_NAME: () => NATIVE_HOST_NAME,
1251
+ NETWORKIDLE_QUIET_MS: () => NETWORKIDLE_QUIET_MS,
1091
1252
  NETWORK_BUFFER_MAX_BYTES: () => NETWORK_BUFFER_MAX_BYTES,
1092
1253
  NETWORK_BUFFER_MAX_ENTRIES: () => NETWORK_BUFFER_MAX_ENTRIES,
1093
1254
  RelayError: () => RelayError,
1094
1255
  TOOL_NAMES: () => TOOL_NAMES,
1256
+ WAIT_POLL_INTERVAL_MS: () => WAIT_POLL_INTERVAL_MS,
1095
1257
  asObject: () => asObject,
1096
1258
  coerceTabId: () => coerceTabId,
1097
1259
  formatRefToken: () => formatRefToken,
@@ -1101,12 +1263,14 @@ __export(dist_exports, {
1101
1263
  optPositiveNumber: () => optPositiveNumber,
1102
1264
  optString: () => optString,
1103
1265
  parseChromeAxArgs: () => parseChromeAxArgs,
1266
+ parseChromeBatchArgs: () => parseChromeBatchArgs,
1104
1267
  parseChromeClickArgs: () => parseChromeClickArgs,
1105
1268
  parseChromeClickAxArgs: () => parseChromeClickAxArgs,
1106
1269
  parseChromeCloseTabsArgs: () => parseChromeCloseTabsArgs,
1107
1270
  parseChromeConsoleArgs: () => parseChromeConsoleArgs,
1108
1271
  parseChromeEvaluateArgs: () => parseChromeEvaluateArgs,
1109
1272
  parseChromeFillArgs: () => parseChromeFillArgs,
1273
+ parseChromeGetArgs: () => parseChromeGetArgs,
1110
1274
  parseChromeGroupArgs: () => parseChromeGroupArgs,
1111
1275
  parseChromeHoverArgs: () => parseChromeHoverArgs,
1112
1276
  parseChromeKeyboardArgs: () => parseChromeKeyboardArgs,
@@ -1120,6 +1284,7 @@ __export(dist_exports, {
1120
1284
  parseChromeSwitchTabArgs: () => parseChromeSwitchTabArgs,
1121
1285
  parseChromeTypeArgs: () => parseChromeTypeArgs,
1122
1286
  parseChromeViewportArgs: () => parseChromeViewportArgs,
1287
+ parseChromeWaitArgs: () => parseChromeWaitArgs,
1123
1288
  parseChromeWorkspaceArgs: () => parseChromeWorkspaceArgs,
1124
1289
  parseGetWindowsAndTabsArgs: () => parseGetWindowsAndTabsArgs,
1125
1290
  parseRefToken: () => parseRefToken,
@@ -1209,7 +1374,17 @@ var init_dist = __esm({
1209
1374
  // Unified page snapshot (adoption-spec Change 1) — AX tree + cursor-
1210
1375
  // interactive sweep, one ref space, compact text rendered CLI-side.
1211
1376
  // Supersedes chrome_read_page and chrome_ax, which now alias to it.
1212
- SNAPSHOT: "chrome_snapshot"
1377
+ SNAPSHOT: "chrome_snapshot",
1378
+ // Adoption-spec Change 3 — block until a condition holds (selector/@ref
1379
+ // visible, text present, URL glob, load state, JS truthy).
1380
+ WAIT: "chrome_wait",
1381
+ // Adoption-spec Change 5 — run N tool calls in one round-trip,
1382
+ // sequentially, bail-on-error by default. Amortizes CLI startup + the
1383
+ // HTTP/native-messaging hop.
1384
+ BATCH: "chrome_batch",
1385
+ // Adoption-spec Change 6 — one value (text/value/attr/count/title/url)
1386
+ // without paying for a full snapshot.
1387
+ GET: "chrome_get"
1213
1388
  };
1214
1389
  RelayError = class extends Error {
1215
1390
  code;
@@ -1244,7 +1419,7 @@ var init_dist = __esm({
1244
1419
  import { Command } from "commander";
1245
1420
 
1246
1421
  // src/index.ts
1247
- var CHROME_RELAY_VERSION = true ? "0.6.0" : "0.0.0-dev";
1422
+ var CHROME_RELAY_VERSION = true ? "0.7.0" : "0.0.0-dev";
1248
1423
 
1249
1424
  // src/commands/shared.ts
1250
1425
  init_dist();
@@ -1681,6 +1856,15 @@ async function runDoctor() {
1681
1856
 
1682
1857
  // src/release-notes.ts
1683
1858
  var RELEASE_NOTES = {
1859
+ "0.7.0": [
1860
+ "`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
+ "`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.",
1862
+ "`get text|value|attr|count|title|url <target>` \u2014 one value, plain on stdout, no snapshot. Targets are @refs or CSS selectors; built for $(...) substitution.",
1863
+ "`batch` \u2014 run up to 50 tool calls in ONE round-trip (one HTTP POST, one native-messaging message, sequential in the extension). Bail-on-error by default, `--no-bail` to continue; per-command result envelopes; nested batches rejected.",
1864
+ "`skills get core` \u2014 the agent playbook is now inlined in the binary, always version-matched. `chrome-relay skills` lists; the same guide is hosted at https://chrome-relay.kushalsm.com/skill.md.",
1865
+ "Top-level --help rewritten around the snapshot -> @ref loop (the old read -> selector flow was stale). Example URLs across help/docs now point at real pages.",
1866
+ "Extension + CLI must both be 0.7.0 for wait/get/batch/--diff \u2014 an older extension returns unsupported_tool (update at chrome://extensions)."
1867
+ ],
1684
1868
  "0.6.0": [
1685
1869
  "Unified `snapshot` with actionable @refs. `chrome-relay snapshot -i` renders the page as compact text (~4-5x smaller than the old `read -i`; 14 KB on the HN front page) \u2014 accessibility tree merged with a cursor-interactive sweep that catches div-soup clickables (cursor:pointer, onclick, tabindex, contenteditable) the AX tree misses. Every element gets a browser-unique @eN ref.",
1686
1870
  'Refs are actionable everywhere: `click @e12`, `fill @e14 "v"`, `hover @e3`, `type -s @e7`. A ref carries its tab \u2014 no --tab needed, a contradicting --tab is target_conflict, so an agent can never click into the page the user is reading. Resolution is backendNodeId fast-path with role+name+nth healing on same-page DOM churn (healed clicks report `healed: true`). Refs reach inside shadow DOM, where CSS selectors can\'t.',
@@ -1979,10 +2163,10 @@ function registerNavigation(ctx) {
1979
2163
  `
1980
2164
 
1981
2165
  Examples:
1982
- chrome-relay navigate "https://example.com" # navigate current tab
1983
- chrome-relay navigate --tab 123 "https://example.com" # navigate an existing tab
1984
- chrome-relay navigate "https://example.com" --new # open in a new background tab
1985
- chrome-relay navigate "https://example.com" --new --active # open new tab AND show it to the user
2166
+ chrome-relay navigate "https://chrome-relay.kushalsm.com" # navigate current tab
2167
+ chrome-relay navigate --tab 123 "https://chrome-relay.kushalsm.com" # navigate an existing tab
2168
+ chrome-relay navigate "https://chrome-relay.kushalsm.com" --new # open in a new background tab
2169
+ chrome-relay navigate "https://chrome-relay.kushalsm.com" --new --active # open new tab AND show it to the user
1986
2170
 
1987
2171
  By default chrome-relay never steals focus \u2014 navigated tabs (new or
1988
2172
  existing) stay in whatever state they're in. Pass --active when you
@@ -1993,7 +2177,7 @@ actually want the user looking at the page.
1993
2177
  if (/^\d+$/.test(url)) {
1994
2178
  process.stderr.write(
1995
2179
  `navigate expects a URL, but "${url}" looks like a tab ID.
1996
- Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate --tab ${url} https://example.com" to navigate it.
2180
+ Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate --tab ${url} https://chrome-relay.kushalsm.com" to navigate it.
1997
2181
  `
1998
2182
  );
1999
2183
  process.exit(1);
@@ -2052,8 +2236,8 @@ where no DOM handle exists. See docs/clicking-strategies.md.
2052
2236
  `
2053
2237
 
2054
2238
  Examples:
2055
- chrome-relay fill @e4 "kushal@example.com"
2056
- chrome-relay fill 'input[name="email"]' "kushal@example.com"
2239
+ chrome-relay fill @e4 "kushal@kushalsm.com"
2240
+ chrome-relay fill 'input[name="email"]' "kushal@kushalsm.com"
2057
2241
  `
2058
2242
  )
2059
2243
  ).action(async (target, value, opts) => {
@@ -2143,6 +2327,32 @@ tooltip appearance, etc.) that a bare click would skip past too quickly.
2143
2327
  // src/commands/capture.ts
2144
2328
  init_dist();
2145
2329
  import { writeFileSync } from "fs";
2330
+ import { structuredPatch } from "diff";
2331
+ function printSnapshotDiff(current, prevText) {
2332
+ if (prevText === null) {
2333
+ process.stderr.write("[chrome-relay] no previous snapshot for this tab \u2014 showing full output.\n");
2334
+ process.stdout.write(current + "\n");
2335
+ return;
2336
+ }
2337
+ if (prevText === current) {
2338
+ process.stdout.write("no changes since last snapshot\n");
2339
+ return;
2340
+ }
2341
+ const patch = structuredPatch("prev", "current", prevText, current, "", "", { context: 3 });
2342
+ let added = 0;
2343
+ let removed = 0;
2344
+ for (const hunk of patch.hunks) {
2345
+ process.stdout.write(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@
2346
+ `);
2347
+ for (const line of hunk.lines) {
2348
+ if (line.startsWith("+")) added++;
2349
+ else if (line.startsWith("-")) removed++;
2350
+ process.stdout.write(line + "\n");
2351
+ }
2352
+ }
2353
+ process.stdout.write(`${added} addition${added === 1 ? "" : "s"}, ${removed} removal${removed === 1 ? "" : "s"}
2354
+ `);
2355
+ }
2146
2356
  function printSnapshot(result, asJson) {
2147
2357
  if (asJson) {
2148
2358
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
@@ -2162,7 +2372,7 @@ function exitWithError(error) {
2162
2372
  function registerCapture(ctx) {
2163
2373
  const { program, withBase, run } = ctx;
2164
2374
  tabOpt(
2165
- 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("--json", "structured output: { title, url, tabId, nodes, refs }").addHelpText(
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(
2166
2376
  "after",
2167
2377
  `
2168
2378
 
@@ -2184,8 +2394,14 @@ error.code = stale_ref, which means: re-run snapshot.
2184
2394
  if (typeof opts.depth === "number") extras.depth = opts.depth;
2185
2395
  if (opts.scope) extras.scope = opts.scope;
2186
2396
  if (opts.urls) extras.urls = true;
2397
+ if (opts.diff) extras.diff = true;
2187
2398
  try {
2188
2399
  const result = await callTool("chrome_snapshot", withBase(opts, extras));
2400
+ if (opts.diff && !opts.json) {
2401
+ const data = result;
2402
+ printSnapshotDiff(renderSnapshot(data), data.prevText ?? null);
2403
+ return;
2404
+ }
2189
2405
  printSnapshot(result, opts.json === true);
2190
2406
  } catch (error) {
2191
2407
  exitWithError(error);
@@ -2568,7 +2784,7 @@ Notes:
2568
2784
 
2569
2785
  Examples:
2570
2786
  chrome-relay network --tab 123 # last ${NETWORK_BUFFER_MAX_ENTRIES} requests
2571
- chrome-relay network --tab 123 --filter api.example.com # url substring
2787
+ chrome-relay network --tab 123 --filter api.kushalsm.com # url substring
2572
2788
  chrome-relay network --tab 123 --status failed
2573
2789
  chrome-relay network --tab 123 --method POST
2574
2790
  chrome-relay network body <requestId> --tab 123 # lazy body fetch
@@ -2648,27 +2864,231 @@ Notes:
2648
2864
  });
2649
2865
  }
2650
2866
 
2867
+ // src/commands/loop.ts
2868
+ init_dist();
2869
+ import { readFileSync } from "fs";
2870
+ 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.';
2872
+ try {
2873
+ 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
+ } catch {
2875
+ return "core skill unavailable in this build \u2014 see https://chrome-relay.kushalsm.com/skill.md";
2876
+ }
2877
+ }
2878
+ function exitWithError2(error) {
2879
+ if (error instanceof RelayError) {
2880
+ process.stderr.write(error.message + "\n");
2881
+ process.stderr.write(JSON.stringify({ relayError: error.toBridgeError() }, null, 2) + "\n");
2882
+ } else {
2883
+ process.stderr.write((error instanceof Error ? error.message : String(error)) + "\n");
2884
+ }
2885
+ process.exit(1);
2886
+ }
2887
+ function registerLoop(ctx) {
2888
+ const { program, withBase, run } = ctx;
2889
+ tabOpt(
2890
+ program.command("wait [target]").description("Block until a condition holds: a selector/@ref is visible, text appears, the URL matches, the page loads, or a JS expression is truthy. Pass a number to just sleep.").option("--text <s>", "body text contains <s>").option("--url <glob>", "URL matches glob (** crosses /, * doesn't)").option("--load <state>", "load | domcontentloaded | networkidle").option("--fn <js>", "JS expression in the page; waits until truthy").option("--timeout <ms>", "max wait (default 10000, capped 25000)", (v) => Number(v)).addHelpText(
2891
+ "after",
2892
+ `
2893
+
2894
+ Examples:
2895
+ chrome-relay wait @e12 # ref resolves and has a box
2896
+ chrome-relay wait ".results" --tab 42 # selector exists and visible
2897
+ chrome-relay wait --text "Welcome" --tab 42
2898
+ chrome-relay wait --url "**/dashboard" --tab 42
2899
+ chrome-relay wait --load networkidle --tab 42
2900
+ chrome-relay wait --fn "window.__APP_READY === true" --tab 42
2901
+ chrome-relay wait 1500 # plain sleep, no tab needed
2902
+
2903
+ Exactly one condition per call. On timeout the error includes the page's
2904
+ current state (url, readyState, whether the selector exists) so you don't
2905
+ need a follow-up probe.
2906
+ `
2907
+ )
2908
+ ).action(async (target, opts) => {
2909
+ if (target && /^\d+$/.test(target)) {
2910
+ const ms = Number(target);
2911
+ await new Promise((r) => setTimeout(r, ms));
2912
+ process.stdout.write(JSON.stringify({ satisfied: true, sleptMs: ms }) + "\n");
2913
+ return;
2914
+ }
2915
+ const extras = {};
2916
+ if (target) {
2917
+ const ref = parseRefToken(target);
2918
+ if (ref) extras.ref = ref;
2919
+ else extras.selector = target;
2920
+ }
2921
+ if (opts.text) extras.text = opts.text;
2922
+ if (opts.url) extras.urlGlob = opts.url;
2923
+ if (opts.load) extras.load = opts.load;
2924
+ if (opts.fn) extras.fn = opts.fn;
2925
+ if (typeof opts.timeout === "number") extras.timeoutMs = opts.timeout;
2926
+ await run(TOOL_NAMES.WAIT, withBase(opts, extras));
2927
+ });
2928
+ const get = program.command("get").description("One value, plain to stdout \u2014 no full snapshot.").addHelpText(
2929
+ "after",
2930
+ `
2931
+
2932
+ Examples:
2933
+ chrome-relay get text @e12
2934
+ chrome-relay get value 'input[name="email"]' --tab 42
2935
+ chrome-relay get attr @e7 href
2936
+ chrome-relay get count ".result" --tab 42
2937
+ chrome-relay get title --tab 42
2938
+ chrome-relay get url --tab 42
2939
+ `
2940
+ );
2941
+ const printValue = async (args) => {
2942
+ try {
2943
+ const result = await callTool(TOOL_NAMES.GET, args);
2944
+ const v = result.value;
2945
+ process.stdout.write((v === null || v === void 0 ? "" : String(v)) + "\n");
2946
+ } catch (error) {
2947
+ exitWithError2(error);
2948
+ }
2949
+ };
2950
+ const addressArgs = (target) => {
2951
+ const ref = parseRefToken(target);
2952
+ return ref ? { ref } : { selector: target };
2953
+ };
2954
+ for (const what of ["text", "value"]) {
2955
+ tabOpt(
2956
+ get.command(`${what} <target>`).description(`${what === "text" ? "Visible text" : "Input value"} of a @ref or CSS selector.`)
2957
+ ).action(async (target, opts) => {
2958
+ await printValue(withBase(opts, { what, ...addressArgs(target) }));
2959
+ });
2960
+ }
2961
+ tabOpt(
2962
+ get.command("attr <target> <name>").description("Attribute value of a @ref or CSS selector.")
2963
+ ).action(async (target, name, opts) => {
2964
+ await printValue(withBase(opts, { what: "attr", attrName: name, ...addressArgs(target) }));
2965
+ });
2966
+ tabOpt(get.command("count <selector>").description("Number of elements matching a CSS selector.")).action(
2967
+ async (selector, opts) => {
2968
+ await printValue(withBase(opts, { what: "count", selector }));
2969
+ }
2970
+ );
2971
+ tabOpt(get.command("title").description("Page title.")).action(async (opts) => {
2972
+ await printValue(withBase(opts, { what: "title" }));
2973
+ });
2974
+ tabOpt(get.command("url").description("Page URL.")).action(async (opts) => {
2975
+ await printValue(withBase(opts, { what: "url" }));
2976
+ });
2977
+ program.command("batch [json]").description("Run multiple tool calls in ONE round-trip, sequentially. JSON array of {name, args}, inline or via --stdin.").option("--stdin", "read the JSON array from stdin").option("--no-bail", "keep going after a failed command (default stops at first error)").addHelpText(
2978
+ "after",
2979
+ `
2980
+
2981
+ Examples:
2982
+ chrome-relay batch '[
2983
+ {"name":"chrome_navigate","args":{"url":"https://chrome-relay.kushalsm.com","newTab":true}},
2984
+ {"name":"chrome_wait","args":{"load":"load"}},
2985
+ {"name":"chrome_snapshot","args":{"interactiveOnly":true}}
2986
+ ]'
2987
+ cat commands.json | chrome-relay batch --stdin
2988
+
2989
+ One HTTP POST, one native-messaging message, sequential execution in the
2990
+ extension. Tool names are the wire names (chrome_navigate, chrome_snapshot,
2991
+ chrome_click_element, ... \u2014 see \`chrome-relay call --help\`). Amortizes CLI
2992
+ startup across N actions. Nested batches are rejected.
2993
+ `
2994
+ ).action(async (json, opts) => {
2995
+ try {
2996
+ let raw = json;
2997
+ if (opts.stdin) {
2998
+ raw = readFileSync(0, "utf8");
2999
+ }
3000
+ if (!raw) {
3001
+ throw new RelayError({
3002
+ code: "invalid_arguments",
3003
+ message: "chrome-relay batch: pass a JSON array inline or via --stdin.",
3004
+ tool: TOOL_NAMES.BATCH,
3005
+ phase: "parse_arguments",
3006
+ retryable: false
3007
+ });
3008
+ }
3009
+ if (Buffer.byteLength(raw, "utf8") > MAX_BATCH_BYTES) {
3010
+ throw new RelayError({
3011
+ code: "invalid_arguments",
3012
+ message: `chrome-relay batch: input exceeds ${MAX_BATCH_BYTES} bytes (native-messaging frame safety). Split the batch.`,
3013
+ tool: TOOL_NAMES.BATCH,
3014
+ phase: "parse_arguments",
3015
+ retryable: false
3016
+ });
3017
+ }
3018
+ let commands;
3019
+ try {
3020
+ commands = JSON.parse(raw);
3021
+ } catch (e) {
3022
+ throw new RelayError({
3023
+ code: "invalid_arguments",
3024
+ message: `chrome-relay batch: input is not valid JSON (${e instanceof Error ? e.message : e}).`,
3025
+ tool: TOOL_NAMES.BATCH,
3026
+ phase: "parse_arguments",
3027
+ retryable: false
3028
+ });
3029
+ }
3030
+ const result = await callTool(TOOL_NAMES.BATCH, { commands, bail: opts.bail !== false });
3031
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
3032
+ const failed = result.results?.some((r) => !r.ok);
3033
+ if (failed) process.exit(1);
3034
+ } catch (error) {
3035
+ exitWithError2(error);
3036
+ }
3037
+ });
3038
+ const skills = program.command("skills [verb] [name]").description("Agent playbooks shipped inside the CLI \u2014 always version-matched to the binary.").addHelpText(
3039
+ "after",
3040
+ `
3041
+
3042
+ Examples:
3043
+ chrome-relay skills # list available skills
3044
+ chrome-relay skills get core # print the core usage guide
3045
+
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.
3048
+ `
3049
+ );
3050
+ skills.action(async (verb, name) => {
3051
+ if (!verb || verb === "list") {
3052
+ process.stdout.write("core \u2014 the Chrome Relay agent playbook (snapshot/@ref loop, text-tool table, gotchas)\n");
3053
+ return;
3054
+ }
3055
+ if (verb === "get" && (name === "core" || name === void 0)) {
3056
+ process.stdout.write(coreSkillText() + "\n");
3057
+ return;
3058
+ }
3059
+ process.stderr.write(`Unknown skill command: ${verb}${name ? ` ${name}` : ""}. Try \`chrome-relay skills\` or \`chrome-relay skills get core\`.
3060
+ `);
3061
+ process.exit(1);
3062
+ });
3063
+ }
3064
+
2651
3065
  // src/program.ts
2652
3066
  function buildProgram() {
2653
3067
  const program = new Command();
2654
- program.name("chrome-relay").description("Connect your local Chrome browser to coding agents through a local bridge.").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(
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(
2655
3069
  "after",
2656
3070
  `
2657
3071
 
2658
- Common agent flow:
3072
+ The core loop:
2659
3073
  chrome-relay tabs
2660
- chrome-relay navigate --tab <tabId> "https://example.com"
2661
- chrome-relay read --tab <tabId> -i
2662
- chrome-relay click --tab <tabId> "<selector>"
2663
- chrome-relay fill --tab <tabId> "<selector>" "value"
2664
- chrome-relay type --tab <tabId> -s "<selector>" "text into rich editor"
3074
+ chrome-relay navigate "https://chrome-relay.kushalsm.com" --new # background tab
3075
+ chrome-relay snapshot --tab <tabId> -i # actionable elements get @refs
3076
+ chrome-relay click @e12 # act on a ref \u2014 no --tab needed
3077
+ chrome-relay fill @e14 "value"
3078
+ chrome-relay snapshot --tab <tabId> -i # re-look after the page changes
3079
+
3080
+ Also:
3081
+ chrome-relay wait --tab <tabId> --text "Welcome" # selector/@ref/text/url/load/fn
3082
+ chrome-relay get text @e12 # one value, no full snapshot
2665
3083
  chrome-relay keys --tab <tabId> Enter
2666
3084
  chrome-relay js --tab <tabId> "return document.title"
2667
3085
  chrome-relay screenshot --tab <tabId> -o evidence.png
3086
+ chrome-relay skills get core # the agent playbook, version-matched
2668
3087
 
2669
3088
  Notes:
2670
- navigate takes a URL. Use --tab to target an existing tab.
2671
- Tools attach via CDP and run on backgrounded tabs without stealing focus.
3089
+ 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).
2672
3092
  `
2673
3093
  );
2674
3094
  const baseArgs = makeBaseArgs(program);
@@ -2683,6 +3103,7 @@ Notes:
2683
3103
  registerInput(ctx);
2684
3104
  registerCapture(ctx);
2685
3105
  registerSessions(ctx);
3106
+ registerLoop(ctx);
2686
3107
  return program;
2687
3108
  }
2688
3109
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- var CHROME_RELAY_VERSION = true ? "0.6.0" : "0.0.0-dev";
2
+ var CHROME_RELAY_VERSION = true ? "0.7.0" : "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.6.0" : "0.0.0-dev";
59
+ var CHROME_RELAY_VERSION = true ? "0.7.0" : "0.0.0-dev";
60
60
 
61
61
  // src/release-notes.ts
62
62
  function compareSemver(a, b) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-relay",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,16 +16,24 @@
16
16
  "typecheck": "tsc -p tsconfig.json --noEmit",
17
17
  "test": "vitest run"
18
18
  },
19
- "description": "Connect your local Chrome browser to coding agents through a local bridge.",
20
- "keywords": ["chrome", "browser", "automation", "agents", "native-messaging"],
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
+ "keywords": [
21
+ "chrome",
22
+ "browser",
23
+ "automation",
24
+ "agents",
25
+ "native-messaging"
26
+ ],
21
27
  "license": "MIT",
22
28
  "dependencies": {
23
29
  "chalk": "^5.4.1",
24
30
  "commander": "^13.1.0",
31
+ "diff": "^7.0.0",
25
32
  "fastify": "^5.3.2"
26
33
  },
27
34
  "devDependencies": {
28
35
  "@chrome-relay/protocol": "workspace:*",
36
+ "@types/diff": "^7",
29
37
  "tsup": "^8.4.0",
30
38
  "vitest": "^3.0.0"
31
39
  }