chrome-relay 0.5.22 → 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 +945 -214
- package/dist/index.js +1 -1
- package/dist/native-host.js +1 -1
- package/package.json +11 -3
package/dist/cli.js
CHANGED
|
@@ -228,140 +228,6 @@ var init_navigate = __esm({
|
|
|
228
228
|
}
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
// ../protocol/dist/args/hover.js
|
|
232
|
-
function parseChromeHoverArgs(input) {
|
|
233
|
-
const obj = asObject(input, TOOL_NAMES.HOVER);
|
|
234
|
-
const target = parseTargetArgs(obj, TOOL_NAMES.HOVER);
|
|
235
|
-
const x = optNumber(obj, "x", TOOL_NAMES.HOVER);
|
|
236
|
-
const y = optNumber(obj, "y", TOOL_NAMES.HOVER);
|
|
237
|
-
if (x !== void 0 !== (y !== void 0)) {
|
|
238
|
-
throw new RelayError({
|
|
239
|
-
code: "invalid_arguments",
|
|
240
|
-
message: "chrome_hover: pass BOTH x and y, or neither (selector mode).",
|
|
241
|
-
tool: TOOL_NAMES.HOVER,
|
|
242
|
-
phase: "parse_arguments",
|
|
243
|
-
details: { received: { x: obj.x, y: obj.y } },
|
|
244
|
-
retryable: false
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
if (x !== void 0 && y !== void 0) {
|
|
248
|
-
return { ...target, kind: "coords", x, y };
|
|
249
|
-
}
|
|
250
|
-
const selector = optString(obj, "selector", TOOL_NAMES.HOVER);
|
|
251
|
-
if (selector) {
|
|
252
|
-
return { ...target, kind: "selector", selector };
|
|
253
|
-
}
|
|
254
|
-
throw new RelayError({
|
|
255
|
-
code: "invalid_arguments",
|
|
256
|
-
message: "chrome_hover requires either a selector or x AND y.",
|
|
257
|
-
tool: TOOL_NAMES.HOVER,
|
|
258
|
-
phase: "parse_arguments",
|
|
259
|
-
details: { received: { selector: obj.selector, x: obj.x, y: obj.y } },
|
|
260
|
-
retryable: false
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
var init_hover = __esm({
|
|
264
|
-
"../protocol/dist/args/hover.js"() {
|
|
265
|
-
"use strict";
|
|
266
|
-
init_dist();
|
|
267
|
-
init_shared();
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// ../protocol/dist/args/network.js
|
|
272
|
-
function parseFilter(obj) {
|
|
273
|
-
const out = {};
|
|
274
|
-
const filter = optString(obj, "filter");
|
|
275
|
-
if (filter)
|
|
276
|
-
out.filter = filter;
|
|
277
|
-
const method = optString(obj, "method");
|
|
278
|
-
if (method)
|
|
279
|
-
out.method = method;
|
|
280
|
-
const limit = optNumber(obj, "limit");
|
|
281
|
-
if (limit !== void 0)
|
|
282
|
-
out.limit = limit;
|
|
283
|
-
const status = obj.status;
|
|
284
|
-
if (status !== void 0 && status !== null) {
|
|
285
|
-
if (typeof status !== "string" || !VALID_STATUSES.includes(status)) {
|
|
286
|
-
throw new RelayError({
|
|
287
|
-
code: "invalid_arguments",
|
|
288
|
-
message: `chrome_network: invalid status ${JSON.stringify(status)}. Expected one of: ${VALID_STATUSES.join(", ")}.`,
|
|
289
|
-
tool: TOOL_NAMES.NETWORK,
|
|
290
|
-
phase: "parse_status",
|
|
291
|
-
details: { received: status, validChoices: VALID_STATUSES },
|
|
292
|
-
retryable: false
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
out.status = status;
|
|
296
|
-
}
|
|
297
|
-
return out;
|
|
298
|
-
}
|
|
299
|
-
function parseChromeNetworkArgs(input) {
|
|
300
|
-
const obj = asObject(input, TOOL_NAMES.NETWORK);
|
|
301
|
-
const target = parseTargetArgs(obj);
|
|
302
|
-
const rawAction = obj.action;
|
|
303
|
-
const action = typeof rawAction === "string" ? rawAction : "read";
|
|
304
|
-
if (action === "clear") {
|
|
305
|
-
return { ...target, action: "clear" };
|
|
306
|
-
}
|
|
307
|
-
if (action === "body") {
|
|
308
|
-
const requestId = optString(obj, "requestId");
|
|
309
|
-
if (!requestId) {
|
|
310
|
-
throw new RelayError({
|
|
311
|
-
code: "invalid_arguments",
|
|
312
|
-
message: "chrome_network body requires `requestId` (a non-empty string).",
|
|
313
|
-
tool: TOOL_NAMES.NETWORK,
|
|
314
|
-
phase: "parse_arguments",
|
|
315
|
-
details: { field: "requestId", received: obj.requestId },
|
|
316
|
-
retryable: false
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
const out = {
|
|
320
|
-
...target,
|
|
321
|
-
action: "body",
|
|
322
|
-
requestId
|
|
323
|
-
};
|
|
324
|
-
const full = optBool(obj, "full");
|
|
325
|
-
if (full !== void 0)
|
|
326
|
-
out.full = full;
|
|
327
|
-
const head = optPositiveNumber(obj, "head", TOOL_NAMES.NETWORK);
|
|
328
|
-
if (head !== void 0)
|
|
329
|
-
out.head = head;
|
|
330
|
-
return out;
|
|
331
|
-
}
|
|
332
|
-
if (action === "har") {
|
|
333
|
-
const withBodies = optBool(obj, "withBodies");
|
|
334
|
-
const bestEffortBodies = optBool(obj, "bestEffortBodies");
|
|
335
|
-
return {
|
|
336
|
-
...target,
|
|
337
|
-
action: "har",
|
|
338
|
-
...withBodies !== void 0 ? { withBodies } : {},
|
|
339
|
-
...bestEffortBodies !== void 0 ? { bestEffortBodies } : {},
|
|
340
|
-
...parseFilter(obj)
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
if (action === "read") {
|
|
344
|
-
return { ...target, action: "read", ...parseFilter(obj) };
|
|
345
|
-
}
|
|
346
|
-
throw new RelayError({
|
|
347
|
-
code: "invalid_arguments",
|
|
348
|
-
message: `chrome_network: unknown action "${action}". Expected read | clear | har | body.`,
|
|
349
|
-
tool: TOOL_NAMES.NETWORK,
|
|
350
|
-
phase: "parse_action",
|
|
351
|
-
details: { received: action, validChoices: ["read", "clear", "har", "body"] },
|
|
352
|
-
retryable: false
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
var VALID_STATUSES;
|
|
356
|
-
var init_network = __esm({
|
|
357
|
-
"../protocol/dist/args/network.js"() {
|
|
358
|
-
"use strict";
|
|
359
|
-
init_dist();
|
|
360
|
-
init_shared();
|
|
361
|
-
VALID_STATUSES = ["ok", "redirect", "client_error", "server_error", "failed"];
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
231
|
// ../protocol/dist/args/simple.js
|
|
366
232
|
function parseGetWindowsAndTabsArgs(input) {
|
|
367
233
|
if (input !== void 0 && input !== null)
|
|
@@ -381,6 +247,25 @@ function parseChromeReadPageArgs(input) {
|
|
|
381
247
|
out.interactiveOnly = io;
|
|
382
248
|
return out;
|
|
383
249
|
}
|
|
250
|
+
function rejectMixedAddressing(tool, obj, modes) {
|
|
251
|
+
const present = [];
|
|
252
|
+
if (modes.ref)
|
|
253
|
+
present.push("ref");
|
|
254
|
+
if (modes.selector)
|
|
255
|
+
present.push("selector");
|
|
256
|
+
if (modes.coords)
|
|
257
|
+
present.push("x/y");
|
|
258
|
+
if (present.length > 1) {
|
|
259
|
+
throw new RelayError({
|
|
260
|
+
code: "invalid_arguments",
|
|
261
|
+
message: `${tool}: ${present.join(" + ")} are mutually exclusive \u2014 pass exactly one addressing mode.`,
|
|
262
|
+
tool,
|
|
263
|
+
phase: "parse_arguments",
|
|
264
|
+
details: { received: present },
|
|
265
|
+
retryable: false
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
384
269
|
function parseChromeClickArgs(input) {
|
|
385
270
|
const obj = asObject(input, TOOL_NAMES.CLICK);
|
|
386
271
|
const target = parseTargetArgs(obj, TOOL_NAMES.CLICK);
|
|
@@ -396,19 +281,24 @@ function parseChromeClickArgs(input) {
|
|
|
396
281
|
retryable: false
|
|
397
282
|
});
|
|
398
283
|
}
|
|
284
|
+
const ref = optString(obj, "ref", TOOL_NAMES.CLICK);
|
|
285
|
+
const selector = optString(obj, "selector", TOOL_NAMES.CLICK);
|
|
286
|
+
rejectMixedAddressing(TOOL_NAMES.CLICK, obj, { ref, selector, coords: x !== void 0 });
|
|
287
|
+
if (ref) {
|
|
288
|
+
return { ...target, kind: "ref", ref };
|
|
289
|
+
}
|
|
399
290
|
if (x !== void 0 && y !== void 0) {
|
|
400
291
|
return { ...target, kind: "coords", x, y };
|
|
401
292
|
}
|
|
402
|
-
const selector = optString(obj, "selector", TOOL_NAMES.CLICK);
|
|
403
293
|
if (selector) {
|
|
404
294
|
return { ...target, kind: "selector", selector };
|
|
405
295
|
}
|
|
406
296
|
throw new RelayError({
|
|
407
297
|
code: "invalid_arguments",
|
|
408
|
-
message: "chrome_click_element requires
|
|
298
|
+
message: "chrome_click_element requires a @ref, a selector, or x AND y.",
|
|
409
299
|
tool: TOOL_NAMES.CLICK,
|
|
410
300
|
phase: "parse_arguments",
|
|
411
|
-
details: { received: { selector: obj.selector, x: obj.x, y: obj.y } },
|
|
301
|
+
details: { received: { ref: obj.ref, selector: obj.selector, x: obj.x, y: obj.y } },
|
|
412
302
|
retryable: false
|
|
413
303
|
});
|
|
414
304
|
}
|
|
@@ -424,10 +314,18 @@ function parseChromeFillArgs(input) {
|
|
|
424
314
|
retryable: false
|
|
425
315
|
});
|
|
426
316
|
}
|
|
317
|
+
const target = parseTargetArgs(obj);
|
|
318
|
+
const ref = optString(obj, "ref", TOOL_NAMES.FILL);
|
|
319
|
+
const selector = optString(obj, "selector", TOOL_NAMES.FILL);
|
|
320
|
+
rejectMixedAddressing(TOOL_NAMES.FILL, obj, { ref, selector });
|
|
321
|
+
if (ref) {
|
|
322
|
+
return { ...target, kind: "ref", ref, value: obj.value };
|
|
323
|
+
}
|
|
427
324
|
return {
|
|
325
|
+
...target,
|
|
326
|
+
kind: "selector",
|
|
428
327
|
selector: requireString(obj, "selector", TOOL_NAMES.FILL),
|
|
429
|
-
value: obj.value
|
|
430
|
-
...parseTargetArgs(obj)
|
|
328
|
+
value: obj.value
|
|
431
329
|
};
|
|
432
330
|
}
|
|
433
331
|
function parseChromeKeyboardArgs(input) {
|
|
@@ -444,8 +342,12 @@ function parseChromeTypeArgs(input) {
|
|
|
444
342
|
...parseTargetArgs(obj)
|
|
445
343
|
};
|
|
446
344
|
const selector = optString(obj, "selector");
|
|
345
|
+
const ref = optString(obj, "ref", TOOL_NAMES.TYPE);
|
|
346
|
+
rejectMixedAddressing(TOOL_NAMES.TYPE, obj, { ref, selector });
|
|
447
347
|
if (selector)
|
|
448
348
|
out.selector = selector;
|
|
349
|
+
if (ref)
|
|
350
|
+
out.ref = ref;
|
|
449
351
|
return out;
|
|
450
352
|
}
|
|
451
353
|
function parseChromeEvaluateArgs(input) {
|
|
@@ -525,6 +427,26 @@ function parseChromeClickAxArgs(input) {
|
|
|
525
427
|
}
|
|
526
428
|
return { node, ...parseTargetArgs(obj) };
|
|
527
429
|
}
|
|
430
|
+
function parseChromeSnapshotArgs(input) {
|
|
431
|
+
const obj = asObject(input, TOOL_NAMES.SNAPSHOT);
|
|
432
|
+
const out = { ...parseTargetArgs(obj, TOOL_NAMES.SNAPSHOT) };
|
|
433
|
+
const io = optBool(obj, "interactiveOnly", TOOL_NAMES.SNAPSHOT);
|
|
434
|
+
if (io !== void 0)
|
|
435
|
+
out.interactiveOnly = io;
|
|
436
|
+
const depth = optPositiveNumber(obj, "depth", TOOL_NAMES.SNAPSHOT);
|
|
437
|
+
if (depth !== void 0)
|
|
438
|
+
out.depth = depth;
|
|
439
|
+
const scope = optString(obj, "scope", TOOL_NAMES.SNAPSHOT);
|
|
440
|
+
if (scope)
|
|
441
|
+
out.scope = scope;
|
|
442
|
+
const urls = optBool(obj, "urls", TOOL_NAMES.SNAPSHOT);
|
|
443
|
+
if (urls !== void 0)
|
|
444
|
+
out.urls = urls;
|
|
445
|
+
const diff = optBool(obj, "diff", TOOL_NAMES.SNAPSHOT);
|
|
446
|
+
if (diff !== void 0)
|
|
447
|
+
out.diff = diff;
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
528
450
|
function parseChromeScreenshotArgs(input) {
|
|
529
451
|
const obj = asObject(input, TOOL_NAMES.SCREENSHOT);
|
|
530
452
|
const out = { ...parseTargetArgs(obj, TOOL_NAMES.SCREENSHOT) };
|
|
@@ -545,11 +467,151 @@ function parseChromeScreenshotArgs(input) {
|
|
|
545
467
|
out.maxEdge = me;
|
|
546
468
|
return out;
|
|
547
469
|
}
|
|
548
|
-
var init_simple = __esm({
|
|
549
|
-
"../protocol/dist/args/simple.js"() {
|
|
470
|
+
var init_simple = __esm({
|
|
471
|
+
"../protocol/dist/args/simple.js"() {
|
|
472
|
+
"use strict";
|
|
473
|
+
init_dist();
|
|
474
|
+
init_shared();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ../protocol/dist/args/hover.js
|
|
479
|
+
function parseChromeHoverArgs(input) {
|
|
480
|
+
const obj = asObject(input, TOOL_NAMES.HOVER);
|
|
481
|
+
const target = parseTargetArgs(obj, TOOL_NAMES.HOVER);
|
|
482
|
+
const x = optNumber(obj, "x", TOOL_NAMES.HOVER);
|
|
483
|
+
const y = optNumber(obj, "y", TOOL_NAMES.HOVER);
|
|
484
|
+
if (x !== void 0 !== (y !== void 0)) {
|
|
485
|
+
throw new RelayError({
|
|
486
|
+
code: "invalid_arguments",
|
|
487
|
+
message: "chrome_hover: pass BOTH x and y, or neither (selector mode).",
|
|
488
|
+
tool: TOOL_NAMES.HOVER,
|
|
489
|
+
phase: "parse_arguments",
|
|
490
|
+
details: { received: { x: obj.x, y: obj.y } },
|
|
491
|
+
retryable: false
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
const ref = optString(obj, "ref", TOOL_NAMES.HOVER);
|
|
495
|
+
const selector = optString(obj, "selector", TOOL_NAMES.HOVER);
|
|
496
|
+
rejectMixedAddressing(TOOL_NAMES.HOVER, obj, { ref, selector, coords: x !== void 0 });
|
|
497
|
+
if (ref) {
|
|
498
|
+
return { ...target, kind: "ref", ref };
|
|
499
|
+
}
|
|
500
|
+
if (x !== void 0 && y !== void 0) {
|
|
501
|
+
return { ...target, kind: "coords", x, y };
|
|
502
|
+
}
|
|
503
|
+
if (selector) {
|
|
504
|
+
return { ...target, kind: "selector", selector };
|
|
505
|
+
}
|
|
506
|
+
throw new RelayError({
|
|
507
|
+
code: "invalid_arguments",
|
|
508
|
+
message: "chrome_hover requires a @ref, a selector, or x AND y.",
|
|
509
|
+
tool: TOOL_NAMES.HOVER,
|
|
510
|
+
phase: "parse_arguments",
|
|
511
|
+
details: { received: { ref: obj.ref, selector: obj.selector, x: obj.x, y: obj.y } },
|
|
512
|
+
retryable: false
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
var init_hover = __esm({
|
|
516
|
+
"../protocol/dist/args/hover.js"() {
|
|
517
|
+
"use strict";
|
|
518
|
+
init_dist();
|
|
519
|
+
init_shared();
|
|
520
|
+
init_simple();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// ../protocol/dist/args/network.js
|
|
525
|
+
function parseFilter(obj) {
|
|
526
|
+
const out = {};
|
|
527
|
+
const filter = optString(obj, "filter");
|
|
528
|
+
if (filter)
|
|
529
|
+
out.filter = filter;
|
|
530
|
+
const method = optString(obj, "method");
|
|
531
|
+
if (method)
|
|
532
|
+
out.method = method;
|
|
533
|
+
const limit = optNumber(obj, "limit");
|
|
534
|
+
if (limit !== void 0)
|
|
535
|
+
out.limit = limit;
|
|
536
|
+
const status = obj.status;
|
|
537
|
+
if (status !== void 0 && status !== null) {
|
|
538
|
+
if (typeof status !== "string" || !VALID_STATUSES.includes(status)) {
|
|
539
|
+
throw new RelayError({
|
|
540
|
+
code: "invalid_arguments",
|
|
541
|
+
message: `chrome_network: invalid status ${JSON.stringify(status)}. Expected one of: ${VALID_STATUSES.join(", ")}.`,
|
|
542
|
+
tool: TOOL_NAMES.NETWORK,
|
|
543
|
+
phase: "parse_status",
|
|
544
|
+
details: { received: status, validChoices: VALID_STATUSES },
|
|
545
|
+
retryable: false
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
out.status = status;
|
|
549
|
+
}
|
|
550
|
+
return out;
|
|
551
|
+
}
|
|
552
|
+
function parseChromeNetworkArgs(input) {
|
|
553
|
+
const obj = asObject(input, TOOL_NAMES.NETWORK);
|
|
554
|
+
const target = parseTargetArgs(obj);
|
|
555
|
+
const rawAction = obj.action;
|
|
556
|
+
const action = typeof rawAction === "string" ? rawAction : "read";
|
|
557
|
+
if (action === "clear") {
|
|
558
|
+
return { ...target, action: "clear" };
|
|
559
|
+
}
|
|
560
|
+
if (action === "body") {
|
|
561
|
+
const requestId = optString(obj, "requestId");
|
|
562
|
+
if (!requestId) {
|
|
563
|
+
throw new RelayError({
|
|
564
|
+
code: "invalid_arguments",
|
|
565
|
+
message: "chrome_network body requires `requestId` (a non-empty string).",
|
|
566
|
+
tool: TOOL_NAMES.NETWORK,
|
|
567
|
+
phase: "parse_arguments",
|
|
568
|
+
details: { field: "requestId", received: obj.requestId },
|
|
569
|
+
retryable: false
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
const out = {
|
|
573
|
+
...target,
|
|
574
|
+
action: "body",
|
|
575
|
+
requestId
|
|
576
|
+
};
|
|
577
|
+
const full = optBool(obj, "full");
|
|
578
|
+
if (full !== void 0)
|
|
579
|
+
out.full = full;
|
|
580
|
+
const head = optPositiveNumber(obj, "head", TOOL_NAMES.NETWORK);
|
|
581
|
+
if (head !== void 0)
|
|
582
|
+
out.head = head;
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
if (action === "har") {
|
|
586
|
+
const withBodies = optBool(obj, "withBodies");
|
|
587
|
+
const bestEffortBodies = optBool(obj, "bestEffortBodies");
|
|
588
|
+
return {
|
|
589
|
+
...target,
|
|
590
|
+
action: "har",
|
|
591
|
+
...withBodies !== void 0 ? { withBodies } : {},
|
|
592
|
+
...bestEffortBodies !== void 0 ? { bestEffortBodies } : {},
|
|
593
|
+
...parseFilter(obj)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
if (action === "read") {
|
|
597
|
+
return { ...target, action: "read", ...parseFilter(obj) };
|
|
598
|
+
}
|
|
599
|
+
throw new RelayError({
|
|
600
|
+
code: "invalid_arguments",
|
|
601
|
+
message: `chrome_network: unknown action "${action}". Expected read | clear | har | body.`,
|
|
602
|
+
tool: TOOL_NAMES.NETWORK,
|
|
603
|
+
phase: "parse_action",
|
|
604
|
+
details: { received: action, validChoices: ["read", "clear", "har", "body"] },
|
|
605
|
+
retryable: false
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
var VALID_STATUSES;
|
|
609
|
+
var init_network = __esm({
|
|
610
|
+
"../protocol/dist/args/network.js"() {
|
|
550
611
|
"use strict";
|
|
551
612
|
init_dist();
|
|
552
613
|
init_shared();
|
|
614
|
+
VALID_STATUSES = ["ok", "redirect", "client_error", "server_error", "failed"];
|
|
553
615
|
}
|
|
554
616
|
});
|
|
555
617
|
|
|
@@ -851,6 +913,170 @@ var init_multi = __esm({
|
|
|
851
913
|
}
|
|
852
914
|
});
|
|
853
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
|
+
|
|
854
1080
|
// ../protocol/dist/args/index.js
|
|
855
1081
|
function parseToolArgs(name, input) {
|
|
856
1082
|
switch (name) {
|
|
@@ -896,6 +1122,14 @@ function parseToolArgs(name, input) {
|
|
|
896
1122
|
return parseChromeHoverArgs(input);
|
|
897
1123
|
case "chrome_screencast":
|
|
898
1124
|
return parseChromeScreencastArgs(input);
|
|
1125
|
+
case "chrome_snapshot":
|
|
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);
|
|
899
1133
|
}
|
|
900
1134
|
const exhaustive = name;
|
|
901
1135
|
return exhaustive;
|
|
@@ -909,30 +1143,85 @@ var init_args = __esm({
|
|
|
909
1143
|
init_network();
|
|
910
1144
|
init_simple();
|
|
911
1145
|
init_multi();
|
|
1146
|
+
init_loop();
|
|
912
1147
|
init_navigate();
|
|
913
1148
|
init_hover();
|
|
914
1149
|
init_network();
|
|
915
1150
|
init_simple();
|
|
1151
|
+
init_loop();
|
|
916
1152
|
init_multi();
|
|
917
1153
|
}
|
|
918
1154
|
});
|
|
919
1155
|
|
|
920
|
-
// ../protocol/dist/
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1156
|
+
// ../protocol/dist/snapshot.js
|
|
1157
|
+
function parseRefToken(input) {
|
|
1158
|
+
const m = REF_TOKEN.exec(input.trim());
|
|
1159
|
+
return m ? m[1] : null;
|
|
1160
|
+
}
|
|
1161
|
+
function formatRefToken(ref) {
|
|
1162
|
+
return `@${ref}`;
|
|
1163
|
+
}
|
|
1164
|
+
function renderAttrs(node) {
|
|
1165
|
+
const parts = [];
|
|
1166
|
+
const attrs = node.attrs;
|
|
1167
|
+
if (attrs) {
|
|
1168
|
+
for (const key of ATTR_ORDER) {
|
|
1169
|
+
if (key === "url")
|
|
1170
|
+
continue;
|
|
1171
|
+
const v = attrs[key];
|
|
1172
|
+
if (v === void 0)
|
|
1173
|
+
continue;
|
|
1174
|
+
if (v === true)
|
|
1175
|
+
parts.push(key);
|
|
1176
|
+
else
|
|
1177
|
+
parts.push(`${key}=${v}`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (node.ref)
|
|
1181
|
+
parts.push(`ref=${node.ref}`);
|
|
1182
|
+
if (attrs?.url)
|
|
1183
|
+
parts.push(`url=${attrs.url}`);
|
|
1184
|
+
return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
|
|
1185
|
+
}
|
|
1186
|
+
function renderNode(node, depth, out) {
|
|
1187
|
+
const indent = " ".repeat(depth);
|
|
1188
|
+
let line = `${indent}- ${node.role}`;
|
|
1189
|
+
if (node.name)
|
|
1190
|
+
line += ` ${JSON.stringify(node.name)}`;
|
|
1191
|
+
line += renderAttrs(node);
|
|
1192
|
+
if (node.value !== void 0 && node.value !== node.name) {
|
|
1193
|
+
line += `: ${node.value}`;
|
|
1194
|
+
}
|
|
1195
|
+
out.push(line);
|
|
1196
|
+
for (const child of node.children ?? []) {
|
|
1197
|
+
renderNode(child, depth + 1, out);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
function renderSnapshot(data) {
|
|
1201
|
+
const nodes = data.nodes ?? [];
|
|
1202
|
+
const out = [`Page: ${data.title ?? ""}`, `URL: ${data.url ?? ""}`, `Tab: ${data.tabId ?? "?"}`, ""];
|
|
1203
|
+
for (const node of nodes)
|
|
1204
|
+
renderNode(node, 0, out);
|
|
1205
|
+
if (nodes.length === 0)
|
|
1206
|
+
out.push("(empty snapshot \u2014 page may still be loading)");
|
|
1207
|
+
return out.join("\n");
|
|
1208
|
+
}
|
|
1209
|
+
var REF_TOKEN, ATTR_ORDER;
|
|
1210
|
+
var init_snapshot = __esm({
|
|
1211
|
+
"../protocol/dist/snapshot.js"() {
|
|
924
1212
|
"use strict";
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1213
|
+
REF_TOKEN = /^@(e\d+)$/;
|
|
1214
|
+
ATTR_ORDER = [
|
|
1215
|
+
"level",
|
|
1216
|
+
"checked",
|
|
1217
|
+
"expanded",
|
|
1218
|
+
"selected",
|
|
1219
|
+
"disabled",
|
|
1220
|
+
"required",
|
|
1221
|
+
"readonly",
|
|
1222
|
+
"pressed",
|
|
1223
|
+
"url"
|
|
1224
|
+
];
|
|
936
1225
|
}
|
|
937
1226
|
});
|
|
938
1227
|
|
|
@@ -952,27 +1241,36 @@ __export(dist_exports, {
|
|
|
952
1241
|
DEFAULT_PING_TIMEOUT_MS: () => DEFAULT_PING_TIMEOUT_MS,
|
|
953
1242
|
DEFAULT_READY_TIMEOUT_MS: () => DEFAULT_READY_TIMEOUT_MS,
|
|
954
1243
|
DEFAULT_TOOL_CALL_TIMEOUT_MS: () => DEFAULT_TOOL_CALL_TIMEOUT_MS,
|
|
1244
|
+
DEFAULT_WAIT_TIMEOUT_MS: () => DEFAULT_WAIT_TIMEOUT_MS,
|
|
955
1245
|
LEGACY_DEV_EXTENSION_ID: () => LEGACY_DEV_EXTENSION_ID,
|
|
956
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,
|
|
957
1250
|
NATIVE_HOST_NAME: () => NATIVE_HOST_NAME,
|
|
1251
|
+
NETWORKIDLE_QUIET_MS: () => NETWORKIDLE_QUIET_MS,
|
|
958
1252
|
NETWORK_BUFFER_MAX_BYTES: () => NETWORK_BUFFER_MAX_BYTES,
|
|
959
1253
|
NETWORK_BUFFER_MAX_ENTRIES: () => NETWORK_BUFFER_MAX_ENTRIES,
|
|
960
1254
|
RelayError: () => RelayError,
|
|
961
1255
|
TOOL_NAMES: () => TOOL_NAMES,
|
|
1256
|
+
WAIT_POLL_INTERVAL_MS: () => WAIT_POLL_INTERVAL_MS,
|
|
962
1257
|
asObject: () => asObject,
|
|
963
1258
|
coerceTabId: () => coerceTabId,
|
|
1259
|
+
formatRefToken: () => formatRefToken,
|
|
964
1260
|
optBool: () => optBool,
|
|
965
1261
|
optNonNegativeNumber: () => optNonNegativeNumber,
|
|
966
1262
|
optNumber: () => optNumber,
|
|
967
1263
|
optPositiveNumber: () => optPositiveNumber,
|
|
968
1264
|
optString: () => optString,
|
|
969
1265
|
parseChromeAxArgs: () => parseChromeAxArgs,
|
|
1266
|
+
parseChromeBatchArgs: () => parseChromeBatchArgs,
|
|
970
1267
|
parseChromeClickArgs: () => parseChromeClickArgs,
|
|
971
1268
|
parseChromeClickAxArgs: () => parseChromeClickAxArgs,
|
|
972
1269
|
parseChromeCloseTabsArgs: () => parseChromeCloseTabsArgs,
|
|
973
1270
|
parseChromeConsoleArgs: () => parseChromeConsoleArgs,
|
|
974
1271
|
parseChromeEvaluateArgs: () => parseChromeEvaluateArgs,
|
|
975
1272
|
parseChromeFillArgs: () => parseChromeFillArgs,
|
|
1273
|
+
parseChromeGetArgs: () => parseChromeGetArgs,
|
|
976
1274
|
parseChromeGroupArgs: () => parseChromeGroupArgs,
|
|
977
1275
|
parseChromeHoverArgs: () => parseChromeHoverArgs,
|
|
978
1276
|
parseChromeKeyboardArgs: () => parseChromeKeyboardArgs,
|
|
@@ -982,13 +1280,18 @@ __export(dist_exports, {
|
|
|
982
1280
|
parseChromeScreencastArgs: () => parseChromeScreencastArgs,
|
|
983
1281
|
parseChromeScreenshotArgs: () => parseChromeScreenshotArgs,
|
|
984
1282
|
parseChromeSelfReloadArgs: () => parseChromeSelfReloadArgs,
|
|
1283
|
+
parseChromeSnapshotArgs: () => parseChromeSnapshotArgs,
|
|
985
1284
|
parseChromeSwitchTabArgs: () => parseChromeSwitchTabArgs,
|
|
986
1285
|
parseChromeTypeArgs: () => parseChromeTypeArgs,
|
|
987
1286
|
parseChromeViewportArgs: () => parseChromeViewportArgs,
|
|
1287
|
+
parseChromeWaitArgs: () => parseChromeWaitArgs,
|
|
988
1288
|
parseChromeWorkspaceArgs: () => parseChromeWorkspaceArgs,
|
|
989
1289
|
parseGetWindowsAndTabsArgs: () => parseGetWindowsAndTabsArgs,
|
|
1290
|
+
parseRefToken: () => parseRefToken,
|
|
990
1291
|
parseTargetArgs: () => parseTargetArgs,
|
|
991
1292
|
parseToolArgs: () => parseToolArgs,
|
|
1293
|
+
rejectMixedAddressing: () => rejectMixedAddressing,
|
|
1294
|
+
renderSnapshot: () => renderSnapshot,
|
|
992
1295
|
requireString: () => requireString,
|
|
993
1296
|
toBridgeError: () => toBridgeError
|
|
994
1297
|
});
|
|
@@ -1010,6 +1313,7 @@ var init_dist = __esm({
|
|
|
1010
1313
|
"use strict";
|
|
1011
1314
|
init_args();
|
|
1012
1315
|
init_limits();
|
|
1316
|
+
init_snapshot();
|
|
1013
1317
|
NATIVE_HOST_NAME = "dev.chrome_relay.native_host";
|
|
1014
1318
|
DEFAULT_HTTP_PORT = 12122;
|
|
1015
1319
|
CHROME_WEB_STORE_EXTENSION_ID = "cpdiapbifblhlcpnmlmfpgfjlacebokb";
|
|
@@ -1066,7 +1370,21 @@ var init_dist = __esm({
|
|
|
1066
1370
|
// CSS transitions, fade-ins, focus-ring motion) — at the cost of requiring
|
|
1067
1371
|
// the tab to be ACTIVE (Chrome doesn't paint backgrounded tabs). See
|
|
1068
1372
|
// docs/recording.md for the active-tab matrix.
|
|
1069
|
-
SCREENCAST: "chrome_screencast"
|
|
1373
|
+
SCREENCAST: "chrome_screencast",
|
|
1374
|
+
// Unified page snapshot (adoption-spec Change 1) — AX tree + cursor-
|
|
1375
|
+
// interactive sweep, one ref space, compact text rendered CLI-side.
|
|
1376
|
+
// Supersedes chrome_read_page and chrome_ax, which now alias to it.
|
|
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"
|
|
1070
1388
|
};
|
|
1071
1389
|
RelayError = class extends Error {
|
|
1072
1390
|
code;
|
|
@@ -1101,7 +1419,7 @@ var init_dist = __esm({
|
|
|
1101
1419
|
import { Command } from "commander";
|
|
1102
1420
|
|
|
1103
1421
|
// src/index.ts
|
|
1104
|
-
var CHROME_RELAY_VERSION = true ? "0.
|
|
1422
|
+
var CHROME_RELAY_VERSION = true ? "0.7.0" : "0.0.0-dev";
|
|
1105
1423
|
|
|
1106
1424
|
// src/commands/shared.ts
|
|
1107
1425
|
init_dist();
|
|
@@ -1281,6 +1599,55 @@ function getChromiumBrowserTargets() {
|
|
|
1281
1599
|
{ label: "Opera", installRoot: path.join(config, "opera"), manifestDir: path.join(config, "opera/NativeMessagingHosts") }
|
|
1282
1600
|
];
|
|
1283
1601
|
}
|
|
1602
|
+
if (process.platform === "win32") {
|
|
1603
|
+
const local = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
|
1604
|
+
const roaming = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
1605
|
+
const manifestBase = path.join(APP_DIR, "NativeMessagingHosts");
|
|
1606
|
+
return [
|
|
1607
|
+
{
|
|
1608
|
+
label: "Google Chrome",
|
|
1609
|
+
installRoot: path.join(local, "Google", "Chrome", "User Data"),
|
|
1610
|
+
manifestDir: path.join(manifestBase, "Google Chrome"),
|
|
1611
|
+
registryKey: `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
label: "Google Chrome Canary",
|
|
1615
|
+
installRoot: path.join(local, "Google", "Chrome SxS", "User Data"),
|
|
1616
|
+
manifestDir: path.join(manifestBase, "Google Chrome Canary"),
|
|
1617
|
+
registryKey: `HKCU\\Software\\Google\\Chrome SxS\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
label: "Chromium",
|
|
1621
|
+
installRoot: path.join(local, "Chromium", "User Data"),
|
|
1622
|
+
manifestDir: path.join(manifestBase, "Chromium"),
|
|
1623
|
+
registryKey: `HKCU\\Software\\Chromium\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
label: "Microsoft Edge",
|
|
1627
|
+
installRoot: path.join(local, "Microsoft", "Edge", "User Data"),
|
|
1628
|
+
manifestDir: path.join(manifestBase, "Microsoft Edge"),
|
|
1629
|
+
registryKey: `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
label: "Brave",
|
|
1633
|
+
installRoot: path.join(local, "BraveSoftware", "Brave-Browser", "User Data"),
|
|
1634
|
+
manifestDir: path.join(manifestBase, "Brave"),
|
|
1635
|
+
registryKey: `HKCU\\Software\\BraveSoftware\\Brave-Browser\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
label: "Vivaldi",
|
|
1639
|
+
installRoot: path.join(local, "Vivaldi", "User Data"),
|
|
1640
|
+
manifestDir: path.join(manifestBase, "Vivaldi"),
|
|
1641
|
+
registryKey: `HKCU\\Software\\Vivaldi\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
label: "Opera",
|
|
1645
|
+
installRoot: path.join(roaming, "Opera Software", "Opera Stable"),
|
|
1646
|
+
manifestDir: path.join(manifestBase, "Opera"),
|
|
1647
|
+
registryKey: `HKCU\\Software\\Opera Software\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`
|
|
1648
|
+
}
|
|
1649
|
+
];
|
|
1650
|
+
}
|
|
1284
1651
|
throw new Error(`Unsupported platform for install: ${process.platform}`);
|
|
1285
1652
|
}
|
|
1286
1653
|
async function pathExists(p) {
|
|
@@ -1306,6 +1673,14 @@ function getDistDir() {
|
|
|
1306
1673
|
}
|
|
1307
1674
|
async function writeWrapperScript(hostPath) {
|
|
1308
1675
|
await mkdir(APP_DIR, { recursive: true });
|
|
1676
|
+
if (process.platform === "win32") {
|
|
1677
|
+
const wrapperPath2 = path.join(APP_DIR, "run-host.cmd");
|
|
1678
|
+
const content2 = `@echo off\r
|
|
1679
|
+
"${process.execPath}" "${hostPath}"\r
|
|
1680
|
+
`;
|
|
1681
|
+
await writeFile(wrapperPath2, content2, "utf8");
|
|
1682
|
+
return wrapperPath2;
|
|
1683
|
+
}
|
|
1309
1684
|
const wrapperPath = path.join(APP_DIR, "run-host.sh");
|
|
1310
1685
|
const content = `#!/bin/sh
|
|
1311
1686
|
exec "${process.execPath}" "${hostPath}"
|
|
@@ -1314,6 +1689,31 @@ exec "${process.execPath}" "${hostPath}"
|
|
|
1314
1689
|
await chmod(wrapperPath, 493);
|
|
1315
1690
|
return wrapperPath;
|
|
1316
1691
|
}
|
|
1692
|
+
function registerWindowsNativeHost(registryKey, manifestPath) {
|
|
1693
|
+
const res = spawnSync("reg.exe", [
|
|
1694
|
+
"ADD",
|
|
1695
|
+
registryKey,
|
|
1696
|
+
"/ve",
|
|
1697
|
+
"/t",
|
|
1698
|
+
"REG_SZ",
|
|
1699
|
+
"/d",
|
|
1700
|
+
manifestPath,
|
|
1701
|
+
"/f"
|
|
1702
|
+
], { encoding: "utf8" });
|
|
1703
|
+
if (res.status !== 0) {
|
|
1704
|
+
const detail = (res.stderr || res.stdout || "").trim();
|
|
1705
|
+
throw new Error(`failed to register ${registryKey}${detail ? `: ${detail}` : ""}`);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
function readWindowsNativeHostRegistry(registryKey) {
|
|
1709
|
+
const res = spawnSync("reg.exe", ["QUERY", registryKey, "/ve"], { encoding: "utf8" });
|
|
1710
|
+
if (res.status !== 0 || !res.stdout) return null;
|
|
1711
|
+
for (const line of res.stdout.split("\n")) {
|
|
1712
|
+
const match = line.match(/REG_SZ\s+(.+?)\s*$/);
|
|
1713
|
+
if (match?.[1]) return match[1].trim();
|
|
1714
|
+
}
|
|
1715
|
+
return null;
|
|
1716
|
+
}
|
|
1317
1717
|
async function writeManifestsForBrowsers(wrapperPath, browsers) {
|
|
1318
1718
|
const manifest = {
|
|
1319
1719
|
name: NATIVE_HOST_NAME,
|
|
@@ -1329,7 +1729,11 @@ async function writeManifestsForBrowsers(wrapperPath, browsers) {
|
|
|
1329
1729
|
await mkdir(target.manifestDir, { recursive: true });
|
|
1330
1730
|
const manifestPath = path.join(target.manifestDir, `${NATIVE_HOST_NAME}.json`);
|
|
1331
1731
|
await writeFile(manifestPath, body, "utf8");
|
|
1332
|
-
|
|
1732
|
+
if (process.platform === "win32") {
|
|
1733
|
+
if (!target.registryKey) throw new Error(`missing registry key for ${target.label}`);
|
|
1734
|
+
registerWindowsNativeHost(target.registryKey, manifestPath);
|
|
1735
|
+
}
|
|
1736
|
+
written.push({ browser: target.label, manifestPath, registryKey: target.registryKey });
|
|
1333
1737
|
}
|
|
1334
1738
|
return written;
|
|
1335
1739
|
}
|
|
@@ -1373,6 +1777,9 @@ async function runInstall() {
|
|
|
1373
1777
|
console.log(`Manifests written:`);
|
|
1374
1778
|
for (const m of writtenManifests) {
|
|
1375
1779
|
console.log(` \u2022 ${m.browser}: ${m.manifestPath}`);
|
|
1780
|
+
if (m.registryKey) {
|
|
1781
|
+
console.log(` registry: ${m.registryKey}`);
|
|
1782
|
+
}
|
|
1376
1783
|
}
|
|
1377
1784
|
console.log(`Local bridge port: ${DEFAULT_HTTP_PORT}`);
|
|
1378
1785
|
console.log(`Allowed extension IDs: ${formatKnownExtensionIds()}`);
|
|
@@ -1382,7 +1789,7 @@ async function runInstall() {
|
|
|
1382
1789
|
}
|
|
1383
1790
|
async function runDoctor() {
|
|
1384
1791
|
try {
|
|
1385
|
-
const wrapperPath = path.join(APP_DIR, "run-host.sh");
|
|
1792
|
+
const wrapperPath = path.join(APP_DIR, process.platform === "win32" ? "run-host.cmd" : "run-host.sh");
|
|
1386
1793
|
await stat(wrapperPath);
|
|
1387
1794
|
console.log(`Wrapper present: yes`);
|
|
1388
1795
|
const installed = await getInstalledBrowsers();
|
|
@@ -1402,6 +1809,14 @@ async function runDoctor() {
|
|
|
1402
1809
|
console.log(` \u2022 ${target.label}: manifest MISSING (${manifestPath})`);
|
|
1403
1810
|
continue;
|
|
1404
1811
|
}
|
|
1812
|
+
if (process.platform === "win32" && target.registryKey) {
|
|
1813
|
+
const registered = readWindowsNativeHostRegistry(target.registryKey);
|
|
1814
|
+
if (path.normalize(registered || "") !== path.normalize(manifestPath)) {
|
|
1815
|
+
allHealthy = false;
|
|
1816
|
+
console.log(` \u2022 ${target.label}: registry MISSING/STALE (${target.registryKey})`);
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1405
1820
|
try {
|
|
1406
1821
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
1407
1822
|
const allowedOrigins = Array.isArray(manifest.allowed_origins) ? manifest.allowed_origins : [];
|
|
@@ -1441,6 +1856,30 @@ async function runDoctor() {
|
|
|
1441
1856
|
|
|
1442
1857
|
// src/release-notes.ts
|
|
1443
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
|
+
],
|
|
1868
|
+
"0.6.0": [
|
|
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.",
|
|
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.',
|
|
1871
|
+
"Refs die on real navigation, deliberately: Chromium reuses backendNodeId integers in the new document, so a stale ref could silently click an unrelated element. Dead refs return the new `error.code = stale_ref` with a re-snapshot hint. SPA route changes (pushState) keep refs alive.",
|
|
1872
|
+
"BREAKING: `read` and `ax` output changed. They are now aliases for `chrome_snapshot` and return the unified snapshot format (compact text / SnapshotData JSON) \u2014 the old `{elements: [{selector, bounds, ...}]}` and CompactAxNode shapes are gone, deliberately: keeping them would mean keeping the deleted walkers in parallel. Both print a deprecation notice; alias removal lands next minor. `click-ax` still works on raw backendNodeIds (now visible in `snapshot --json` refs).",
|
|
1873
|
+
"Ref clicks hit-test before dispatch: if an unrelated element (overlay, sticky header, modal) owns the click point, the click fails with the new `error.code = click_intercepted` naming the interceptor \u2014 instead of reporting success while the overlay ate the click. Same-lineage hits (inner text, wrapping label) pass; fill/type skip the check (writing to a covered input is legitimate).",
|
|
1874
|
+
"`snapshot --scope <css>` now bounds the cursor-interactive sweep too \u2014 a scoped snapshot never returns actionable refs for elements outside the scope subtree.",
|
|
1875
|
+
"Error hygiene: in-page failures (bad selector, malformed CSS, zero-size element, unfocusable element) now map to structured codes (element_not_found, invalid_arguments) instead of internal_error with a raw JS stack.",
|
|
1876
|
+
"Snapshot flags: -i (ref-bearing only), -d <n> (depth cap), -s <css> (scope to subtree), -u (include hrefs), --json (structured envelope incl. the refs map with backendNodeIds)."
|
|
1877
|
+
],
|
|
1878
|
+
"0.5.23": [
|
|
1879
|
+
"Windows native-host install. `chrome-relay install` now supports `win32`: it writes a `run-host.cmd` wrapper, writes browser-specific native-messaging manifests under `~/.chrome-relay/NativeMessagingHosts`, and registers each manifest through HKCU registry keys.",
|
|
1880
|
+
"Detected Windows browsers: Chrome, Chrome Canary, Chromium, Edge, Brave, Vivaldi, and Opera. Detection uses the browser profile directory; if none are detected, install falls back to Chrome so a later Chrome install can find the host after re-running install.",
|
|
1881
|
+
"`chrome-relay doctor` now checks the Windows wrapper and registry entries, so stale or missing native-host registration points at the failing browser instead of only showing a generic extension connection failure."
|
|
1882
|
+
],
|
|
1444
1883
|
"0.5.22": [
|
|
1445
1884
|
"Multi-browser install. `chrome-relay install` now writes the native-messaging manifest into every detected Chromium-fork browser's NativeMessagingHosts dir, not just Google Chrome's. Detected: Chrome, Chrome Canary, Chromium, Edge, Brave, Vivaldi, Arc, Opera (macOS + Linux paths). Detection is parent-dir existence \u2014 we never speculatively create profile dirs for browsers that aren't installed.",
|
|
1446
1885
|
"Why this matters: the extension installs fine via Chrome Web Store in any Chromium fork, but the bridge silently failed because the host manifest was only at Chrome's path. Arc + Brave users hit `connectNative()` errors with no obvious cause.",
|
|
@@ -1724,10 +2163,10 @@ function registerNavigation(ctx) {
|
|
|
1724
2163
|
`
|
|
1725
2164
|
|
|
1726
2165
|
Examples:
|
|
1727
|
-
chrome-relay navigate "https://
|
|
1728
|
-
chrome-relay navigate --tab 123 "https://
|
|
1729
|
-
chrome-relay navigate "https://
|
|
1730
|
-
chrome-relay navigate "https://
|
|
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
|
|
1731
2170
|
|
|
1732
2171
|
By default chrome-relay never steals focus \u2014 navigated tabs (new or
|
|
1733
2172
|
existing) stay in whatever state they're in. Pass --active when you
|
|
@@ -1738,7 +2177,7 @@ actually want the user looking at the page.
|
|
|
1738
2177
|
if (/^\d+$/.test(url)) {
|
|
1739
2178
|
process.stderr.write(
|
|
1740
2179
|
`navigate expects a URL, but "${url}" looks like a tab ID.
|
|
1741
|
-
Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate --tab ${url} https://
|
|
2180
|
+
Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate --tab ${url} https://chrome-relay.kushalsm.com" to navigate it.
|
|
1742
2181
|
`
|
|
1743
2182
|
);
|
|
1744
2183
|
process.exit(1);
|
|
@@ -1761,34 +2200,48 @@ Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate
|
|
|
1761
2200
|
}
|
|
1762
2201
|
|
|
1763
2202
|
// src/commands/input.ts
|
|
2203
|
+
init_dist();
|
|
2204
|
+
function addressArg(value) {
|
|
2205
|
+
const ref = parseRefToken(value);
|
|
2206
|
+
return ref ? { ref } : { selector: value };
|
|
2207
|
+
}
|
|
1764
2208
|
function registerInput(ctx) {
|
|
1765
2209
|
const { program, withBase, run } = ctx;
|
|
1766
2210
|
tabOpt(
|
|
1767
|
-
program.command("click [
|
|
2211
|
+
program.command("click [target]").description("Click an element. Pass a @ref from `snapshot`, a CSS selector, OR --x/--y coordinates.").option("--x <px>", "explicit x coordinate (CSS pixels); requires --y", (v) => Number(v)).option("--y <px>", "explicit y coordinate (CSS pixels); requires --x", (v) => Number(v)).addHelpText(
|
|
1768
2212
|
"after",
|
|
1769
2213
|
`
|
|
1770
2214
|
|
|
1771
2215
|
Examples:
|
|
2216
|
+
chrome-relay click @e12
|
|
1772
2217
|
chrome-relay click 'button[aria-label="Save"]'
|
|
1773
2218
|
chrome-relay click --tab 123 --x 1327 --y 771
|
|
1774
2219
|
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2220
|
+
Prefer @refs from \`chrome-relay snapshot\` \u2014 they carry their own tab and
|
|
2221
|
+
survive DOM churn (backendNodeId + role/name heal). CSS selectors for
|
|
2222
|
+
elements you know statically. Coordinates for canvas/SVG chart internals
|
|
2223
|
+
where no DOM handle exists. See docs/clicking-strategies.md.
|
|
1779
2224
|
`
|
|
1780
2225
|
)
|
|
1781
|
-
).action(async (
|
|
2226
|
+
).action(async (target, opts) => {
|
|
1782
2227
|
const extras = {};
|
|
1783
|
-
if (
|
|
2228
|
+
if (target) Object.assign(extras, addressArg(target));
|
|
1784
2229
|
if (typeof opts.x === "number") extras.x = opts.x;
|
|
1785
2230
|
if (typeof opts.y === "number") extras.y = opts.y;
|
|
1786
2231
|
await run("chrome_click_element", withBase(opts, extras));
|
|
1787
2232
|
});
|
|
1788
2233
|
tabOpt(
|
|
1789
|
-
program.command("fill <
|
|
1790
|
-
|
|
1791
|
-
|
|
2234
|
+
program.command("fill <target> <value>").description("Fill an input or textarea. Target is a @ref from `snapshot` or a CSS selector.").addHelpText(
|
|
2235
|
+
"after",
|
|
2236
|
+
`
|
|
2237
|
+
|
|
2238
|
+
Examples:
|
|
2239
|
+
chrome-relay fill @e4 "kushal@kushalsm.com"
|
|
2240
|
+
chrome-relay fill 'input[name="email"]' "kushal@kushalsm.com"
|
|
2241
|
+
`
|
|
2242
|
+
)
|
|
2243
|
+
).action(async (target, value, opts) => {
|
|
2244
|
+
await run("chrome_fill_or_select", withBase(opts, { ...addressArg(target), value }));
|
|
1792
2245
|
});
|
|
1793
2246
|
tabOpt(
|
|
1794
2247
|
program.command("keys <keys>").description("Press a single key or chord via trusted CDP input (e.g. Enter, Cmd+K).").addHelpText(
|
|
@@ -1808,7 +2261,7 @@ For typing text into a field, use \`chrome-relay type\` instead.
|
|
|
1808
2261
|
await run("chrome_keyboard", withBase(opts, { keys }));
|
|
1809
2262
|
});
|
|
1810
2263
|
tabOpt(
|
|
1811
|
-
program.command("type <text>").description("Insert text via trusted CDP input. Works in contenteditable / Draft.js / Lexical.").option("-s, --selector <
|
|
2264
|
+
program.command("type <text>").description("Insert text via trusted CDP input. Works in contenteditable / Draft.js / Lexical.").option("-s, --selector <target>", "focus this element first (@ref or CSS selector)").addHelpText(
|
|
1812
2265
|
"after",
|
|
1813
2266
|
`
|
|
1814
2267
|
|
|
@@ -1825,7 +2278,7 @@ When to pick which:
|
|
|
1825
2278
|
)
|
|
1826
2279
|
).action(async (text, opts) => {
|
|
1827
2280
|
const extras = { text };
|
|
1828
|
-
if (opts.selector) extras
|
|
2281
|
+
if (opts.selector) Object.assign(extras, addressArg(opts.selector));
|
|
1829
2282
|
await run("chrome_type", withBase(opts, extras));
|
|
1830
2283
|
});
|
|
1831
2284
|
tabOpt(
|
|
@@ -1850,7 +2303,7 @@ Notes:
|
|
|
1850
2303
|
await run("chrome_evaluate", withBase(opts, extras));
|
|
1851
2304
|
});
|
|
1852
2305
|
tabOpt(
|
|
1853
|
-
program.command("hover [
|
|
2306
|
+
program.command("hover [target]").description("Move the pointer over an element (@ref or CSS selector) 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(
|
|
1854
2307
|
"after",
|
|
1855
2308
|
`
|
|
1856
2309
|
|
|
@@ -1862,9 +2315,9 @@ Use before screencast to capture hover-driven micro-states (button glow,
|
|
|
1862
2315
|
tooltip appearance, etc.) that a bare click would skip past too quickly.
|
|
1863
2316
|
`
|
|
1864
2317
|
)
|
|
1865
|
-
).action(async (
|
|
2318
|
+
).action(async (target, opts) => {
|
|
1866
2319
|
const extras = {};
|
|
1867
|
-
if (
|
|
2320
|
+
if (target) Object.assign(extras, addressArg(target));
|
|
1868
2321
|
if (typeof opts.x === "number") extras.x = opts.x;
|
|
1869
2322
|
if (typeof opts.y === "number") extras.y = opts.y;
|
|
1870
2323
|
await run("chrome_hover", withBase(opts, extras));
|
|
@@ -1872,9 +2325,88 @@ tooltip appearance, etc.) that a bare click would skip past too quickly.
|
|
|
1872
2325
|
}
|
|
1873
2326
|
|
|
1874
2327
|
// src/commands/capture.ts
|
|
2328
|
+
init_dist();
|
|
1875
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
|
+
}
|
|
2356
|
+
function printSnapshot(result, asJson) {
|
|
2357
|
+
if (asJson) {
|
|
2358
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
process.stdout.write(renderSnapshot(result) + "\n");
|
|
2362
|
+
}
|
|
2363
|
+
function exitWithError(error) {
|
|
2364
|
+
if (error instanceof RelayError) {
|
|
2365
|
+
process.stderr.write(error.message + "\n");
|
|
2366
|
+
process.stderr.write(JSON.stringify({ relayError: error.toBridgeError() }, null, 2) + "\n");
|
|
2367
|
+
} else {
|
|
2368
|
+
process.stderr.write((error instanceof Error ? error.message : String(error)) + "\n");
|
|
2369
|
+
}
|
|
2370
|
+
process.exit(1);
|
|
2371
|
+
}
|
|
1876
2372
|
function registerCapture(ctx) {
|
|
1877
2373
|
const { program, withBase, run } = ctx;
|
|
2374
|
+
tabOpt(
|
|
2375
|
+
program.command("snapshot").description("Page snapshot with actionable @refs \u2014 accessibility tree + cursor-interactive sweep, compact text.").option("-i, --interactive", "only ref-bearing elements (buttons, links, inputs, named content, clickables)").option("-d, --depth <n>", "truncate the tree at this depth", (v) => Number(v)).option("-s, --scope <css>", "restrict to the subtree of the first CSS match").option("-u, --urls", "include link hrefs as url= attrs").option("--diff", "print only what changed since the previous snapshot of this tab (~100 tokens instead of a re-read)").option("--json", "structured output: { title, url, tabId, nodes, refs }").addHelpText(
|
|
2376
|
+
"after",
|
|
2377
|
+
`
|
|
2378
|
+
|
|
2379
|
+
Examples:
|
|
2380
|
+
chrome-relay snapshot -i # see the page, get @refs
|
|
2381
|
+
chrome-relay click @e12 # act on a ref \u2014 no --tab needed
|
|
2382
|
+
chrome-relay snapshot -i -s "#main" # scope to a subtree
|
|
2383
|
+
chrome-relay snapshot --json # machine-readable envelope
|
|
2384
|
+
|
|
2385
|
+
The core loop: snapshot -i \u2192 click/fill @eN \u2192 snapshot -i again after the
|
|
2386
|
+
page changes. Refs carry their own tab and heal across DOM churn
|
|
2387
|
+
(backendNodeId fast path + role/name re-find); a dead ref returns
|
|
2388
|
+
error.code = stale_ref, which means: re-run snapshot.
|
|
2389
|
+
`
|
|
2390
|
+
)
|
|
2391
|
+
).action(async (opts) => {
|
|
2392
|
+
const extras = {};
|
|
2393
|
+
if (opts.interactive) extras.interactiveOnly = true;
|
|
2394
|
+
if (typeof opts.depth === "number") extras.depth = opts.depth;
|
|
2395
|
+
if (opts.scope) extras.scope = opts.scope;
|
|
2396
|
+
if (opts.urls) extras.urls = true;
|
|
2397
|
+
if (opts.diff) extras.diff = true;
|
|
2398
|
+
try {
|
|
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
|
+
}
|
|
2405
|
+
printSnapshot(result, opts.json === true);
|
|
2406
|
+
} catch (error) {
|
|
2407
|
+
exitWithError(error);
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
1878
2410
|
tabOpt(
|
|
1879
2411
|
program.command("screenshot").description("Capture a screenshot of any tab without activating it.").option("--full", "capture beyond the viewport (full page)").option("--bbox <rect>", "capture a region: 'x,y,width,height' (pixels)").option("--selector <css>", "capture the bounding box of a CSS selector").option("--padding <px>", "pixels of padding around --selector region", (v) => Number(v)).option("--max-edge <px>", "downscale so longer edge \u2264 this many pixels (no default; opt-in)", (v) => Number(v)).option("-o, --out <path>", "save image to path (base64 PNG decoded)").addHelpText(
|
|
1880
2412
|
"after",
|
|
@@ -1914,43 +2446,37 @@ full-tab screenshot when an agent only needs to see one component.
|
|
|
1914
2446
|
}
|
|
1915
2447
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1916
2448
|
} catch (error) {
|
|
1917
|
-
|
|
1918
|
-
(error instanceof Error ? error.message : String(error)) + "\n"
|
|
1919
|
-
);
|
|
1920
|
-
process.exit(1);
|
|
2449
|
+
exitWithError(error);
|
|
1921
2450
|
}
|
|
1922
2451
|
});
|
|
1923
2452
|
tabOpt(
|
|
1924
|
-
program.command("read").description("
|
|
2453
|
+
program.command("read").description("[deprecated \u2014 use `snapshot`] Alias for the unified snapshot.").option("-i, --interactive", "only ref-bearing elements").option("--json", "structured output")
|
|
1925
2454
|
).action(async (opts) => {
|
|
2455
|
+
process.stderr.write("[chrome-relay] deprecated: `read` is now an alias for `snapshot` (new output format). Use `chrome-relay snapshot`.\n");
|
|
1926
2456
|
const extras = {};
|
|
1927
2457
|
if (opts.interactive) extras.interactiveOnly = true;
|
|
1928
|
-
|
|
2458
|
+
try {
|
|
2459
|
+
const result = await callTool("chrome_read_page", withBase(opts, extras));
|
|
2460
|
+
printSnapshot(result, opts.json === true);
|
|
2461
|
+
} catch (error) {
|
|
2462
|
+
exitWithError(error);
|
|
2463
|
+
}
|
|
1929
2464
|
});
|
|
1930
2465
|
tabOpt(
|
|
1931
|
-
program.command("ax").description("
|
|
1932
|
-
"after",
|
|
1933
|
-
`
|
|
1934
|
-
|
|
1935
|
-
Examples:
|
|
1936
|
-
chrome-relay ax --tab 123
|
|
1937
|
-
chrome-relay ax --tab 123 --interactive-only
|
|
1938
|
-
chrome-relay ax --tab 123 --root main --interactive-only
|
|
1939
|
-
|
|
1940
|
-
Notes:
|
|
1941
|
-
Each node carries an "id" \u2014 that's the backendDOMNodeId. Pass it to
|
|
1942
|
-
\`chrome-relay click-ax --node <id>\` to click without a CSS selector.
|
|
1943
|
-
`
|
|
1944
|
-
)
|
|
2466
|
+
program.command("ax").description("[deprecated \u2014 use `snapshot`] Alias for the unified snapshot.").option("-i, --interactive-only", "only ref-bearing elements").option("--root <role>", "(ignored \u2014 use `snapshot --scope <css>`)").option("--include-subframes", "(ignored \u2014 snapshot is top-frame only)").option("--json", "structured output")
|
|
1945
2467
|
).action(async (opts) => {
|
|
2468
|
+
process.stderr.write("[chrome-relay] deprecated: `ax` is now an alias for `snapshot` (new output format, one ref space). Use `chrome-relay snapshot`.\n");
|
|
1946
2469
|
const extras = {};
|
|
1947
2470
|
if (opts.interactiveOnly) extras.interactiveOnly = true;
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2471
|
+
try {
|
|
2472
|
+
const result = await callTool("chrome_ax", withBase(opts, extras));
|
|
2473
|
+
printSnapshot(result, opts.json === true);
|
|
2474
|
+
} catch (error) {
|
|
2475
|
+
exitWithError(error);
|
|
2476
|
+
}
|
|
1951
2477
|
});
|
|
1952
2478
|
tabOpt(
|
|
1953
|
-
program.command("click-ax").description("
|
|
2479
|
+
program.command("click-ax").description("[deprecated \u2014 use `click @eN`] Click by raw backendDOMNodeId (from `snapshot --json` refs).").requiredOption("--node <id>", "backendDOMNodeId from `chrome-relay ax`", (v) => Number(v)).addHelpText(
|
|
1954
2480
|
"after",
|
|
1955
2481
|
`
|
|
1956
2482
|
|
|
@@ -2258,7 +2784,7 @@ Notes:
|
|
|
2258
2784
|
|
|
2259
2785
|
Examples:
|
|
2260
2786
|
chrome-relay network --tab 123 # last ${NETWORK_BUFFER_MAX_ENTRIES} requests
|
|
2261
|
-
chrome-relay network --tab 123 --filter api.
|
|
2787
|
+
chrome-relay network --tab 123 --filter api.kushalsm.com # url substring
|
|
2262
2788
|
chrome-relay network --tab 123 --status failed
|
|
2263
2789
|
chrome-relay network --tab 123 --method POST
|
|
2264
2790
|
chrome-relay network body <requestId> --tab 123 # lazy body fetch
|
|
@@ -2338,27 +2864,231 @@ Notes:
|
|
|
2338
2864
|
});
|
|
2339
2865
|
}
|
|
2340
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
|
+
|
|
2341
3065
|
// src/program.ts
|
|
2342
3066
|
function buildProgram() {
|
|
2343
3067
|
const program = new Command();
|
|
2344
|
-
program.name("chrome-relay").description("
|
|
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(
|
|
2345
3069
|
"after",
|
|
2346
3070
|
`
|
|
2347
3071
|
|
|
2348
|
-
|
|
3072
|
+
The core loop:
|
|
2349
3073
|
chrome-relay tabs
|
|
2350
|
-
chrome-relay navigate
|
|
2351
|
-
chrome-relay
|
|
2352
|
-
chrome-relay click --tab
|
|
2353
|
-
chrome-relay fill
|
|
2354
|
-
chrome-relay
|
|
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
|
|
2355
3083
|
chrome-relay keys --tab <tabId> Enter
|
|
2356
3084
|
chrome-relay js --tab <tabId> "return document.title"
|
|
2357
3085
|
chrome-relay screenshot --tab <tabId> -o evidence.png
|
|
3086
|
+
chrome-relay skills get core # the agent playbook, version-matched
|
|
2358
3087
|
|
|
2359
3088
|
Notes:
|
|
2360
|
-
|
|
2361
|
-
|
|
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).
|
|
2362
3092
|
`
|
|
2363
3093
|
);
|
|
2364
3094
|
const baseArgs = makeBaseArgs(program);
|
|
@@ -2373,6 +3103,7 @@ Notes:
|
|
|
2373
3103
|
registerInput(ctx);
|
|
2374
3104
|
registerCapture(ctx);
|
|
2375
3105
|
registerSessions(ctx);
|
|
3106
|
+
registerLoop(ctx);
|
|
2376
3107
|
return program;
|
|
2377
3108
|
}
|
|
2378
3109
|
|