chrome-relay 0.5.12 → 0.5.14

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
@@ -37,26 +37,60 @@ function requireString(obj, key, tool) {
37
37
  }
38
38
  return v;
39
39
  }
40
- function optString(obj, key) {
40
+ function rejectWrongType(obj, key, expected, tool) {
41
+ throw new RelayError({
42
+ code: "invalid_arguments",
43
+ message: `${tool ?? "<unknown tool>"}: \`${key}\` must be ${expected} (got ${typeof obj[key]}).`,
44
+ tool,
45
+ phase: "parse_arguments",
46
+ details: { field: key, expected, received: obj[key] },
47
+ retryable: false
48
+ });
49
+ }
50
+ function optString(obj, key, tool) {
41
51
  const v = obj[key];
42
- return typeof v === "string" && v ? v : void 0;
52
+ if (v === void 0 || v === null)
53
+ return void 0;
54
+ if (typeof v !== "string")
55
+ rejectWrongType(obj, key, "a string", tool);
56
+ return v || void 0;
43
57
  }
44
- function optNumber(obj, key) {
58
+ function optNumber(obj, key, tool) {
45
59
  const v = obj[key];
46
- return typeof v === "number" && Number.isFinite(v) ? v : void 0;
60
+ if (v === void 0 || v === null)
61
+ return void 0;
62
+ if (typeof v !== "number" || !Number.isFinite(v))
63
+ rejectWrongType(obj, key, "a finite number", tool);
64
+ return v;
47
65
  }
48
- function optBool(obj, key) {
66
+ function optBool(obj, key, tool) {
49
67
  const v = obj[key];
50
- return typeof v === "boolean" ? v : void 0;
68
+ if (v === void 0 || v === null)
69
+ return void 0;
70
+ if (typeof v !== "boolean")
71
+ rejectWrongType(obj, key, "a boolean", tool);
72
+ return v;
51
73
  }
52
- function parseTargetArgs(obj) {
74
+ function parseTargetArgs(obj, tool) {
53
75
  const out = {};
54
- if (typeof obj.tabId === "number")
76
+ if (obj.tabId !== void 0 && obj.tabId !== null) {
77
+ if (typeof obj.tabId !== "number" || !Number.isFinite(obj.tabId)) {
78
+ rejectWrongType(obj, "tabId", "a finite number", tool);
79
+ }
55
80
  out.tabId = obj.tabId;
56
- if (typeof obj.workspaceName === "string" && obj.workspaceName)
57
- out.workspaceName = obj.workspaceName;
58
- if (typeof obj.groupName === "string" && obj.groupName)
59
- out.groupName = obj.groupName;
81
+ }
82
+ if (obj.workspaceName !== void 0 && obj.workspaceName !== null) {
83
+ if (typeof obj.workspaceName !== "string")
84
+ rejectWrongType(obj, "workspaceName", "a string", tool);
85
+ if (obj.workspaceName)
86
+ out.workspaceName = obj.workspaceName;
87
+ }
88
+ if (obj.groupName !== void 0 && obj.groupName !== null) {
89
+ if (typeof obj.groupName !== "string")
90
+ rejectWrongType(obj, "groupName", "a string", tool);
91
+ if (obj.groupName)
92
+ out.groupName = obj.groupName;
93
+ }
60
94
  return out;
61
95
  }
62
96
  var init_shared = __esm({
@@ -69,26 +103,38 @@ var init_shared = __esm({
69
103
  // ../protocol/dist/args/navigate.js
70
104
  function parseChromeNavigateArgs(input) {
71
105
  const obj = asObject(input, TOOL_NAMES.NAVIGATE);
72
- const out = {
73
- url: requireString(obj, "url", TOOL_NAMES.NAVIGATE),
74
- ...parseTargetArgs(obj)
75
- };
106
+ const out = { url: requireString(obj, "url", TOOL_NAMES.NAVIGATE) };
76
107
  if (typeof obj.tabId === "string" && obj.tabId) {
77
108
  const n = Number(obj.tabId);
78
- if (Number.isFinite(n))
79
- out.tabId = n;
109
+ if (!Number.isFinite(n)) {
110
+ throw new RelayError({
111
+ code: "invalid_arguments",
112
+ message: `chrome_navigate: invalid tabId ${JSON.stringify(obj.tabId)}. Expected a number.`,
113
+ tool: TOOL_NAMES.NAVIGATE,
114
+ phase: "parse_arguments",
115
+ details: { field: "tabId", received: obj.tabId },
116
+ retryable: false
117
+ });
118
+ }
119
+ out.tabId = n;
80
120
  } else {
81
- const n = optNumber(obj, "tabId");
121
+ const n = optNumber(obj, "tabId", TOOL_NAMES.NAVIGATE);
82
122
  if (n !== void 0)
83
123
  out.tabId = n;
84
124
  }
85
- const newTab = optBool(obj, "newTab");
125
+ const { tabId: _, ...rest } = obj;
126
+ const target = parseTargetArgs(rest, TOOL_NAMES.NAVIGATE);
127
+ if (target.workspaceName)
128
+ out.workspaceName = target.workspaceName;
129
+ if (target.groupName)
130
+ out.groupName = target.groupName;
131
+ const newTab = optBool(obj, "newTab", TOOL_NAMES.NAVIGATE);
86
132
  if (newTab !== void 0)
87
133
  out.newTab = newTab;
88
- const active = optBool(obj, "active");
134
+ const active = optBool(obj, "active", TOOL_NAMES.NAVIGATE);
89
135
  if (active !== void 0)
90
136
  out.active = active;
91
- const allowPartial = optBool(obj, "allowPartial");
137
+ const allowPartial = optBool(obj, "allowPartial", TOOL_NAMES.NAVIGATE);
92
138
  if (allowPartial !== void 0)
93
139
  out.allowPartial = allowPartial;
94
140
  void optString;
@@ -105,13 +151,13 @@ var init_navigate = __esm({
105
151
  // ../protocol/dist/args/hover.js
106
152
  function parseChromeHoverArgs(input) {
107
153
  const obj = asObject(input, TOOL_NAMES.HOVER);
108
- const target = parseTargetArgs(obj);
109
- const x = optNumber(obj, "x");
110
- const y = optNumber(obj, "y");
154
+ const target = parseTargetArgs(obj, TOOL_NAMES.HOVER);
155
+ const x = optNumber(obj, "x", TOOL_NAMES.HOVER);
156
+ const y = optNumber(obj, "y", TOOL_NAMES.HOVER);
111
157
  if (x !== void 0 && y !== void 0) {
112
158
  return { ...target, kind: "coords", x, y };
113
159
  }
114
- const selector = optString(obj, "selector");
160
+ const selector = optString(obj, "selector", TOOL_NAMES.HOVER);
115
161
  if (selector) {
116
162
  return { ...target, kind: "selector", selector };
117
163
  }
@@ -226,6 +272,475 @@ var init_network = __esm({
226
272
  }
227
273
  });
228
274
 
275
+ // ../protocol/dist/args/simple.js
276
+ function parseGetWindowsAndTabsArgs(_input) {
277
+ return {};
278
+ }
279
+ function parseChromeSelfReloadArgs(_input) {
280
+ return {};
281
+ }
282
+ function parseChromeReadPageArgs(input) {
283
+ const obj = asObject(input, TOOL_NAMES.READ_PAGE);
284
+ const out = { ...parseTargetArgs(obj) };
285
+ const io = optBool(obj, "interactiveOnly");
286
+ if (io !== void 0)
287
+ out.interactiveOnly = io;
288
+ return out;
289
+ }
290
+ function parseChromeClickArgs(input) {
291
+ const obj = asObject(input, TOOL_NAMES.CLICK);
292
+ return {
293
+ selector: requireString(obj, "selector", TOOL_NAMES.CLICK),
294
+ ...parseTargetArgs(obj)
295
+ };
296
+ }
297
+ function parseChromeFillArgs(input) {
298
+ const obj = asObject(input, TOOL_NAMES.FILL);
299
+ if (typeof obj.value !== "string") {
300
+ throw new RelayError({
301
+ code: "invalid_arguments",
302
+ message: `${TOOL_NAMES.FILL}: \`value\` is required and must be a string (empty string allowed).`,
303
+ tool: TOOL_NAMES.FILL,
304
+ phase: "parse_arguments",
305
+ details: { field: "value", received: obj.value },
306
+ retryable: false
307
+ });
308
+ }
309
+ return {
310
+ selector: requireString(obj, "selector", TOOL_NAMES.FILL),
311
+ value: obj.value,
312
+ ...parseTargetArgs(obj)
313
+ };
314
+ }
315
+ function parseChromeKeyboardArgs(input) {
316
+ const obj = asObject(input, TOOL_NAMES.KEYBOARD);
317
+ return {
318
+ keys: requireString(obj, "keys", TOOL_NAMES.KEYBOARD),
319
+ ...parseTargetArgs(obj)
320
+ };
321
+ }
322
+ function parseChromeTypeArgs(input) {
323
+ const obj = asObject(input, TOOL_NAMES.TYPE);
324
+ const out = {
325
+ text: requireString(obj, "text", TOOL_NAMES.TYPE),
326
+ ...parseTargetArgs(obj)
327
+ };
328
+ const selector = optString(obj, "selector");
329
+ if (selector)
330
+ out.selector = selector;
331
+ return out;
332
+ }
333
+ function parseChromeEvaluateArgs(input) {
334
+ const obj = asObject(input, TOOL_NAMES.EVALUATE);
335
+ const out = {
336
+ code: requireString(obj, "code", TOOL_NAMES.EVALUATE),
337
+ ...parseTargetArgs(obj)
338
+ };
339
+ const t = optNumber(obj, "timeoutMs");
340
+ if (t !== void 0)
341
+ out.timeoutMs = t;
342
+ return out;
343
+ }
344
+ function parseChromeSwitchTabArgs(input) {
345
+ const obj = asObject(input, TOOL_NAMES.SWITCH_TAB);
346
+ const tabId = Number(obj.tabId);
347
+ if (!Number.isFinite(tabId)) {
348
+ throw new RelayError({
349
+ code: "invalid_arguments",
350
+ message: `${TOOL_NAMES.SWITCH_TAB} requires a numeric tabId.`,
351
+ tool: TOOL_NAMES.SWITCH_TAB,
352
+ phase: "parse_arguments",
353
+ details: { received: obj.tabId },
354
+ retryable: false
355
+ });
356
+ }
357
+ return { tabId };
358
+ }
359
+ function parseChromeCloseTabsArgs(input) {
360
+ const obj = asObject(input, TOOL_NAMES.CLOSE_TABS);
361
+ if (!Array.isArray(obj.tabIds)) {
362
+ throw new RelayError({
363
+ code: "invalid_arguments",
364
+ message: `${TOOL_NAMES.CLOSE_TABS} requires a numeric tabIds array.`,
365
+ tool: TOOL_NAMES.CLOSE_TABS,
366
+ phase: "parse_arguments",
367
+ details: { field: "tabIds", received: obj.tabIds },
368
+ retryable: false
369
+ });
370
+ }
371
+ const tabIds = obj.tabIds.map((v) => Number(v));
372
+ if (tabIds.length === 0 || tabIds.some((n) => !Number.isFinite(n))) {
373
+ throw new RelayError({
374
+ code: "invalid_arguments",
375
+ message: `${TOOL_NAMES.CLOSE_TABS} requires a non-empty array of numeric tab IDs.`,
376
+ tool: TOOL_NAMES.CLOSE_TABS,
377
+ phase: "parse_arguments",
378
+ details: { received: obj.tabIds },
379
+ retryable: false
380
+ });
381
+ }
382
+ return { tabIds };
383
+ }
384
+ function parseChromeAxArgs(input) {
385
+ const obj = asObject(input, TOOL_NAMES.AX);
386
+ const out = { ...parseTargetArgs(obj) };
387
+ const io = optBool(obj, "interactiveOnly");
388
+ if (io !== void 0)
389
+ out.interactiveOnly = io;
390
+ const root = optString(obj, "rootRole");
391
+ if (root)
392
+ out.rootRole = root;
393
+ const is = optBool(obj, "includeSubframes");
394
+ if (is !== void 0)
395
+ out.includeSubframes = is;
396
+ return out;
397
+ }
398
+ function parseChromeClickAxArgs(input) {
399
+ const obj = asObject(input, TOOL_NAMES.CLICK_AX);
400
+ const node = Number(obj.node ?? obj.id);
401
+ if (!Number.isFinite(node) || node <= 0) {
402
+ throw new RelayError({
403
+ code: "invalid_arguments",
404
+ message: `${TOOL_NAMES.CLICK_AX} requires \`node\` (a positive backendDOMNodeId from chrome_ax).`,
405
+ tool: TOOL_NAMES.CLICK_AX,
406
+ phase: "parse_arguments",
407
+ details: { received: obj.node ?? obj.id },
408
+ retryable: false
409
+ });
410
+ }
411
+ return { node, ...parseTargetArgs(obj) };
412
+ }
413
+ function parseChromeScreenshotArgs(input) {
414
+ const obj = asObject(input, TOOL_NAMES.SCREENSHOT);
415
+ const out = { ...parseTargetArgs(obj) };
416
+ const fp = optBool(obj, "fullPage");
417
+ if (fp !== void 0)
418
+ out.fullPage = fp;
419
+ const bbox = optString(obj, "bbox");
420
+ if (bbox)
421
+ out.bbox = bbox;
422
+ const sel = optString(obj, "selector");
423
+ if (sel)
424
+ out.selector = sel;
425
+ const pad = optNumber(obj, "padding");
426
+ if (pad !== void 0)
427
+ out.padding = pad;
428
+ const me = optNumber(obj, "maxEdge");
429
+ if (me !== void 0 && me > 0)
430
+ out.maxEdge = me;
431
+ return out;
432
+ }
433
+ var init_simple = __esm({
434
+ "../protocol/dist/args/simple.js"() {
435
+ "use strict";
436
+ init_dist();
437
+ init_shared();
438
+ }
439
+ });
440
+
441
+ // ../protocol/dist/args/multi.js
442
+ function invalidAction(tool, received, expected) {
443
+ throw new RelayError({
444
+ code: "invalid_arguments",
445
+ message: `${tool}: unknown action ${JSON.stringify(received)}. Expected ${expected.join(" | ")}.`,
446
+ tool,
447
+ phase: "parse_action",
448
+ details: { received, validChoices: expected },
449
+ retryable: false
450
+ });
451
+ }
452
+ function parseChromeViewportArgs(input) {
453
+ const obj = asObject(input, TOOL_NAMES.VIEWPORT);
454
+ const action = typeof obj.action === "string" ? obj.action : "set";
455
+ if (!VALID_VIEWPORT_ACTIONS.includes(action)) {
456
+ invalidAction(TOOL_NAMES.VIEWPORT, action, VALID_VIEWPORT_ACTIONS);
457
+ }
458
+ const target = parseTargetArgs(obj);
459
+ if (action === "list")
460
+ return { action: "list" };
461
+ if (action === "clear")
462
+ return { ...target, action: "clear" };
463
+ if (action === "preset") {
464
+ return { ...target, action: "preset", name: requireString(obj, "name", TOOL_NAMES.VIEWPORT) };
465
+ }
466
+ const width = Number(obj.width);
467
+ const height = Number(obj.height);
468
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
469
+ throw new RelayError({
470
+ code: "invalid_arguments",
471
+ message: `${TOOL_NAMES.VIEWPORT} set requires positive numeric width and height.`,
472
+ tool: TOOL_NAMES.VIEWPORT,
473
+ phase: "parse_dimensions",
474
+ details: { received: { width: obj.width, height: obj.height } },
475
+ retryable: false
476
+ });
477
+ }
478
+ const out = {
479
+ ...target,
480
+ action: "set",
481
+ width,
482
+ height
483
+ };
484
+ const dpr = optNumber(obj, "dpr");
485
+ if (dpr !== void 0)
486
+ out.dpr = dpr;
487
+ const mobile = optBool(obj, "mobile");
488
+ if (mobile !== void 0)
489
+ out.mobile = mobile;
490
+ const hasTouch = optBool(obj, "hasTouch");
491
+ if (hasTouch !== void 0)
492
+ out.hasTouch = hasTouch;
493
+ const userAgent = optString(obj, "userAgent");
494
+ if (userAgent)
495
+ out.userAgent = userAgent;
496
+ return out;
497
+ }
498
+ function parseLevels(input) {
499
+ if (input === void 0 || input === null)
500
+ return void 0;
501
+ const valid = new Set(VALID_CONSOLE_LEVELS);
502
+ const verify = (s) => {
503
+ if (typeof s !== "string" || !valid.has(s)) {
504
+ throw new RelayError({
505
+ code: "invalid_arguments",
506
+ message: `${TOOL_NAMES.CONSOLE}: invalid level ${JSON.stringify(s)}. Expected one of: ${VALID_CONSOLE_LEVELS.join(", ")}.`,
507
+ tool: TOOL_NAMES.CONSOLE,
508
+ phase: "parse_levels",
509
+ details: { received: s, validChoices: VALID_CONSOLE_LEVELS },
510
+ retryable: false
511
+ });
512
+ }
513
+ return s;
514
+ };
515
+ if (typeof input === "string")
516
+ return input.split(",").map((s) => verify(s.trim()));
517
+ if (Array.isArray(input))
518
+ return input.map(verify);
519
+ throw new RelayError({
520
+ code: "invalid_arguments",
521
+ message: `${TOOL_NAMES.CONSOLE}: invalid levels argument ${JSON.stringify(input)}. Expected a comma-separated string or an array of strings.`,
522
+ tool: TOOL_NAMES.CONSOLE,
523
+ phase: "parse_levels",
524
+ details: { received: input },
525
+ retryable: false
526
+ });
527
+ }
528
+ function parseChromeConsoleArgs(input) {
529
+ const obj = asObject(input, TOOL_NAMES.CONSOLE);
530
+ const target = parseTargetArgs(obj);
531
+ const action = typeof obj.action === "string" ? obj.action : "read";
532
+ if (!VALID_CONSOLE_ACTIONS.includes(action)) {
533
+ invalidAction(TOOL_NAMES.CONSOLE, action, VALID_CONSOLE_ACTIONS);
534
+ }
535
+ if (action === "clear")
536
+ return { ...target, action: "clear" };
537
+ const out = { ...target, action: "read" };
538
+ const levels = parseLevels(obj.levels);
539
+ if (levels)
540
+ out.levels = levels;
541
+ const since = optNumber(obj, "since");
542
+ if (since !== void 0)
543
+ out.since = since;
544
+ const limit = optNumber(obj, "limit");
545
+ if (limit !== void 0)
546
+ out.limit = limit;
547
+ return out;
548
+ }
549
+ function parseChromeWorkspaceArgs(input) {
550
+ const obj = asObject(input, TOOL_NAMES.WORKSPACE);
551
+ const action = typeof obj.action === "string" ? obj.action : "list";
552
+ if (!VALID_WORKSPACE_ACTIONS.includes(action)) {
553
+ invalidAction(TOOL_NAMES.WORKSPACE, action, VALID_WORKSPACE_ACTIONS);
554
+ }
555
+ if (action === "list")
556
+ return { action: "list" };
557
+ if (action === "close")
558
+ return { action: "close", name: requireString(obj, "name", TOOL_NAMES.WORKSPACE) };
559
+ const out = {
560
+ action: "create",
561
+ name: requireString(obj, "name", TOOL_NAMES.WORKSPACE)
562
+ };
563
+ const url = optString(obj, "url");
564
+ if (url)
565
+ out.url = url;
566
+ const label = optString(obj, "label");
567
+ if (label)
568
+ out.label = label;
569
+ return out;
570
+ }
571
+ function parseTabIds(raw) {
572
+ const reject = (bad) => {
573
+ throw new RelayError({
574
+ code: "invalid_arguments",
575
+ message: `${TOOL_NAMES.GROUP}: invalid tabId ${JSON.stringify(bad)}. Expected a number or a comma-separated list of numbers.`,
576
+ tool: TOOL_NAMES.GROUP,
577
+ phase: "parse_tab_ids",
578
+ details: { received: bad },
579
+ retryable: false
580
+ });
581
+ };
582
+ const coerce = (v) => {
583
+ const n = Number(typeof v === "string" ? v.trim() : v);
584
+ if (!Number.isFinite(n))
585
+ reject(v);
586
+ return n;
587
+ };
588
+ if (Array.isArray(raw))
589
+ return raw.map(coerce);
590
+ if (typeof raw === "string")
591
+ return raw.split(",").map(coerce);
592
+ if (typeof raw === "number")
593
+ return [raw];
594
+ return [];
595
+ }
596
+ function parseColor(raw) {
597
+ if (raw === void 0 || raw === null)
598
+ return void 0;
599
+ if (typeof raw !== "string") {
600
+ throw new RelayError({
601
+ code: "invalid_arguments",
602
+ message: `${TOOL_NAMES.GROUP}: invalid color ${JSON.stringify(raw)}. Expected one of: ${VALID_GROUP_COLORS.join(", ")}.`,
603
+ tool: TOOL_NAMES.GROUP,
604
+ phase: "parse_color",
605
+ details: { received: raw, validChoices: VALID_GROUP_COLORS },
606
+ retryable: false
607
+ });
608
+ }
609
+ const c = raw.toLowerCase();
610
+ if (!VALID_GROUP_COLORS.includes(c)) {
611
+ throw new RelayError({
612
+ code: "invalid_arguments",
613
+ message: `${TOOL_NAMES.GROUP}: invalid color "${raw}". Expected one of: ${VALID_GROUP_COLORS.join(", ")}.`,
614
+ tool: TOOL_NAMES.GROUP,
615
+ phase: "parse_color",
616
+ details: { received: raw, validChoices: VALID_GROUP_COLORS },
617
+ retryable: false
618
+ });
619
+ }
620
+ return c;
621
+ }
622
+ function parseChromeGroupArgs(input) {
623
+ const obj = asObject(input, TOOL_NAMES.GROUP);
624
+ const action = typeof obj.action === "string" ? obj.action : "list";
625
+ if (!VALID_GROUP_ACTIONS.includes(action)) {
626
+ invalidAction(TOOL_NAMES.GROUP, action, VALID_GROUP_ACTIONS);
627
+ }
628
+ if (action === "list")
629
+ return { action: "list" };
630
+ if (action === "close")
631
+ return { action: "close", name: requireString(obj, "name", TOOL_NAMES.GROUP) };
632
+ if (action === "remove") {
633
+ const tabIds2 = parseTabIds(obj.tabIds);
634
+ if (tabIds2.length === 0) {
635
+ throw new RelayError({
636
+ code: "invalid_arguments",
637
+ message: `${TOOL_NAMES.GROUP} remove requires tabIds.`,
638
+ tool: TOOL_NAMES.GROUP,
639
+ phase: "parse_arguments",
640
+ details: { field: "tabIds" },
641
+ retryable: false
642
+ });
643
+ }
644
+ return { action: "remove", tabIds: tabIds2 };
645
+ }
646
+ if (action === "add") {
647
+ const tabIds2 = parseTabIds(obj.tabIds);
648
+ if (tabIds2.length === 0) {
649
+ throw new RelayError({
650
+ code: "invalid_arguments",
651
+ message: `${TOOL_NAMES.GROUP} add requires tabIds.`,
652
+ tool: TOOL_NAMES.GROUP,
653
+ phase: "parse_arguments",
654
+ details: { field: "tabIds" },
655
+ retryable: false
656
+ });
657
+ }
658
+ return { action: "add", name: requireString(obj, "name", TOOL_NAMES.GROUP), tabIds: tabIds2 };
659
+ }
660
+ const tabIds = parseTabIds(obj.tabIds);
661
+ if (tabIds.length === 0) {
662
+ throw new RelayError({
663
+ code: "invalid_arguments",
664
+ message: `${TOOL_NAMES.GROUP} create requires at least one tabId.`,
665
+ tool: TOOL_NAMES.GROUP,
666
+ phase: "parse_arguments",
667
+ details: { field: "tabIds" },
668
+ retryable: false
669
+ });
670
+ }
671
+ const out = {
672
+ action: "create",
673
+ name: requireString(obj, "name", TOOL_NAMES.GROUP),
674
+ tabIds
675
+ };
676
+ const color = parseColor(obj.color);
677
+ if (color)
678
+ out.color = color;
679
+ const collapsed = optBool(obj, "collapsed");
680
+ if (collapsed !== void 0)
681
+ out.collapsed = collapsed;
682
+ const windowId = optNumber(obj, "windowId");
683
+ if (windowId !== void 0)
684
+ out.windowId = windowId;
685
+ return out;
686
+ }
687
+ function parseChromeScreencastArgs(input) {
688
+ const obj = asObject(input, TOOL_NAMES.SCREENCAST);
689
+ const target = parseTargetArgs(obj);
690
+ const action = typeof obj.action === "string" ? obj.action : "start";
691
+ if (!VALID_SCREENCAST_ACTIONS.includes(action)) {
692
+ invalidAction(TOOL_NAMES.SCREENCAST, action, VALID_SCREENCAST_ACTIONS);
693
+ }
694
+ if (action === "stop")
695
+ return { ...target, action: "stop" };
696
+ const out = {
697
+ ...target,
698
+ action: "start"
699
+ };
700
+ if (obj.format !== void 0 && obj.format !== null) {
701
+ if (obj.format !== "jpeg" && obj.format !== "png") {
702
+ throw new RelayError({
703
+ code: "invalid_arguments",
704
+ message: `${TOOL_NAMES.SCREENCAST}: invalid format ${JSON.stringify(obj.format)}. Expected "jpeg" or "png".`,
705
+ tool: TOOL_NAMES.SCREENCAST,
706
+ phase: "parse_format",
707
+ details: { received: obj.format, validChoices: VALID_SCREENCAST_FORMATS },
708
+ retryable: false
709
+ });
710
+ }
711
+ out.format = obj.format;
712
+ }
713
+ const q = optNumber(obj, "quality");
714
+ if (q !== void 0)
715
+ out.quality = q;
716
+ const mw = optNumber(obj, "maxWidth");
717
+ if (mw !== void 0)
718
+ out.maxWidth = mw;
719
+ const mh = optNumber(obj, "maxHeight");
720
+ if (mh !== void 0)
721
+ out.maxHeight = mh;
722
+ const en = optNumber(obj, "everyNthFrame");
723
+ if (en !== void 0)
724
+ out.everyNthFrame = en;
725
+ return out;
726
+ }
727
+ var VALID_VIEWPORT_ACTIONS, VALID_CONSOLE_ACTIONS, VALID_CONSOLE_LEVELS, VALID_WORKSPACE_ACTIONS, VALID_GROUP_ACTIONS, VALID_GROUP_COLORS, VALID_SCREENCAST_ACTIONS, VALID_SCREENCAST_FORMATS;
728
+ var init_multi = __esm({
729
+ "../protocol/dist/args/multi.js"() {
730
+ "use strict";
731
+ init_dist();
732
+ init_shared();
733
+ VALID_VIEWPORT_ACTIONS = ["set", "preset", "clear", "list"];
734
+ VALID_CONSOLE_ACTIONS = ["read", "clear"];
735
+ VALID_CONSOLE_LEVELS = ["log", "info", "warn", "error", "debug", "exception"];
736
+ VALID_WORKSPACE_ACTIONS = ["create", "list", "close"];
737
+ VALID_GROUP_ACTIONS = ["create", "list", "close", "add", "remove"];
738
+ VALID_GROUP_COLORS = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
739
+ VALID_SCREENCAST_ACTIONS = ["start", "stop"];
740
+ VALID_SCREENCAST_FORMATS = ["jpeg", "png"];
741
+ }
742
+ });
743
+
229
744
  // ../protocol/dist/args/index.js
230
745
  var init_args = __esm({
231
746
  "../protocol/dist/args/index.js"() {
@@ -234,6 +749,8 @@ var init_args = __esm({
234
749
  init_navigate();
235
750
  init_hover();
236
751
  init_network();
752
+ init_simple();
753
+ init_multi();
237
754
  }
238
755
  });
239
756
 
@@ -253,9 +770,27 @@ __export(dist_exports, {
253
770
  optBool: () => optBool,
254
771
  optNumber: () => optNumber,
255
772
  optString: () => optString,
773
+ parseChromeAxArgs: () => parseChromeAxArgs,
774
+ parseChromeClickArgs: () => parseChromeClickArgs,
775
+ parseChromeClickAxArgs: () => parseChromeClickAxArgs,
776
+ parseChromeCloseTabsArgs: () => parseChromeCloseTabsArgs,
777
+ parseChromeConsoleArgs: () => parseChromeConsoleArgs,
778
+ parseChromeEvaluateArgs: () => parseChromeEvaluateArgs,
779
+ parseChromeFillArgs: () => parseChromeFillArgs,
780
+ parseChromeGroupArgs: () => parseChromeGroupArgs,
256
781
  parseChromeHoverArgs: () => parseChromeHoverArgs,
782
+ parseChromeKeyboardArgs: () => parseChromeKeyboardArgs,
257
783
  parseChromeNavigateArgs: () => parseChromeNavigateArgs,
258
784
  parseChromeNetworkArgs: () => parseChromeNetworkArgs,
785
+ parseChromeReadPageArgs: () => parseChromeReadPageArgs,
786
+ parseChromeScreencastArgs: () => parseChromeScreencastArgs,
787
+ parseChromeScreenshotArgs: () => parseChromeScreenshotArgs,
788
+ parseChromeSelfReloadArgs: () => parseChromeSelfReloadArgs,
789
+ parseChromeSwitchTabArgs: () => parseChromeSwitchTabArgs,
790
+ parseChromeTypeArgs: () => parseChromeTypeArgs,
791
+ parseChromeViewportArgs: () => parseChromeViewportArgs,
792
+ parseChromeWorkspaceArgs: () => parseChromeWorkspaceArgs,
793
+ parseGetWindowsAndTabsArgs: () => parseGetWindowsAndTabsArgs,
259
794
  parseTargetArgs: () => parseTargetArgs,
260
795
  requireString: () => requireString,
261
796
  toBridgeError: () => toBridgeError
@@ -368,7 +903,7 @@ var init_dist = __esm({
368
903
  import { Command } from "commander";
369
904
 
370
905
  // src/index.ts
371
- var CHROME_RELAY_VERSION = true ? "0.5.12" : "0.0.0-dev";
906
+ var CHROME_RELAY_VERSION = true ? "0.5.14" : "0.0.0-dev";
372
907
 
373
908
  // src/commands/shared.ts
374
909
  init_dist();
@@ -416,6 +951,11 @@ async function callTool(name, args) {
416
951
  }
417
952
 
418
953
  // src/commands/shared.ts
954
+ function makeWithBase(baseArgs) {
955
+ return function withBase(opts, extras) {
956
+ return { ...baseArgs(opts), ...extras ?? {} };
957
+ };
958
+ }
419
959
  function tabOpt(cmd) {
420
960
  return cmd.option("-t, --tab <id>", "target tab ID", (v) => Number(v)).option("--workspace <name>", "target the active tab in a named workspace window (see `chrome-relay workspace`)").option("--group <name>", "target the active tab in a named tab-group (see `chrome-relay group`)");
421
961
  }
@@ -602,6 +1142,22 @@ async function runDoctor() {
602
1142
 
603
1143
  // src/release-notes.ts
604
1144
  var RELEASE_NOTES = {
1145
+ "0.5.14": [
1146
+ "Optional parser fields are now strict \u2014 passing a present-but-wrong-type value (e.g. `{newTab: 'yes'}` instead of `true`) throws RelayError(invalid_arguments) instead of silently being dropped. undefined/null still means 'caller omitted the field' and the handler uses its default.",
1147
+ "parseTargetArgs is strict too: `{tabId: '5'}` rejects (numeric tabId required). Navigate is the one exception \u2014 it coerces string tabId for back-compat since it's used as a 'reference window' rather than a strict target.",
1148
+ "CLI tab-group --tabs arg now forwarded as the raw comma-separated string. The protocol parser (parseChromeGroupArgs) does the strict per-element parsing so `--tabs 1,foo,3` errors instead of silently becoming [1,3].",
1149
+ "Deleted the dead duplicate parser module at apps/extension/src/browser/parsers.ts and its test file. The protocol parsers replace them; behavior identical.",
1150
+ "HAR creator.version now reads from chrome.runtime.getManifest() instead of the hardcoded stale '0.2.x'. Drift-proof.",
1151
+ "New CLI helper `withBase(opts, extras)` collapses the `const args = {}; Object.assign(args, baseArgs(opts)); args.foo = bar` pattern into one expression. All four command modules migrated \u2014 `Object.assign(args, baseArgs(opts))` is gone from the codebase.",
1152
+ "Tests: +2 strict-optional cases. Total 407 (was 433 \u2014 28 dropped were duplicates of protocol parsers)."
1153
+ ],
1154
+ "0.5.13": [
1155
+ "Protocol arg-parser coverage complete (code-quality-hardening Risk 1 \u2014 finished). Every one of the 22 tools now has an executable parser in @chrome-relay/protocol that returns a typed args object with `code:'invalid_arguments'` errors on malformed input.",
1156
+ "New parsers in this release: parseChrome{ReadPage,Click,Fill,Keyboard,Type,Evaluate,SwitchTab,CloseTabs,Ax,ClickAx,Screenshot,Viewport,Console,Workspace,Group,Screencast}Args, plus parseGetWindowsAndTabsArgs and parseChromeSelfReloadArgs.",
1157
+ "All extension handlers now consume their parser at the top of the handler body \u2014 silent shape drift between CLI and extension is structurally impossible.",
1158
+ "Multi-action tools (viewport, console, network, workspace, group, screencast) return discriminated unions so the handler branches with TypeScript narrowing instead of `typeof args.action === 'string'` boilerplate.",
1159
+ "36 new tests in packages/protocol/test/args-all.test.ts. Total now 433 (was 397)."
1160
+ ],
605
1161
  "0.5.12": [
606
1162
  "Protocol-owned tool arg parsers (code-quality-hardening Risk 1). New @chrome-relay/protocol exports: `parseChromeNavigateArgs`, `parseChromeHoverArgs`, `parseChromeNetworkArgs`. Each is the single source of truth for what its tool accepts \u2014 CLI and extension consume the same parser so silent shape drift can't happen.",
607
1163
  "Pattern established with 3 representative tools (navigate, hover, network \u2014 the doc-followup explicitly named these). Remaining 19 tools are mechanical follow-up (~20 lines + tests each). Each parser throws `RelayError(invalid_arguments)` with field/received/validChoices in details.",
@@ -794,7 +1350,7 @@ function registerInstallUpdate(program) {
794
1350
 
795
1351
  // src/commands/navigation.ts
796
1352
  function registerNavigation(ctx) {
797
- const { program, baseArgs, run } = ctx;
1353
+ const { program, withBase, run } = ctx;
798
1354
  program.command("tabs [verb]").description("List open Chrome windows and tabs. (verb 'list' is accepted as alias)").action(async (verb) => {
799
1355
  if (verb && verb !== "list") {
800
1356
  process.stderr.write(`unknown tabs verb: ${verb}. Use 'tabs' or 'tabs list'.
@@ -823,11 +1379,10 @@ Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate
823
1379
  );
824
1380
  process.exit(1);
825
1381
  }
826
- const args = { url };
827
- Object.assign(args, baseArgs(opts));
828
- if (opts.new) args.newTab = true;
829
- if (opts.inactive) args.active = false;
830
- await run("chrome_navigate", args);
1382
+ const extras = { url };
1383
+ if (opts.new) extras.newTab = true;
1384
+ if (opts.inactive) extras.active = false;
1385
+ await run("chrome_navigate", withBase(opts, extras));
831
1386
  });
832
1387
  program.command("switch <tabId>").description("Activate a tab by ID.").action(async (tabId) => {
833
1388
  await run("chrome_switch_tab", { tabId: Number(tabId) });
@@ -843,20 +1398,16 @@ Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate
843
1398
 
844
1399
  // src/commands/input.ts
845
1400
  function registerInput(ctx) {
846
- const { program, baseArgs, run } = ctx;
1401
+ const { program, withBase, run } = ctx;
847
1402
  tabOpt(
848
1403
  program.command("click <selector>").description("Click an element by CSS selector.")
849
1404
  ).action(async (selector, opts) => {
850
- const args = { selector };
851
- Object.assign(args, baseArgs(opts));
852
- await run("chrome_click_element", args);
1405
+ await run("chrome_click_element", withBase(opts, { selector }));
853
1406
  });
854
1407
  tabOpt(
855
1408
  program.command("fill <selector> <value>").description("Fill an input or textarea.")
856
1409
  ).action(async (selector, value, opts) => {
857
- const args = { selector, value };
858
- Object.assign(args, baseArgs(opts));
859
- await run("chrome_fill_or_select", args);
1410
+ await run("chrome_fill_or_select", withBase(opts, { selector, value }));
860
1411
  });
861
1412
  tabOpt(
862
1413
  program.command("keys <keys>").description("Press a single key or chord via trusted CDP input (e.g. Enter, Cmd+K).").addHelpText(
@@ -873,9 +1424,7 @@ For typing text into a field, use \`chrome-relay type\` instead.
873
1424
  `
874
1425
  )
875
1426
  ).action(async (keys, opts) => {
876
- const args = { keys };
877
- Object.assign(args, baseArgs(opts));
878
- await run("chrome_keyboard", args);
1427
+ await run("chrome_keyboard", withBase(opts, { keys }));
879
1428
  });
880
1429
  tabOpt(
881
1430
  program.command("type <text>").description("Insert text via trusted CDP input. Works in contenteditable / Draft.js / Lexical.").option("-s, --selector <selector>", "focus this element first").addHelpText(
@@ -894,10 +1443,9 @@ When to pick which:
894
1443
  `
895
1444
  )
896
1445
  ).action(async (text, opts) => {
897
- const args = { text };
898
- Object.assign(args, baseArgs(opts));
899
- if (opts.selector) args.selector = opts.selector;
900
- await run("chrome_type", args);
1446
+ const extras = { text };
1447
+ if (opts.selector) extras.selector = opts.selector;
1448
+ await run("chrome_type", withBase(opts, extras));
901
1449
  });
902
1450
  tabOpt(
903
1451
  program.command("js <code>").description("Evaluate JavaScript in the page MAIN world. Use `return` for the value.").option("--timeout-ms <ms>", "execution timeout in milliseconds (default 15000)", (v) => Number(v)).addHelpText(
@@ -916,10 +1464,9 @@ Notes:
916
1464
  `
917
1465
  )
918
1466
  ).action(async (code, opts) => {
919
- const args = { code };
920
- Object.assign(args, baseArgs(opts));
921
- if (typeof opts.timeoutMs === "number") args.timeoutMs = opts.timeoutMs;
922
- await run("chrome_evaluate", args);
1467
+ const extras = { code };
1468
+ if (typeof opts.timeoutMs === "number") extras.timeoutMs = opts.timeoutMs;
1469
+ await run("chrome_evaluate", withBase(opts, extras));
923
1470
  });
924
1471
  tabOpt(
925
1472
  program.command("hover [selector]").description("Move the pointer over an element or coordinates. Fires :hover styles.").option("--x <px>", "explicit x coordinate (CSS pixels)", (v) => Number(v)).option("--y <px>", "explicit y coordinate (CSS pixels)", (v) => Number(v)).addHelpText(
@@ -935,21 +1482,20 @@ tooltip appearance, etc.) that a bare click would skip past too quickly.
935
1482
  `
936
1483
  )
937
1484
  ).action(async (selector, opts) => {
938
- const args = {};
939
- Object.assign(args, baseArgs(opts));
940
- if (selector) args.selector = selector;
1485
+ const extras = {};
1486
+ if (selector) extras.selector = selector;
941
1487
  if (typeof opts.x === "number" && typeof opts.y === "number") {
942
- args.x = opts.x;
943
- args.y = opts.y;
1488
+ extras.x = opts.x;
1489
+ extras.y = opts.y;
944
1490
  }
945
- await run("chrome_hover", args);
1491
+ await run("chrome_hover", withBase(opts, extras));
946
1492
  });
947
1493
  }
948
1494
 
949
1495
  // src/commands/capture.ts
950
1496
  import { writeFileSync } from "fs";
951
1497
  function registerCapture(ctx) {
952
- const { program, baseArgs, run } = ctx;
1498
+ const { program, withBase, run } = ctx;
953
1499
  tabOpt(
954
1500
  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(
955
1501
  "after",
@@ -968,13 +1514,13 @@ full-tab screenshot when an agent only needs to see one component.
968
1514
  `
969
1515
  )
970
1516
  ).action(async (opts) => {
971
- const args = {};
972
- Object.assign(args, baseArgs(opts));
973
- if (opts.full) args.fullPage = true;
974
- if (opts.bbox) args.bbox = opts.bbox;
975
- if (opts.selector) args.selector = opts.selector;
976
- if (typeof opts.padding === "number") args.padding = opts.padding;
977
- if (typeof opts.maxEdge === "number") args.maxEdge = opts.maxEdge;
1517
+ const extras = {};
1518
+ if (opts.full) extras.fullPage = true;
1519
+ if (opts.bbox) extras.bbox = opts.bbox;
1520
+ if (opts.selector) extras.selector = opts.selector;
1521
+ if (typeof opts.padding === "number") extras.padding = opts.padding;
1522
+ if (typeof opts.maxEdge === "number") extras.maxEdge = opts.maxEdge;
1523
+ const args = withBase(opts, extras);
978
1524
  try {
979
1525
  const result = await callTool("chrome_screenshot", args);
980
1526
  if (opts.out && result && typeof result === "object") {
@@ -998,10 +1544,9 @@ full-tab screenshot when an agent only needs to see one component.
998
1544
  tabOpt(
999
1545
  program.command("read").description("Extract page structure and interactive elements.").option("-i, --interactive", "return only interactive elements")
1000
1546
  ).action(async (opts) => {
1001
- const args = {};
1002
- Object.assign(args, baseArgs(opts));
1003
- if (opts.interactive) args.interactiveOnly = true;
1004
- await run("chrome_read_page", args);
1547
+ const extras = {};
1548
+ if (opts.interactive) extras.interactiveOnly = true;
1549
+ await run("chrome_read_page", withBase(opts, extras));
1005
1550
  });
1006
1551
  tabOpt(
1007
1552
  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(
@@ -1019,11 +1564,11 @@ Notes:
1019
1564
  `
1020
1565
  )
1021
1566
  ).action(async (opts) => {
1022
- const args = baseArgs(opts);
1023
- if (opts.interactiveOnly) args.interactiveOnly = true;
1024
- if (opts.root) args.rootRole = opts.root;
1025
- if (opts.includeSubframes) args.includeSubframes = true;
1026
- await run("chrome_ax", args);
1567
+ const extras = {};
1568
+ if (opts.interactiveOnly) extras.interactiveOnly = true;
1569
+ if (opts.root) extras.rootRole = opts.root;
1570
+ if (opts.includeSubframes) extras.includeSubframes = true;
1571
+ await run("chrome_ax", withBase(opts, extras));
1027
1572
  });
1028
1573
  tabOpt(
1029
1574
  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(
@@ -1039,9 +1584,7 @@ Notes:
1039
1584
  `
1040
1585
  )
1041
1586
  ).action(async (opts) => {
1042
- const args = baseArgs(opts);
1043
- args.node = opts.node;
1044
- await run("chrome_click_ax", args);
1587
+ await run("chrome_click_ax", withBase(opts, { node: opts.node }));
1045
1588
  });
1046
1589
  const screencast = program.command("screencast").description("Record a tab via CDP (paint-driven). Requires an active tab.").addHelpText(
1047
1590
  "after",
@@ -1066,20 +1609,18 @@ Notes:
1066
1609
  tabOpt(
1067
1610
  screencast.command("start").description("Begin screencast capture on a tab.").option("--format <fmt>", "jpeg | png (default jpeg)").option("--quality <n>", "jpeg quality 0-100 (default 80)", (v) => Number(v)).option("--max-width <px>", "downscale; aspect preserved", (v) => Number(v)).option("--max-height <px>", "downscale; aspect preserved", (v) => Number(v)).option("--every-nth <n>", "throttle: keep 1 in N frames (default 1)", (v) => Number(v))
1068
1611
  ).action(async (opts) => {
1069
- const args = { action: "start" };
1070
- Object.assign(args, baseArgs(opts));
1071
- if (opts.format) args.format = opts.format;
1072
- if (typeof opts.quality === "number") args.quality = opts.quality;
1073
- if (typeof opts.maxWidth === "number") args.maxWidth = opts.maxWidth;
1074
- if (typeof opts.maxHeight === "number") args.maxHeight = opts.maxHeight;
1075
- if (typeof opts.everyNth === "number") args.everyNthFrame = opts.everyNth;
1076
- await run("chrome_screencast", args);
1612
+ const extras = { action: "start" };
1613
+ if (opts.format) extras.format = opts.format;
1614
+ if (typeof opts.quality === "number") extras.quality = opts.quality;
1615
+ if (typeof opts.maxWidth === "number") extras.maxWidth = opts.maxWidth;
1616
+ if (typeof opts.maxHeight === "number") extras.maxHeight = opts.maxHeight;
1617
+ if (typeof opts.everyNth === "number") extras.everyNthFrame = opts.everyNth;
1618
+ await run("chrome_screencast", withBase(opts, extras));
1077
1619
  });
1078
1620
  tabOpt(
1079
1621
  screencast.command("stop").description("Stop the screencast and emit frames (or write to disk).").option("-o, --out <dir>", "write frames as JPEGs into this directory (created if missing)").option("--gif", "after writing frames, ffmpeg them into <dir>.gif (requires ffmpeg on PATH)").option("--mp4", "after writing frames, ffmpeg them into <dir>.mp4 (requires ffmpeg on PATH)").option("--fps <n>", "assumed framerate when invoking ffmpeg (default 15)", (v) => Number(v)).option("--no-dedupe", "keep raw frames; default collapses consecutive identical frames via SHA-256").option("--allow-missing-ffmpeg", "with --gif/--mp4: skip ffmpeg step (and emit a warning) if ffmpeg isn't on PATH, instead of failing with external_dependency_missing")
1080
1622
  ).action(async (opts) => {
1081
- const args = { action: "stop" };
1082
- Object.assign(args, baseArgs(opts));
1623
+ const args = withBase(opts, { action: "stop" });
1083
1624
  try {
1084
1625
  const result = await callTool("chrome_screencast", args);
1085
1626
  if (!opts.out) {
@@ -1214,7 +1755,7 @@ function netFilterArgs(opts) {
1214
1755
  return a;
1215
1756
  }
1216
1757
  function registerSessions(ctx) {
1217
- const { program, baseArgs, run } = ctx;
1758
+ const { program, withBase, run } = ctx;
1218
1759
  const viewport = program.command("viewport").description("Emulate device viewport, DPR, mobile flag, touch, and user agent.").addHelpText(
1219
1760
  "after",
1220
1761
  `
@@ -1235,27 +1776,22 @@ Notes:
1235
1776
  tabOpt(
1236
1777
  viewport.command("set").description("Apply explicit viewport dimensions.").requiredOption("--width <px>", "viewport width in CSS pixels", (v) => Number(v)).requiredOption("--height <px>", "viewport height in CSS pixels", (v) => Number(v)).option("--dpr <ratio>", "device pixel ratio (1, 2, 3...)", (v) => Number(v)).option("--mobile", "set the mobile flag (affects meta viewport interpretation)").option("--touch", "enable touch event emulation").option("--user-agent <ua>", "override the User-Agent header")
1237
1778
  ).action(async (opts) => {
1238
- const args = { action: "set", width: opts.width, height: opts.height };
1239
- Object.assign(args, baseArgs(opts));
1240
- if (opts.dpr !== void 0) args.dpr = opts.dpr;
1241
- if (opts.mobile) args.mobile = true;
1242
- if (opts.touch) args.hasTouch = true;
1243
- if (opts.userAgent) args.userAgent = opts.userAgent;
1244
- await run("chrome_viewport", args);
1779
+ const extras = { action: "set", width: opts.width, height: opts.height };
1780
+ if (opts.dpr !== void 0) extras.dpr = opts.dpr;
1781
+ if (opts.mobile) extras.mobile = true;
1782
+ if (opts.touch) extras.hasTouch = true;
1783
+ if (opts.userAgent) extras.userAgent = opts.userAgent;
1784
+ await run("chrome_viewport", withBase(opts, extras));
1245
1785
  });
1246
1786
  tabOpt(
1247
1787
  viewport.command("preset <name>").description("Apply a named device preset (iphone-14, pixel-7, desktop-1440, etc).")
1248
1788
  ).action(async (name, opts) => {
1249
- const args = { action: "preset", name };
1250
- Object.assign(args, baseArgs(opts));
1251
- await run("chrome_viewport", args);
1789
+ await run("chrome_viewport", withBase(opts, { action: "preset", name }));
1252
1790
  });
1253
1791
  tabOpt(
1254
1792
  viewport.command("clear").description("Drop the viewport override and return the tab to its native size.")
1255
1793
  ).action(async (opts) => {
1256
- const args = { action: "clear" };
1257
- Object.assign(args, baseArgs(opts));
1258
- await run("chrome_viewport", args);
1794
+ await run("chrome_viewport", withBase(opts, { action: "clear" }));
1259
1795
  });
1260
1796
  viewport.command("list").description("List available presets.").action(async () => {
1261
1797
  await run("chrome_viewport", { action: "list" });
@@ -1316,8 +1852,7 @@ Notes:
1316
1852
  `
1317
1853
  );
1318
1854
  group.command("create <name>").description("Group existing tabs into a new tab-group bound to <name>.").requiredOption("--tabs <ids>", "comma-separated tab IDs to group, e.g. 123,456,789").option("--color <color>", "grey | blue | red | yellow | green | pink | purple | cyan | orange").option("--collapsed", "create the group in its collapsed state").action(async (name, opts) => {
1319
- const args = { action: "create", name };
1320
- args.tabIds = String(opts.tabs).split(",").map((s) => Number(s.trim())).filter(Number.isFinite);
1855
+ const args = { action: "create", name, tabIds: String(opts.tabs) };
1321
1856
  if (opts.color) args.color = opts.color;
1322
1857
  if (opts.collapsed) args.collapsed = true;
1323
1858
  await run("chrome_group", args);
@@ -1329,12 +1864,10 @@ Notes:
1329
1864
  await run("chrome_group", { action: "close", name });
1330
1865
  });
1331
1866
  group.command("add <name>").description("Add existing tabs to an existing tab-group.").requiredOption("--tabs <ids>", "comma-separated tab IDs to add").action(async (name, opts) => {
1332
- const tabIds = String(opts.tabs).split(",").map((s) => Number(s.trim())).filter(Number.isFinite);
1333
- await run("chrome_group", { action: "add", name, tabIds });
1867
+ await run("chrome_group", { action: "add", name, tabIds: String(opts.tabs) });
1334
1868
  });
1335
1869
  group.command("remove").description("Ungroup specific tabs (they remain open, just outside any tab-group).").requiredOption("--tabs <ids>", "comma-separated tab IDs to ungroup").action(async (opts) => {
1336
- const tabIds = String(opts.tabs).split(",").map((s) => Number(s.trim())).filter(Number.isFinite);
1337
- await run("chrome_group", { action: "remove", tabIds });
1870
+ await run("chrome_group", { action: "remove", tabIds: String(opts.tabs) });
1338
1871
  });
1339
1872
  const network = tabOpt(netFilterOpts(
1340
1873
  program.command("network").description("Capture HTTP request/response metadata. Ring buffer, last 200 per tab.")
@@ -1364,41 +1897,38 @@ Notes:
1364
1897
  WebSocket frames and SSE streams are out of scope.
1365
1898
  `
1366
1899
  ).action(async (opts) => {
1367
- const args = { ...baseArgs(opts), ...netFilterArgs(opts) };
1368
- await run("chrome_network", args);
1900
+ await run("chrome_network", withBase(opts, netFilterArgs(opts)));
1369
1901
  });
1370
1902
  tabOpt(netFilterOpts(
1371
1903
  network.command("read").description("(alias) list captured network entries.")
1372
1904
  )).action(async (opts) => {
1373
- const args = { ...baseArgs(opts), ...netFilterArgs(opts) };
1374
- await run("chrome_network", args);
1905
+ await run("chrome_network", withBase(opts, netFilterArgs(opts)));
1375
1906
  });
1376
1907
  tabOpt(
1377
1908
  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")
1378
1909
  ).action(async (requestId, opts) => {
1379
- const args = { ...baseArgs(opts), action: "body", requestId };
1380
- if (opts.full) args.full = true;
1381
- if (typeof opts.head === "number") args.head = opts.head;
1382
- await run("chrome_network", args);
1910
+ const extras = { action: "body", requestId };
1911
+ if (opts.full) extras.full = true;
1912
+ if (typeof opts.head === "number") extras.head = opts.head;
1913
+ await run("chrome_network", withBase(opts, extras));
1383
1914
  });
1384
1915
  tabOpt(netFilterOpts(
1385
1916
  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")
1386
1917
  )).action(async (opts) => {
1387
- const args = { ...baseArgs(opts), ...netFilterArgs(opts), action: "har" };
1388
- if (opts.withBodies) args.withBodies = true;
1389
- if (opts.bestEffortBodies) args.bestEffortBodies = true;
1918
+ const extras = { ...netFilterArgs(opts), action: "har" };
1919
+ if (opts.withBodies) extras.withBodies = true;
1920
+ if (opts.bestEffortBodies) extras.bestEffortBodies = true;
1390
1921
  if (!opts.withBodies) {
1391
1922
  process.stderr.write(
1392
1923
  "[chrome-relay] HAR exported WITHOUT response bodies. Pass --with-bodies to include them (strict by default; add --best-effort-bodies to allow per-entry misses).\n"
1393
1924
  );
1394
1925
  }
1395
- await run("chrome_network", args);
1926
+ await run("chrome_network", withBase(opts, extras));
1396
1927
  });
1397
1928
  tabOpt(
1398
1929
  network.command("clear").description("Wipe the network buffer for this tab.")
1399
1930
  ).action(async (opts) => {
1400
- const args = { ...baseArgs(opts), action: "clear" };
1401
- await run("chrome_network", args);
1931
+ await run("chrome_network", withBase(opts, { action: "clear" }));
1402
1932
  });
1403
1933
  tabOpt(
1404
1934
  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(
@@ -1418,12 +1948,12 @@ Notes:
1418
1948
  `
1419
1949
  )
1420
1950
  ).action(async (opts) => {
1421
- const args = baseArgs(opts);
1422
- if (opts.clear) args.action = "clear";
1423
- if (opts.level) args.levels = opts.level;
1424
- if (typeof opts.since === "number") args.since = opts.since;
1425
- if (typeof opts.limit === "number") args.limit = opts.limit;
1426
- await run("chrome_console", args);
1951
+ const extras = {};
1952
+ if (opts.clear) extras.action = "clear";
1953
+ if (opts.level) extras.levels = opts.level;
1954
+ if (typeof opts.since === "number") extras.since = opts.since;
1955
+ if (typeof opts.limit === "number") extras.limit = opts.limit;
1956
+ await run("chrome_console", withBase(opts, extras));
1427
1957
  });
1428
1958
  }
1429
1959
 
@@ -1450,9 +1980,11 @@ Notes:
1450
1980
  Tools attach via CDP and run on backgrounded tabs without stealing focus.
1451
1981
  `
1452
1982
  );
1983
+ const baseArgs = makeBaseArgs(program);
1453
1984
  const ctx = {
1454
1985
  program,
1455
- baseArgs: makeBaseArgs(program),
1986
+ baseArgs,
1987
+ withBase: makeWithBase(baseArgs),
1456
1988
  run: runTool
1457
1989
  };
1458
1990
  registerInstallUpdate(program);
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- var CHROME_RELAY_VERSION = true ? "0.5.12" : "0.0.0-dev";
2
+ var CHROME_RELAY_VERSION = true ? "0.5.14" : "0.0.0-dev";
3
3
  export {
4
4
  CHROME_RELAY_VERSION
5
5
  };
@@ -48,7 +48,7 @@ function toBridgeError(unknownErr, fallbackTool) {
48
48
  }
49
49
 
50
50
  // src/index.ts
51
- var CHROME_RELAY_VERSION = true ? "0.5.12" : "0.0.0-dev";
51
+ var CHROME_RELAY_VERSION = true ? "0.5.14" : "0.0.0-dev";
52
52
 
53
53
  // src/release-notes.ts
54
54
  function compareSemver(a, b) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-relay",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",