chrome-relay 0.2.5 → 0.3.1

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.
Files changed (2) hide show
  1. package/dist/cli.js +172 -12
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -154,7 +154,7 @@ async function callTool(name, args) {
154
154
  // src/program.ts
155
155
  function buildProgram() {
156
156
  const program = new Command();
157
- program.name("chrome-relay").description("Connect your local Chrome browser to coding agents through a local bridge.").version(CHROME_RELAY_VERSION).showHelpAfterError().addHelpText(
157
+ program.name("chrome-relay").description("Connect your local Chrome browser to coding agents through a local bridge.").version(CHROME_RELAY_VERSION).showHelpAfterError().option("--group <name>", "target the active tab of a named group window (works at top level too)").enablePositionalOptions().addHelpText(
158
158
  "after",
159
159
  `
160
160
 
@@ -197,7 +197,14 @@ Notes:
197
197
  }
198
198
  }
199
199
  function tabOpt(cmd) {
200
- return cmd.option("-t, --tab <id>", "target tab ID", (v) => Number(v));
200
+ return cmd.option("-t, --tab <id>", "target tab ID", (v) => Number(v)).option("--group <name>", "target the active tab of a named group window (see `chrome-relay group`)");
201
+ }
202
+ function baseArgs(opts) {
203
+ const args = {};
204
+ if (opts.tab !== void 0) args.tabId = opts.tab;
205
+ const effectiveGroup = opts.group ?? program.opts().group;
206
+ if (effectiveGroup) args.groupName = effectiveGroup;
207
+ return args;
201
208
  }
202
209
  program.command("tabs").description("List open Chrome windows and tabs.").action(async () => {
203
210
  await run("get_windows_and_tabs", {});
@@ -223,7 +230,7 @@ Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate
223
230
  process.exit(1);
224
231
  }
225
232
  const args = { url };
226
- if (opts.tab !== void 0) args.tabId = opts.tab;
233
+ Object.assign(args, baseArgs(opts));
227
234
  if (opts.new) args.newTab = true;
228
235
  if (opts.inactive) args.active = false;
229
236
  await run("chrome_navigate", args);
@@ -247,7 +254,7 @@ full-tab screenshot when an agent only needs to see one component.
247
254
  )
248
255
  ).action(async (opts) => {
249
256
  const args = {};
250
- if (opts.tab !== void 0) args.tabId = opts.tab;
257
+ Object.assign(args, baseArgs(opts));
251
258
  if (opts.full) args.fullPage = true;
252
259
  if (opts.bbox) args.bbox = opts.bbox;
253
260
  if (opts.selector) args.selector = opts.selector;
@@ -276,7 +283,7 @@ full-tab screenshot when an agent only needs to see one component.
276
283
  program.command("read").description("Extract page structure and interactive elements.").option("-i, --interactive", "return only interactive elements")
277
284
  ).action(async (opts) => {
278
285
  const args = {};
279
- if (opts.tab !== void 0) args.tabId = opts.tab;
286
+ Object.assign(args, baseArgs(opts));
280
287
  if (opts.interactive) args.interactiveOnly = true;
281
288
  await run("chrome_read_page", args);
282
289
  });
@@ -284,14 +291,14 @@ full-tab screenshot when an agent only needs to see one component.
284
291
  program.command("click <selector>").description("Click an element by CSS selector.")
285
292
  ).action(async (selector, opts) => {
286
293
  const args = { selector };
287
- if (opts.tab !== void 0) args.tabId = opts.tab;
294
+ Object.assign(args, baseArgs(opts));
288
295
  await run("chrome_click_element", args);
289
296
  });
290
297
  tabOpt(
291
298
  program.command("fill <selector> <value>").description("Fill an input or textarea.")
292
299
  ).action(async (selector, value, opts) => {
293
300
  const args = { selector, value };
294
- if (opts.tab !== void 0) args.tabId = opts.tab;
301
+ Object.assign(args, baseArgs(opts));
295
302
  await run("chrome_fill_or_select", args);
296
303
  });
297
304
  tabOpt(
@@ -310,7 +317,7 @@ For typing text into a field, use \`chrome-relay type\` instead.
310
317
  )
311
318
  ).action(async (keys, opts) => {
312
319
  const args = { keys };
313
- if (opts.tab !== void 0) args.tabId = opts.tab;
320
+ Object.assign(args, baseArgs(opts));
314
321
  await run("chrome_keyboard", args);
315
322
  });
316
323
  tabOpt(
@@ -331,7 +338,7 @@ When to pick which:
331
338
  )
332
339
  ).action(async (text, opts) => {
333
340
  const args = { text };
334
- if (opts.tab !== void 0) args.tabId = opts.tab;
341
+ Object.assign(args, baseArgs(opts));
335
342
  if (opts.selector) args.selector = opts.selector;
336
343
  await run("chrome_type", args);
337
344
  });
@@ -353,7 +360,7 @@ Notes:
353
360
  )
354
361
  ).action(async (code, opts) => {
355
362
  const args = { code };
356
- if (opts.tab !== void 0) args.tabId = opts.tab;
363
+ Object.assign(args, baseArgs(opts));
357
364
  if (typeof opts.timeoutMs === "number") args.timeoutMs = opts.timeoutMs;
358
365
  await run("chrome_evaluate", args);
359
366
  });
@@ -399,19 +406,172 @@ Notes:
399
406
  viewport.command("preset <name>").description("Apply a named device preset (iphone-14, pixel-7, desktop-1440, etc).")
400
407
  ).action(async (name, opts) => {
401
408
  const args = { action: "preset", name };
402
- if (opts.tab !== void 0) args.tabId = opts.tab;
409
+ Object.assign(args, baseArgs(opts));
403
410
  await run("chrome_viewport", args);
404
411
  });
405
412
  tabOpt(
406
413
  viewport.command("clear").description("Drop the viewport override and return the tab to its native size.")
407
414
  ).action(async (opts) => {
408
415
  const args = { action: "clear" };
409
- if (opts.tab !== void 0) args.tabId = opts.tab;
416
+ Object.assign(args, baseArgs(opts));
410
417
  await run("chrome_viewport", args);
411
418
  });
412
419
  viewport.command("list").description("List available presets.").action(async () => {
413
420
  await run("chrome_viewport", { action: "list" });
414
421
  });
422
+ program.command("self-reload").description("Restart the chrome-relay extension's service worker (picks up newly built code).").action(async () => {
423
+ await run("chrome_self_reload", {});
424
+ });
425
+ tabOpt(
426
+ program.command("ax").description("Extract the accessibility tree \u2014 ~30\xD7 smaller than `read` and more semantic.").option("-i, --interactive-only", "filter to actionable roles (button, link, textbox, ...)").option("--root <role>", "start from the first node matching this role (e.g. 'main')").option("--include-subframes", "walk subframes too (default: top frame only)").addHelpText(
427
+ "after",
428
+ `
429
+
430
+ Examples:
431
+ chrome-relay ax --tab 123
432
+ chrome-relay ax --tab 123 --interactive-only
433
+ chrome-relay ax --tab 123 --root main --interactive-only
434
+
435
+ Notes:
436
+ Each node carries an "id" \u2014 that's the backendDOMNodeId. Pass it to
437
+ \`chrome-relay click-ax --node <id>\` to click without a CSS selector.
438
+ `
439
+ )
440
+ ).action(async (opts) => {
441
+ const args = baseArgs(opts);
442
+ if (opts.interactiveOnly) args.interactiveOnly = true;
443
+ if (opts.root) args.rootRole = opts.root;
444
+ if (opts.includeSubframes) args.includeSubframes = true;
445
+ await run("chrome_ax", args);
446
+ });
447
+ tabOpt(
448
+ program.command("click-ax").description("Click an element by its backendDOMNodeId from a previous `ax` call.").requiredOption("--node <id>", "backendDOMNodeId from `chrome-relay ax`", (v) => Number(v)).addHelpText(
449
+ "after",
450
+ `
451
+
452
+ Examples:
453
+ chrome-relay click-ax --tab 123 --node 456
454
+
455
+ Notes:
456
+ Throws explicitly if the node id is stale (page mutated since you called
457
+ \`ax\`). Re-run \`ax\` and pass the fresh id.
458
+ `
459
+ )
460
+ ).action(async (opts) => {
461
+ const args = baseArgs(opts);
462
+ args.node = opts.node;
463
+ await run("chrome_click_ax", args);
464
+ });
465
+ const group = program.command("group").description("Manage named Chrome windows so multiple agents can drive separate windows.").addHelpText(
466
+ "after",
467
+ `
468
+
469
+ Examples:
470
+ chrome-relay group create bidsmith-h01 --url https://reddit.com
471
+ chrome-relay group list
472
+ chrome-relay --group bidsmith-h01 navigate https://news.ycombinator.com
473
+ chrome-relay --group bidsmith-h01 screenshot -o evidence.png
474
+ chrome-relay group close bidsmith-h01
475
+
476
+ Notes:
477
+ Hard lifecycle: if you manually close the group's window, the next
478
+ --group operation fails loudly until you run \`group close\` + \`group create\` again.
479
+ If you pass both --tab and --group on the same command, --tab wins.
480
+ `
481
+ );
482
+ group.command("create <name>").description("Open a new Chrome window and bind it to <name>.").option("--url <url>", "initial URL (default about:blank)").option("--label <label>", "human-readable description shown in popup/list").action(async (name, opts) => {
483
+ const args = { action: "create", name };
484
+ if (opts.url) args.url = opts.url;
485
+ if (opts.label) args.label = opts.label;
486
+ await run("chrome_group", args);
487
+ });
488
+ group.command("list").description("List all known groups + whether their window is still alive.").action(async () => {
489
+ await run("chrome_group", { action: "list" });
490
+ });
491
+ group.command("close <name>").description("Close the group's window (if alive) and remove the binding.").action(async (name) => {
492
+ await run("chrome_group", { action: "close", name });
493
+ });
494
+ const network = program.command("network").description("Capture HTTP request/response metadata. Ring buffer, last 200 per tab.").addHelpText(
495
+ "after",
496
+ `
497
+
498
+ Examples:
499
+ chrome-relay network --tab 123 # last 200 requests
500
+ chrome-relay network --tab 123 --filter api.example.com # url substring
501
+ chrome-relay network --tab 123 --status failed # only failures
502
+ chrome-relay network --tab 123 --method POST
503
+ chrome-relay network --tab 123 --body <requestId> # lazy body fetch
504
+ chrome-relay network --tab 123 har > capture.har # HAR export
505
+ chrome-relay network --tab 123 --clear
506
+
507
+ Privacy:
508
+ Capturing network traffic includes Authorization headers, cookies, and
509
+ request/response bodies. The capture stays in the extension's memory and
510
+ is wiped on tab close. Don't run this on a tab whose state you wouldn't
511
+ share with the agent invoking chrome-relay.
512
+
513
+ Notes:
514
+ Bodies are NOT eagerly buffered \u2014 Chrome GCs response bodies ~30s after
515
+ the request finishes. Use \`--body <id>\` promptly. WebSocket frames and
516
+ SSE streams are out of scope.
517
+ `
518
+ );
519
+ tabOpt(
520
+ network.command("read", { isDefault: true }).description("List captured network entries.").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))
521
+ ).action(async (opts) => {
522
+ const args = baseArgs(opts);
523
+ if (opts.filter) args.filter = opts.filter;
524
+ if (opts.status) args.status = opts.status;
525
+ if (opts.method) args.method = opts.method;
526
+ if (typeof opts.limit === "number") args.limit = opts.limit;
527
+ await run("chrome_network", args);
528
+ });
529
+ tabOpt(
530
+ network.command("body <requestId>").description("Fetch the response body for one request (lazy; may fail if GC'd).")
531
+ ).action(async (requestId, opts) => {
532
+ const args = { ...baseArgs(opts), action: "body", requestId };
533
+ await run("chrome_network", args);
534
+ });
535
+ tabOpt(
536
+ network.command("har").description("Emit HAR-compatible JSON for the captured entries.").option("--filter <substr>", "url substring filter").option("--status <bucket>", "status bucket filter")
537
+ ).action(async (opts) => {
538
+ const args = { ...baseArgs(opts), action: "har" };
539
+ if (opts.filter) args.filter = opts.filter;
540
+ if (opts.status) args.status = opts.status;
541
+ await run("chrome_network", args);
542
+ });
543
+ tabOpt(
544
+ network.command("clear").description("Wipe the network buffer for this tab.")
545
+ ).action(async (opts) => {
546
+ const args = { ...baseArgs(opts), action: "clear" };
547
+ await run("chrome_network", args);
548
+ });
549
+ tabOpt(
550
+ program.command("console").description("Read console.log/warn/error + page exceptions (ring buffer, last 200).").option("--level <levels>", "comma-separated: log,info,warn,error,debug,exception").option("--since <id>", "only return entries with id > since (live-tail-ish)", (v) => Number(v)).option("--limit <n>", "cap response length", (v) => Number(v)).option("--clear", "wipe the buffer (no read)").addHelpText(
551
+ "after",
552
+ `
553
+
554
+ Examples:
555
+ chrome-relay console --tab 123
556
+ chrome-relay console --tab 123 --level error,exception
557
+ chrome-relay console --tab 123 --since 50 # entries newer than id 50 (tail-style polling)
558
+ chrome-relay console --tab 123 --clear
559
+
560
+ Notes:
561
+ Ring buffer holds the last 200 entries per tab (or 256 KB, whichever first).
562
+ Wipes on tab close. First call on a tab subscribes; subsequent calls are
563
+ instant in-memory reads.
564
+ `
565
+ )
566
+ ).action(async (opts) => {
567
+ const args = {};
568
+ if (opts.tab !== void 0) args.tabId = opts.tab;
569
+ if (opts.clear) args.action = "clear";
570
+ if (opts.level) args.levels = opts.level;
571
+ if (typeof opts.since === "number") args.since = opts.since;
572
+ if (typeof opts.limit === "number") args.limit = opts.limit;
573
+ await run("chrome_console", args);
574
+ });
415
575
  return program;
416
576
  }
417
577
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-relay",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -27,7 +27,7 @@
27
27
  "devDependencies": {
28
28
  "tsup": "^8.4.0",
29
29
  "vitest": "^3.0.0",
30
- "@chrome-relay/protocol": "0.2.5"
30
+ "@chrome-relay/protocol": "0.3.0"
31
31
  },
32
32
  "scripts": {
33
33
  "build": "tsup",