chrome-relay 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +83 -46
- package/dist/index.js +1 -1
- package/dist/native-host.js +11 -4
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -445,6 +445,9 @@ function parseChromeSnapshotArgs(input) {
|
|
|
445
445
|
const diff = optBool(obj, "diff", TOOL_NAMES.SNAPSHOT);
|
|
446
446
|
if (diff !== void 0)
|
|
447
447
|
out.diff = diff;
|
|
448
|
+
const elide = optBool(obj, "elide", TOOL_NAMES.SNAPSHOT);
|
|
449
|
+
if (elide !== void 0)
|
|
450
|
+
out.elide = elide;
|
|
448
451
|
return out;
|
|
449
452
|
}
|
|
450
453
|
function parseChromeScreenshotArgs(input) {
|
|
@@ -585,16 +588,19 @@ function parseChromeNetworkArgs(input) {
|
|
|
585
588
|
if (action === "har") {
|
|
586
589
|
const withBodies = optBool(obj, "withBodies");
|
|
587
590
|
const bestEffortBodies = optBool(obj, "bestEffortBodies");
|
|
591
|
+
const rawHeadersHar = optBool(obj, "rawHeaders");
|
|
588
592
|
return {
|
|
589
593
|
...target,
|
|
590
594
|
action: "har",
|
|
591
595
|
...withBodies !== void 0 ? { withBodies } : {},
|
|
592
596
|
...bestEffortBodies !== void 0 ? { bestEffortBodies } : {},
|
|
597
|
+
...rawHeadersHar !== void 0 ? { rawHeaders: rawHeadersHar } : {},
|
|
593
598
|
...parseFilter(obj)
|
|
594
599
|
};
|
|
595
600
|
}
|
|
596
601
|
if (action === "read") {
|
|
597
|
-
|
|
602
|
+
const rawHeaders = optBool(obj, "rawHeaders");
|
|
603
|
+
return { ...target, action: "read", ...rawHeaders !== void 0 ? { rawHeaders } : {}, ...parseFilter(obj) };
|
|
598
604
|
}
|
|
599
605
|
throw new RelayError({
|
|
600
606
|
code: "invalid_arguments",
|
|
@@ -1419,7 +1425,7 @@ var init_dist = __esm({
|
|
|
1419
1425
|
import { Command } from "commander";
|
|
1420
1426
|
|
|
1421
1427
|
// src/index.ts
|
|
1422
|
-
var CHROME_RELAY_VERSION = true ? "0.7.
|
|
1428
|
+
var CHROME_RELAY_VERSION = true ? "0.7.2" : "0.0.0-dev";
|
|
1423
1429
|
|
|
1424
1430
|
// src/commands/shared.ts
|
|
1425
1431
|
init_dist();
|
|
@@ -1527,8 +1533,8 @@ function emitTargetOverride(kind, from, to) {
|
|
|
1527
1533
|
}
|
|
1528
1534
|
async function runToolImpl(name, args) {
|
|
1529
1535
|
try {
|
|
1530
|
-
|
|
1531
|
-
const result = await callTool(name,
|
|
1536
|
+
if (isToolName(name)) parseToolArgs(name, args);
|
|
1537
|
+
const result = await callTool(name, args);
|
|
1532
1538
|
if (typeof result === "string") {
|
|
1533
1539
|
process.stdout.write(result + "\n");
|
|
1534
1540
|
} else {
|
|
@@ -1836,15 +1842,20 @@ async function runDoctor() {
|
|
|
1836
1842
|
console.log(`Tip: run "chrome-relay install" to refresh manifests for every detected browser.`);
|
|
1837
1843
|
}
|
|
1838
1844
|
let serverReachable = false;
|
|
1845
|
+
let connected = {};
|
|
1839
1846
|
try {
|
|
1840
1847
|
const response = await fetch(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/ping`);
|
|
1841
1848
|
serverReachable = response.ok;
|
|
1849
|
+
if (response.ok) connected = await response.json();
|
|
1842
1850
|
} catch {
|
|
1843
1851
|
serverReachable = false;
|
|
1844
1852
|
}
|
|
1845
1853
|
console.log(`Allowed extension IDs: ${formatKnownExtensionIds()}`);
|
|
1846
1854
|
console.log(`Local bridge reachable: ${serverReachable ? "yes" : "no"}`);
|
|
1847
|
-
if (
|
|
1855
|
+
if (serverReachable) {
|
|
1856
|
+
const flavor = connected.extensionId === CHROME_WEB_STORE_EXTENSION_ID ? "Chrome Web Store" : connected.extensionId === LOCAL_UNPACKED_EXTENSION_ID ? "local unpacked (dev)" : connected.extensionId === LEGACY_DEV_EXTENSION_ID ? "legacy dev" : connected.extensionId ? "unknown id" : "id not reported (pre-0.8 host still running \u2014 restart Chrome or run chrome-relay update)";
|
|
1857
|
+
console.log(`Connected extension: ${connected.extensionVersion ?? "?"} \u2014 ${flavor}${connected.extensionId ? ` (${connected.extensionId})` : ""}`);
|
|
1858
|
+
} else {
|
|
1848
1859
|
console.log(`Tip: load the extension in one of the detected browsers so it can launch the native host.`);
|
|
1849
1860
|
}
|
|
1850
1861
|
return allHealthy;
|
|
@@ -1856,6 +1867,14 @@ async function runDoctor() {
|
|
|
1856
1867
|
|
|
1857
1868
|
// src/release-notes.ts
|
|
1858
1869
|
var RELEASE_NOTES = {
|
|
1870
|
+
"0.7.2": [
|
|
1871
|
+
"Fix (Windows): `chrome-relay update` now resolves the installed binary with `where` (Windows has no `which`), picks the spawnable `.cmd`/`.exe` over the extensionless npm shim, and re-execs it through a shell with the path quoted. Pre-0.7.2 the `which` call always failed on Windows, so update skipped the post-install manifest refresh and re-fired the cli-outdated nudge \u2014 the same loop fixed on macOS/Linux in 0.5.21, now closed on Windows too.",
|
|
1872
|
+
"Fix (Windows): `screencast stop --gif/--mp4` detected ffmpeg with `which`, which doesn't exist on Windows, so the stitch step failed with `external_dependency_missing` even when ffmpeg was on PATH. Now uses `where` on win32. No change to the macOS/Linux path.",
|
|
1873
|
+
"No extension change. Both fixes are CLI-only; existing 0.7.x extensions work unchanged."
|
|
1874
|
+
],
|
|
1875
|
+
"0.7.1": [
|
|
1876
|
+
"Fix: `wait` failed with `invalid_arguments: got 0 conditions` when invoked through the CLI. Root cause: the CLI transmitted the PARSED tool args over the bridge, and chrome_wait's parser transforms its shape ({selector} -> {condition}), so the extension re-parsed a shape it doesn't accept. The CLI now validates locally but transmits raw args \u2014 parsers run on the same raw shape at both ends, as designed. No extension change needed; 0.7.0 extensions work."
|
|
1877
|
+
],
|
|
1859
1878
|
"0.7.0": [
|
|
1860
1879
|
"`wait` \u2014 block until a condition holds: `wait <css|@ref>` (exists and visible), `wait --text <s>`, `wait --url <glob>`, `wait --load load|domcontentloaded|networkidle`, `wait --fn <js>`, or `wait <ms>` for a plain sleep. One condition per call; default 10s, capped 25s (under the transport timeout, so waits always resolve in their own round-trip). Timeout errors include the page's current state so no follow-up probe is needed.",
|
|
1861
1880
|
"`snapshot --diff` \u2014 print only what changed since this tab's previous snapshot (unified hunks + an additions/removals count; ~100 tokens instead of a full re-read). The full snapshot is still taken and the ref map still refreshes \u2014 refs in the diff are current and clickable.",
|
|
@@ -2048,6 +2067,23 @@ function listReleaseNotesSince(since) {
|
|
|
2048
2067
|
}
|
|
2049
2068
|
|
|
2050
2069
|
// src/commands/install-update.ts
|
|
2070
|
+
function whichBinary(spawnSync2, name) {
|
|
2071
|
+
const tool = process.platform === "win32" ? "where" : "which";
|
|
2072
|
+
const res = spawnSync2(tool, [name]);
|
|
2073
|
+
if (res.status !== 0) return null;
|
|
2074
|
+
const lines = (res.stdout?.toString() ?? "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
2075
|
+
if (lines.length === 0) return null;
|
|
2076
|
+
if (process.platform === "win32") {
|
|
2077
|
+
return lines.find((l) => /\.(cmd|bat|exe)$/i.test(l)) ?? lines[0];
|
|
2078
|
+
}
|
|
2079
|
+
return lines[0];
|
|
2080
|
+
}
|
|
2081
|
+
function spawnCli(spawnSync2, bin, args, opts = {}) {
|
|
2082
|
+
if (process.platform === "win32") {
|
|
2083
|
+
return spawnSync2(`"${bin}"`, args, { ...opts, shell: true });
|
|
2084
|
+
}
|
|
2085
|
+
return spawnSync2(bin, args, opts);
|
|
2086
|
+
}
|
|
2051
2087
|
function registerInstallUpdate(program) {
|
|
2052
2088
|
program.command("install").description("Install and register the local Chrome Relay host.").action(async () => {
|
|
2053
2089
|
await runInstall();
|
|
@@ -2090,22 +2126,21 @@ function registerInstallUpdate(program) {
|
|
|
2090
2126
|
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
2091
2127
|
process.exit(1);
|
|
2092
2128
|
}
|
|
2093
|
-
const
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
const versionOut = spawnSync2(newBin, ["--version"]);
|
|
2129
|
+
const newBin = whichBinary(spawnSync2, "chrome-relay");
|
|
2130
|
+
if (newBin) {
|
|
2131
|
+
const versionOut = spawnCli(spawnSync2, newBin, ["--version"]);
|
|
2097
2132
|
const newVersion = (versionOut.stdout?.toString() ?? "").trim();
|
|
2098
2133
|
out.binary.path = newBin;
|
|
2099
2134
|
if (newVersion && newVersion !== fromVersion) {
|
|
2100
2135
|
out.updatedTo = newVersion;
|
|
2101
|
-
const install2 = spawnSync2
|
|
2136
|
+
const install2 = spawnCli(spawnSync2, newBin, ["install"], { stdio: "inherit" });
|
|
2102
2137
|
if (install2.status !== 0) {
|
|
2103
2138
|
out.warnings.push({
|
|
2104
2139
|
code: "install_refresh_failed",
|
|
2105
2140
|
message: `Update installed the new package but \`${newBin} install\` exited ${install2.status}. Run it manually to refresh the native host manifest.`
|
|
2106
2141
|
});
|
|
2107
2142
|
}
|
|
2108
|
-
const rn = spawnSync2
|
|
2143
|
+
const rn = spawnCli(spawnSync2, newBin, ["release-notes", "--since", fromVersion]);
|
|
2109
2144
|
try {
|
|
2110
2145
|
const parsed = JSON.parse(rn.stdout?.toString() ?? "");
|
|
2111
2146
|
if (Array.isArray(parsed.changes)) {
|
|
@@ -2127,7 +2162,7 @@ function registerInstallUpdate(program) {
|
|
|
2127
2162
|
} else {
|
|
2128
2163
|
out.warnings.push({
|
|
2129
2164
|
code: "update_not_verified",
|
|
2130
|
-
message: `Install completed but
|
|
2165
|
+
message: `Install completed but \`${process.platform === "win32" ? "where" : "which"} chrome-relay\` did not return a path. Could not verify the active binary changed.`
|
|
2131
2166
|
});
|
|
2132
2167
|
}
|
|
2133
2168
|
}
|
|
@@ -2158,7 +2193,7 @@ function registerNavigation(ctx) {
|
|
|
2158
2193
|
await run("get_windows_and_tabs", {});
|
|
2159
2194
|
});
|
|
2160
2195
|
tabOpt(
|
|
2161
|
-
program.command("navigate <url>").description("Navigate a tab to a URL. Use --tab <id> to target an existing tab.").option("--new", "open in a new tab").option("--active", "activate the tab after navigating (default: background
|
|
2196
|
+
program.command("navigate <url>").description("Navigate a tab to a URL. Use --tab <id> to target an existing tab.").option("--new", "open in a new tab").option("--active", "activate the tab after navigating (default: background, no focus theft)").addHelpText(
|
|
2162
2197
|
"after",
|
|
2163
2198
|
`
|
|
2164
2199
|
|
|
@@ -2168,7 +2203,7 @@ Examples:
|
|
|
2168
2203
|
chrome-relay navigate "https://chrome-relay.kushalsm.com" --new # open in a new background tab
|
|
2169
2204
|
chrome-relay navigate "https://chrome-relay.kushalsm.com" --new --active # open new tab AND show it to the user
|
|
2170
2205
|
|
|
2171
|
-
By default chrome-relay never steals focus
|
|
2206
|
+
By default chrome-relay never steals focus. Navigated tabs (new or
|
|
2172
2207
|
existing) stay in whatever state they're in. Pass --active when you
|
|
2173
2208
|
actually want the user looking at the page.
|
|
2174
2209
|
`
|
|
@@ -2217,7 +2252,7 @@ Examples:
|
|
|
2217
2252
|
chrome-relay click 'button[aria-label="Save"]'
|
|
2218
2253
|
chrome-relay click --tab 123 --x 1327 --y 771
|
|
2219
2254
|
|
|
2220
|
-
Prefer @refs from \`chrome-relay snapshot
|
|
2255
|
+
Prefer @refs from \`chrome-relay snapshot\`. They carry their own tab and
|
|
2221
2256
|
survive DOM churn (backendNodeId + role/name heal). CSS selectors for
|
|
2222
2257
|
elements you know statically. Coordinates for canvas/SVG chart internals
|
|
2223
2258
|
where no DOM handle exists. See docs/clicking-strategies.md.
|
|
@@ -2270,10 +2305,10 @@ Examples:
|
|
|
2270
2305
|
chrome-relay type "appended into already-focused element"
|
|
2271
2306
|
|
|
2272
2307
|
When to pick which:
|
|
2273
|
-
fill
|
|
2274
|
-
type
|
|
2275
|
-
keys
|
|
2276
|
-
js
|
|
2308
|
+
fill: plain <input>, <textarea>, <select>, React-controlled inputs (atomic write).
|
|
2309
|
+
type: contenteditable, Draft.js, Lexical, ProseMirror (trusted text commit).
|
|
2310
|
+
keys: Enter, Tab, Esc, arrows, modifier chords (single key press).
|
|
2311
|
+
js: anything else.
|
|
2277
2312
|
`
|
|
2278
2313
|
)
|
|
2279
2314
|
).action(async (text, opts) => {
|
|
@@ -2330,7 +2365,7 @@ import { writeFileSync } from "fs";
|
|
|
2330
2365
|
import { structuredPatch } from "diff";
|
|
2331
2366
|
function printSnapshotDiff(current, prevText) {
|
|
2332
2367
|
if (prevText === null) {
|
|
2333
|
-
process.stderr.write("[chrome-relay] no previous snapshot for this tab
|
|
2368
|
+
process.stderr.write("[chrome-relay] no previous snapshot for this tab. Showing full output.\n");
|
|
2334
2369
|
process.stdout.write(current + "\n");
|
|
2335
2370
|
return;
|
|
2336
2371
|
}
|
|
@@ -2372,17 +2407,17 @@ function exitWithError(error) {
|
|
|
2372
2407
|
function registerCapture(ctx) {
|
|
2373
2408
|
const { program, withBase, run } = ctx;
|
|
2374
2409
|
tabOpt(
|
|
2375
|
-
program.command("snapshot").description("Page snapshot with actionable @refs
|
|
2410
|
+
program.command("snapshot").description("Page snapshot with actionable @refs. Accessibility tree plus cursor-interactive sweep, compact text.").option("-i, --interactive", "only ref-bearing elements (buttons, links, inputs, named content, clickables)").option("-d, --depth <n>", "truncate the tree at this depth", (v) => Number(v)).option("-s, --scope <css>", "restrict to the subtree of the first CSS match").option("-u, --urls", "include link hrefs as url= attrs").option("--diff", "print only what changed since the previous snapshot of this tab (~100 tokens instead of a re-read)").option("--no-elide", "print every row of long identical-shape runs (default keeps 10 + a count marker)").option("--json", "structured output: { title, url, tabId, nodes, refs }").addHelpText(
|
|
2376
2411
|
"after",
|
|
2377
2412
|
`
|
|
2378
2413
|
|
|
2379
2414
|
Examples:
|
|
2380
2415
|
chrome-relay snapshot -i # see the page, get @refs
|
|
2381
|
-
chrome-relay click @e12 # act on a ref
|
|
2416
|
+
chrome-relay click @e12 # act on a ref, no --tab needed
|
|
2382
2417
|
chrome-relay snapshot -i -s "#main" # scope to a subtree
|
|
2383
2418
|
chrome-relay snapshot --json # machine-readable envelope
|
|
2384
2419
|
|
|
2385
|
-
The core loop: snapshot -i
|
|
2420
|
+
The core loop: snapshot -i -> click/fill @eN -> snapshot -i again after the
|
|
2386
2421
|
page changes. Refs carry their own tab and heal across DOM churn
|
|
2387
2422
|
(backendNodeId fast path + role/name re-find); a dead ref returns
|
|
2388
2423
|
error.code = stale_ref, which means: re-run snapshot.
|
|
@@ -2395,6 +2430,7 @@ error.code = stale_ref, which means: re-run snapshot.
|
|
|
2395
2430
|
if (opts.scope) extras.scope = opts.scope;
|
|
2396
2431
|
if (opts.urls) extras.urls = true;
|
|
2397
2432
|
if (opts.diff) extras.diff = true;
|
|
2433
|
+
if (opts.elide === false) extras.elide = false;
|
|
2398
2434
|
try {
|
|
2399
2435
|
const result = await callTool("chrome_snapshot", withBase(opts, extras));
|
|
2400
2436
|
if (opts.diff && !opts.json) {
|
|
@@ -2408,7 +2444,7 @@ error.code = stale_ref, which means: re-run snapshot.
|
|
|
2408
2444
|
}
|
|
2409
2445
|
});
|
|
2410
2446
|
tabOpt(
|
|
2411
|
-
program.command("screenshot").description("Capture a screenshot of any tab without activating it.").option("--full", "capture beyond the viewport (full page)").option("--bbox <rect>", "capture a region: 'x,y,width,height' (pixels)").option("--selector <css>", "capture the bounding box of a CSS selector").option("--padding <px>", "pixels of padding around --selector region", (v) => Number(v)).option("--max-edge <px>", "downscale so longer edge
|
|
2447
|
+
program.command("screenshot").description("Capture a screenshot of any tab without activating it.").option("--full", "capture beyond the viewport (full page)").option("--bbox <rect>", "capture a region: 'x,y,width,height' (pixels)").option("--selector <css>", "capture the bounding box of a CSS selector").option("--padding <px>", "pixels of padding around --selector region", (v) => Number(v)).option("--max-edge <px>", "downscale so longer edge is at most this many pixels (no default; opt-in)", (v) => Number(v)).option("-o, --out <path>", "save image to path (base64 PNG decoded)").addHelpText(
|
|
2412
2448
|
"after",
|
|
2413
2449
|
`
|
|
2414
2450
|
|
|
@@ -2450,7 +2486,7 @@ full-tab screenshot when an agent only needs to see one component.
|
|
|
2450
2486
|
}
|
|
2451
2487
|
});
|
|
2452
2488
|
tabOpt(
|
|
2453
|
-
program.command("read").description("[deprecated
|
|
2489
|
+
program.command("read").description("[deprecated: use `snapshot`] Alias for the unified snapshot.").option("-i, --interactive", "only ref-bearing elements").option("--json", "structured output")
|
|
2454
2490
|
).action(async (opts) => {
|
|
2455
2491
|
process.stderr.write("[chrome-relay] deprecated: `read` is now an alias for `snapshot` (new output format). Use `chrome-relay snapshot`.\n");
|
|
2456
2492
|
const extras = {};
|
|
@@ -2463,7 +2499,7 @@ full-tab screenshot when an agent only needs to see one component.
|
|
|
2463
2499
|
}
|
|
2464
2500
|
});
|
|
2465
2501
|
tabOpt(
|
|
2466
|
-
program.command("ax").description("[deprecated
|
|
2502
|
+
program.command("ax").description("[deprecated: use `snapshot`] Alias for the unified snapshot.").option("-i, --interactive-only", "only ref-bearing elements").option("--root <role>", "(ignored: use `snapshot --scope <css>`)").option("--include-subframes", "(ignored: snapshot is top-frame only)").option("--json", "structured output")
|
|
2467
2503
|
).action(async (opts) => {
|
|
2468
2504
|
process.stderr.write("[chrome-relay] deprecated: `ax` is now an alias for `snapshot` (new output format, one ref space). Use `chrome-relay snapshot`.\n");
|
|
2469
2505
|
const extras = {};
|
|
@@ -2476,7 +2512,7 @@ full-tab screenshot when an agent only needs to see one component.
|
|
|
2476
2512
|
}
|
|
2477
2513
|
});
|
|
2478
2514
|
tabOpt(
|
|
2479
|
-
program.command("click-ax").description("[deprecated
|
|
2515
|
+
program.command("click-ax").description("[deprecated: use `click @eN`] Click by raw backendDOMNodeId (from `snapshot --json` refs).").requiredOption("--node <id>", "backendDOMNodeId from `chrome-relay ax`", (v) => Number(v)).addHelpText(
|
|
2480
2516
|
"after",
|
|
2481
2517
|
`
|
|
2482
2518
|
|
|
@@ -2582,10 +2618,10 @@ Notes:
|
|
|
2582
2618
|
if (opts.gif || opts.mp4) {
|
|
2583
2619
|
const fps = typeof opts.fps === "number" ? opts.fps : 15;
|
|
2584
2620
|
const { spawnSync: spawnSync2 } = await import("child_process");
|
|
2585
|
-
const which = spawnSync2("which", ["ffmpeg"]);
|
|
2621
|
+
const which = spawnSync2(process.platform === "win32" ? "where" : "which", ["ffmpeg"]);
|
|
2586
2622
|
if (which.status !== 0) {
|
|
2587
2623
|
if (opts.allowMissingFfmpeg) {
|
|
2588
|
-
process.stderr.write("[chrome-relay] ffmpeg not on PATH
|
|
2624
|
+
process.stderr.write("[chrome-relay] ffmpeg not on PATH. Skipping --gif/--mp4 (allow-missing-ffmpeg).\n");
|
|
2589
2625
|
return;
|
|
2590
2626
|
}
|
|
2591
2627
|
const { RelayError: RelayError2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
@@ -2651,7 +2687,7 @@ Notes:
|
|
|
2651
2687
|
init_dist();
|
|
2652
2688
|
var CONSOLE_BUFFER_MAX_KB = Math.round(CONSOLE_BUFFER_MAX_BYTES / 1024);
|
|
2653
2689
|
function netFilterOpts(cmd) {
|
|
2654
|
-
return cmd.option("--filter <substr>", "url substring filter").option("--status <bucket>", "ok | redirect | client_error | server_error | failed").option("--method <verb>", "exact method, e.g. POST").option("--limit <n>", "cap response length", (v) => Number(v));
|
|
2690
|
+
return cmd.option("--filter <substr>", "url substring filter").option("--status <bucket>", "ok | redirect | client_error | server_error | failed").option("--method <verb>", "exact method, e.g. POST").option("--limit <n>", "cap response length", (v) => Number(v)).option("--raw-headers", "include sensitive header values (cookies, auth, tokens). REDACTED by default so output is safe to paste");
|
|
2655
2691
|
}
|
|
2656
2692
|
function netFilterArgs(opts) {
|
|
2657
2693
|
const a = {};
|
|
@@ -2659,6 +2695,7 @@ function netFilterArgs(opts) {
|
|
|
2659
2695
|
if (opts.status) a.status = opts.status;
|
|
2660
2696
|
if (opts.method) a.method = opts.method;
|
|
2661
2697
|
if (typeof opts.limit === "number") a.limit = opts.limit;
|
|
2698
|
+
if (opts.rawHeaders) a.rawHeaders = true;
|
|
2662
2699
|
return a;
|
|
2663
2700
|
}
|
|
2664
2701
|
function registerSessions(ctx) {
|
|
@@ -2799,8 +2836,8 @@ Privacy:
|
|
|
2799
2836
|
share with the agent invoking chrome-relay.
|
|
2800
2837
|
|
|
2801
2838
|
Notes:
|
|
2802
|
-
Bodies are NOT eagerly buffered
|
|
2803
|
-
the request finishes. Use
|
|
2839
|
+
Bodies are NOT eagerly buffered. Chrome GCs response bodies ~30s after
|
|
2840
|
+
the request finishes. Use \`network body <requestId>\` or \`network har --with-bodies\` promptly.
|
|
2804
2841
|
WebSocket frames and SSE streams are out of scope.
|
|
2805
2842
|
`
|
|
2806
2843
|
).action(async (opts) => {
|
|
@@ -2812,7 +2849,7 @@ Notes:
|
|
|
2812
2849
|
await run("chrome_network", withBase(opts, netFilterArgs(opts)));
|
|
2813
2850
|
});
|
|
2814
2851
|
tabOpt(
|
|
2815
|
-
network.command("body <requestId>").description("Fetch the response body for one request (lazy; may fail if GC'd).").option("--head <bytes>", "truncate to first N bytes", (v) => Number(v)).option("--full", "return the full body
|
|
2852
|
+
network.command("body <requestId>").description("Fetch the response body for one request (lazy; may fail if GC'd).").option("--head <bytes>", "truncate to first N bytes", (v) => Number(v)).option("--full", "return the full body. Default truncates to 8 KB")
|
|
2816
2853
|
).action(async (requestId, opts) => {
|
|
2817
2854
|
const extras = { action: "body", requestId };
|
|
2818
2855
|
if (opts.full) extras.full = true;
|
|
@@ -2820,7 +2857,7 @@ Notes:
|
|
|
2820
2857
|
await run("chrome_network", withBase(opts, extras));
|
|
2821
2858
|
});
|
|
2822
2859
|
tabOpt(netFilterOpts(
|
|
2823
|
-
network.command("har").description("Emit HAR-compatible JSON for the captured entries.").option("--with-bodies", "fetch response bodies before emitting; strict by default
|
|
2860
|
+
network.command("har").description("Emit HAR-compatible JSON for the captured entries.").option("--with-bodies", "fetch response bodies before emitting; strict by default, fails if any body cannot be fetched").option("--best-effort-bodies", "with --with-bodies: keep the HAR even when some bodies are missing/errored (legacy behavior); per-entry _chrome_relay.bodyState/bodyError records what failed")
|
|
2824
2861
|
)).action(async (opts) => {
|
|
2825
2862
|
const extras = { ...netFilterArgs(opts), action: "har" };
|
|
2826
2863
|
if (opts.withBodies) extras.withBodies = true;
|
|
@@ -2868,11 +2905,11 @@ Notes:
|
|
|
2868
2905
|
init_dist();
|
|
2869
2906
|
import { readFileSync } from "fs";
|
|
2870
2907
|
function coreSkillText() {
|
|
2871
|
-
if (true) return '# Chrome Relay\n\nDrives the user\'s real Chrome through a Chrome extension + local native host. Prefer it when logged-in browser state (auth cookies, sessions, installed extensions) matters.\n\n## Setup\n\n1. [Chrome extension](https://chromewebstore.google.com/detail/chrome-relay/cpdiapbifblhlcpnmlmfpgfjlacebokb)\n2. CLI:\n ```sh\n pnpm add -g chrome-relay\n chrome-relay install\n chrome-relay doctor\n ```\n\nVerify CLI
|
|
2908
|
+
if (true) return '# Chrome Relay\n\nDrives the user\'s real Chrome through a Chrome extension + local native host. Prefer it when logged-in browser state (auth cookies, sessions, installed extensions) matters.\n\n## Setup\n\n1. [Chrome extension](https://chromewebstore.google.com/detail/chrome-relay/cpdiapbifblhlcpnmlmfpgfjlacebokb)\n2. CLI:\n ```sh\n pnpm add -g chrome-relay\n chrome-relay install\n chrome-relay doctor\n ```\n\nVerify CLI >= 0.7.0. wait/get/batch/`snapshot --diff` landed there. 0.6.0 brought the snapshot/@ref loop. >= 0.5.20 fixed a silent click bug on Radix/React-Aria UIs:\n```sh\nchrome-relay --version\n```\n\n## The core loop\n\n```sh\nchrome-relay tabs # find or create a tab\nchrome-relay navigate "https://kushalsm.com" --new # background tab by default\nchrome-relay snapshot --tab 1234 -i # see the page: actionable elements get @refs\nchrome-relay click @e12 # act on refs, no --tab, no selector\nchrome-relay fill @e14 "hello"\nchrome-relay wait --text "Saved" --tab 1234 # block until the page reacts\nchrome-relay snapshot --tab 1234 --diff # print only what changed (~100 tokens)\n```\n\nSnapshot output is compact indented text, usually 1 to 15 KB for most pages. Read it directly, no jq needed:\n\n```\n- link "Hacker News" [ref=e4]\n- textbox "Search" [ref=e41]: current value\n- checkbox "Remember me" [checked, ref=e42]\n- clickable "Open card" [ref=e88] # cursor-pointer div the AX tree missed\n```\n\n**Refs carry their own tab.** `click @e12` acts on the tab that produced e12, never the active tab. Safe while the user keeps browsing. A contradicting `--tab` errors with `target_conflict`.\n\n**Ref lifetime.** Refs survive same-page DOM churn (cached backendNodeId, healed by role+name re-find when nodes are replaced) but die on real navigation. A dead ref returns `error.code = stale_ref`. Re-run `snapshot`.\n\n**Interception.** Ref clicks hit-test the point first. If an overlay / sticky header / modal owns it, you get `error.code = click_intercepted` naming the interceptor. Dismiss it or scroll, then retry. The click was NOT delivered. `fill`/`type` skip this check (covered inputs are still writable).\n\n## Tool surface\n\n| Command | What it does |\n|---|---|\n| `tabs` | List windows + tabs with their `tabId`s |\n| `navigate <url>` | Open in current tab. `--new` opens in a **background** tab (default). `--active` brings it to foreground. `--tab <id>` retargets an existing tab. |\n| `snapshot --tab <id> -i` | Page snapshot with actionable `@refs`: accessibility tree plus cursor-interactive sweep, one ref space, compact text. `-d N` depth cap, `-s <css>` scope to subtree, `-u` include hrefs, `--diff` print only changes since the last snapshot, `--json` structured envelope with the refs map. |\n| `wait <css\\|@ref>` / `wait --text` / `--url <glob>` / `--load networkidle` / `--fn <js>` | Block until a condition holds (one per call, default 10s, max 25s). `wait 1500` just sleeps. On timeout the error includes current page state. |\n| `get text\\|value\\|attr\\|count\\|title\\|url <target>` | One value, plain to stdout. No full snapshot. `get text @e12`, `get attr @e7 href`, `get count ".row"`. |\n| `batch \'[{"name":"chrome_...","args":{...}}, ...]\'` | N tool calls in ONE round-trip, sequential, bail-on-error by default. Use wire tool names. |\n| `skills get core` | Print this playbook, version-matched to the installed binary. |\n| `click <@ref \\| selector> --tab <id>` | Trusted hover + press + release at element center (`pointerType: "mouse"`). Refs need no `--tab`. |\n| `click --x N --y N --tab <id>` | Coordinate-mode click for canvas/SVG chart internals with no DOM handle. |\n| `hover <@ref \\| selector \\| --x --y>` | Pointer move only. Fires `:hover` styles. |\n| `fill <@ref \\| selector> <value>` | Atomic value write into `<input>`/`<textarea>`/`<select>`. Bypasses React\'s value tracker. Refs reach inside shadow DOM (selectors can\'t). |\n| `type <text> [-s <@ref \\| selector>]` | CDP `Input.insertText`. Use for contenteditable / Draft.js / Lexical / ProseMirror. **Appends** at caret; clear the input first if it had a value. |\n| `keys <chord> --tab <id>` | Single key or chord: `Enter`, `Tab`, `Escape`, `Cmd+K`, `Shift+ArrowDown`. |\n| `js <code> --tab <id>` | `Runtime.evaluate` in MAIN world. Use `return` for the value. Top-level `await` works. |\n| `screenshot --tab <id> -o <path>` | PNG. `--full` captures beyond viewport. `--max-edge N` resizes. |\n| `screencast --tab <id> -o <path>` | Record a tab via CDP (paint-driven). Requires an active tab. |\n| `network --tab <id>` | HTTP request/response ring buffer, last 200 per tab. `network body <requestId>` fetches a body while Chrome still has it. `network har --with-bodies` exports a HAR with bodies. |\n| `console --tab <id>` | `console.log/warn/error` + page exceptions, last 200. |\n| `viewport` | Emulate device viewport, DPR, mobile flag, touch, UA. |\n| `workspace` / `group` | Manage named windows / tab-groups so multiple agents can drive separate windows. |\n| `switch <tabId>` / `close <tabIds...>` | Activate or close tabs |\n| `self-reload` | Restart the extension\'s service worker after a rebuild |\n| `release-notes --since <ver>` / `update` | Queryable changelog; agent-readable JSON. |\n| `call <tool> [json]` | Raw pass-through for any internal tool. |\n| `read` / `ax` / `click-ax` | **Deprecated**. Aliases for `snapshot` / `click @ref`. Will be removed; don\'t use in new work. |\n\n## Picking the right text tool\n\n| Target element | Tool |\n|---|---|\n| `<input>`, `<textarea>`, `<select>` (including React-controlled, shadow DOM) | `fill @ref` |\n| `[contenteditable]`, `role="textbox"`, Draft.js / Lexical / ProseMirror, X compose, LinkedIn DM, new Reddit composer | `type` |\n| Submit, navigate menus, modifier shortcuts | `keys` |\n| Combobox / autocomplete option selection | `type` into filter, then `keys ArrowDown`, then `keys Enter` ([why](references/patterns.md)) |\n| Framework-internal pokes, scraping, custom widgets | `js` |\n\n## Element addressing: the fallback ladder\n\n1. **`@ref` from `snapshot -i`**: default. Covers buttons/links/inputs, named content, cursor-pointer div-soup (the sweep), and shadow DOM.\n2. **CSS selector**: when you know the selector statically and don\'t need a snapshot.\n3. **`js` probe, then coordinate click**: canvas internals and SVG chart segments (anonymous `<path>` elements have no DOM handle anywhere):\n ```sh\n chrome-relay js --tab 1234 "const r = document.querySelector(\'svg path\').getBoundingClientRect(); return {x: r.x + r.width/2, y: r.y + r.height/2}"\n chrome-relay click --tab 1234 --x 312 --y 218\n ```\n\n## Don\'t poll. Wait.\n\nA snapshot after every action wastes turns. The cheap loop on a changing page:\n\n```sh\nchrome-relay click @e12\nchrome-relay wait --text "Saved" --tab 1234 # or wait <selector> / --url / --load\nchrome-relay snapshot --tab 1234 --diff # only the changes, refs included\n```\n\n## Top gotchas\n\n0. **`snapshot -i` is for ACTING, not fact extraction.** It prints ref-bearing elements only. Non-interactive values (dashboard metrics, paragraph text, chart labels) drop out. Measured live: a Cloudflare Pages metrics page lost all its numbers under `-i`. To READ facts, use full `snapshot`, `get text <target>`, or a `js` projection.\n1. **`type` appends.** It inserts at the caret. If the input had a value (autosaved draft, default text), clear it first via `js` or `keys` (Cmd+A then Backspace).\n2. **Refs die on navigation.** `stale_ref` means the page changed under you; re-snapshot. Don\'t retry the same ref.\n3. **Coords go stale fast.** Read `getBoundingClientRect`, scroll/reflow, then click, and you hit the wrong element. For autocomplete popups especially, use keyboard nav, not coord clicks.\n4. **Click "succeeded" but nothing happened.** First diagnostic: `document.elementFromPoint(x, y)`. If it returns a wrapper or form background, your coords are wrong. If it returns the right element but state didn\'t change, you\'re likely on chrome-relay <0.5.20. Upgrade.\n\nMore recipes: [references/patterns.md](references/patterns.md)\nFailure modes: [references/troubleshooting.md](references/troubleshooting.md)\n\n## Operational guidance\n\n- **Don\'t give up early.** A failing click is information, not a stop signal. Attach a document-level listener with `capture:true` and watch what fires:\n ```sh\n chrome-relay js --tab 1234 "\n [\'pointerdown\',\'mousedown\',\'click\'].forEach(t =>\n document.addEventListener(t, e => console.log(t, e.target.tagName, e.target.className), {capture:true})\n );\n return \'listening\'\n "\n # do the action, then:\n chrome-relay console --tab 1234\n ```\n- **Don\'t echo secrets.** When extracting tokens / API keys via `js`, write the result directly to a file. Never `echo $TOKEN` or interpolate into shell strings. It ends up in scrollback, logs, and tool transcripts.\n- **Redact `network` output.** Request/response headers carry cookies, auth/CSRF tokens, account and project IDs. Never paste raw `chrome-relay network` output into chat, docs, issues, or commits. Filter to the fields you need (url, status, timings) or redact headers first.\n- **Capture before irreversible actions** (form submit, send message, account change). Save the screenshot path.\n\n## Guardrails\n\n- Errors are structured: branch on `relayError.code` (`stale_ref`, `click_intercepted`, `element_not_found`, `target_conflict`, `timeout`), not on message text.\n- If a flag is unclear, `chrome-relay <command> --help` is authoritative. These docs lag.';
|
|
2872
2909
|
try {
|
|
2873
2910
|
return readFileSync(new URL("../../../../skills/chrome-relay/SKILL.md", import.meta.url), "utf8").replace(/^---\n[\s\S]*?\n---\n/, "").replace(/<!--[\s\S]*?-->\n*/, "").trim();
|
|
2874
2911
|
} catch {
|
|
2875
|
-
return "core skill unavailable in this build
|
|
2912
|
+
return "core skill unavailable in this build. See https://chrome-relay.kushalsm.com/skill.md";
|
|
2876
2913
|
}
|
|
2877
2914
|
}
|
|
2878
2915
|
function exitWithError2(error) {
|
|
@@ -2925,7 +2962,7 @@ need a follow-up probe.
|
|
|
2925
2962
|
if (typeof opts.timeout === "number") extras.timeoutMs = opts.timeout;
|
|
2926
2963
|
await run(TOOL_NAMES.WAIT, withBase(opts, extras));
|
|
2927
2964
|
});
|
|
2928
|
-
const get = program.command("get").description("One value, plain to stdout
|
|
2965
|
+
const get = program.command("get").description("One value, plain to stdout. No full snapshot.").addHelpText(
|
|
2929
2966
|
"after",
|
|
2930
2967
|
`
|
|
2931
2968
|
|
|
@@ -2988,7 +3025,7 @@ Examples:
|
|
|
2988
3025
|
|
|
2989
3026
|
One HTTP POST, one native-messaging message, sequential execution in the
|
|
2990
3027
|
extension. Tool names are the wire names (chrome_navigate, chrome_snapshot,
|
|
2991
|
-
chrome_click_element,
|
|
3028
|
+
chrome_click_element, ...; see \`chrome-relay call --help\`). Amortizes CLI
|
|
2992
3029
|
startup across N actions. Nested batches are rejected.
|
|
2993
3030
|
`
|
|
2994
3031
|
).action(async (json, opts) => {
|
|
@@ -3035,7 +3072,7 @@ startup across N actions. Nested batches are rejected.
|
|
|
3035
3072
|
exitWithError2(error);
|
|
3036
3073
|
}
|
|
3037
3074
|
});
|
|
3038
|
-
const skills = program.command("skills [verb] [name]").description("Agent playbooks shipped inside the CLI
|
|
3075
|
+
const skills = program.command("skills [verb] [name]").description("Agent playbooks shipped inside the CLI. Always version-matched to the binary.").addHelpText(
|
|
3039
3076
|
"after",
|
|
3040
3077
|
`
|
|
3041
3078
|
|
|
@@ -3043,13 +3080,13 @@ Examples:
|
|
|
3043
3080
|
chrome-relay skills # list available skills
|
|
3044
3081
|
chrome-relay skills get core # print the core usage guide
|
|
3045
3082
|
|
|
3046
|
-
The same guide is hosted at https://chrome-relay.kushalsm.com/skill.md
|
|
3047
|
-
|
|
3083
|
+
The same guide is hosted at https://chrome-relay.kushalsm.com/skill.md.
|
|
3084
|
+
The binary copy is authoritative for the version you have installed.
|
|
3048
3085
|
`
|
|
3049
3086
|
);
|
|
3050
3087
|
skills.action(async (verb, name) => {
|
|
3051
3088
|
if (!verb || verb === "list") {
|
|
3052
|
-
process.stdout.write("core
|
|
3089
|
+
process.stdout.write("core: the Chrome Relay agent playbook (snapshot/@ref loop, text-tool table, gotchas)\n");
|
|
3053
3090
|
return;
|
|
3054
3091
|
}
|
|
3055
3092
|
if (verb === "get" && (name === "core" || name === void 0)) {
|
|
@@ -3065,7 +3102,7 @@ the binary copy is authoritative for the version you have installed.
|
|
|
3065
3102
|
// src/program.ts
|
|
3066
3103
|
function buildProgram() {
|
|
3067
3104
|
const program = new Command();
|
|
3068
|
-
program.name("chrome-relay").description("Your agent drives the Chrome you're signed into
|
|
3105
|
+
program.name("chrome-relay").description("Your agent drives the Chrome you're signed into. Reads pages, clicks buttons, fills forms from any shell.").version(CHROME_RELAY_VERSION).showHelpAfterError().option("--workspace <name>", "target the active tab in a named workspace window (works at top level too)").option("--group <name>", "target the active tab in a named tab-group (works at top level too)").enablePositionalOptions().addHelpText(
|
|
3069
3106
|
"after",
|
|
3070
3107
|
`
|
|
3071
3108
|
|
|
@@ -3073,7 +3110,7 @@ The core loop:
|
|
|
3073
3110
|
chrome-relay tabs
|
|
3074
3111
|
chrome-relay navigate "https://chrome-relay.kushalsm.com" --new # background tab
|
|
3075
3112
|
chrome-relay snapshot --tab <tabId> -i # actionable elements get @refs
|
|
3076
|
-
chrome-relay click @e12 # act on a ref
|
|
3113
|
+
chrome-relay click @e12 # act on a ref, no --tab needed
|
|
3077
3114
|
chrome-relay fill @e14 "value"
|
|
3078
3115
|
chrome-relay snapshot --tab <tabId> -i # re-look after the page changes
|
|
3079
3116
|
|
|
@@ -3087,8 +3124,8 @@ Also:
|
|
|
3087
3124
|
|
|
3088
3125
|
Notes:
|
|
3089
3126
|
Refs come from snapshot and carry their own tab. Tools attach via CDP and
|
|
3090
|
-
run on backgrounded tabs without stealing focus. Errors are structured
|
|
3091
|
-
|
|
3127
|
+
run on backgrounded tabs without stealing focus. Errors are structured.
|
|
3128
|
+
Branch on relayError.code (stale_ref means: re-run snapshot).
|
|
3092
3129
|
`
|
|
3093
3130
|
);
|
|
3094
3131
|
const baseArgs = makeBaseArgs(program);
|
package/dist/index.js
CHANGED
package/dist/native-host.js
CHANGED
|
@@ -56,7 +56,7 @@ function toBridgeError(unknownErr, fallbackTool) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// src/index.ts
|
|
59
|
-
var CHROME_RELAY_VERSION = true ? "0.7.
|
|
59
|
+
var CHROME_RELAY_VERSION = true ? "0.7.2" : "0.0.0-dev";
|
|
60
60
|
|
|
61
61
|
// src/release-notes.ts
|
|
62
62
|
function compareSemver(a, b) {
|
|
@@ -104,7 +104,8 @@ var RelayHttpServer = class {
|
|
|
104
104
|
ok: true,
|
|
105
105
|
port: this.port,
|
|
106
106
|
cliVersion: CHROME_RELAY_VERSION,
|
|
107
|
-
extensionVersion: this.bridge.getExtensionVersion() ?? null
|
|
107
|
+
extensionVersion: this.bridge.getExtensionVersion() ?? null,
|
|
108
|
+
extensionId: this.bridge.getExtensionId() ?? null
|
|
108
109
|
}));
|
|
109
110
|
this.app.post("/call", async (request, reply) => {
|
|
110
111
|
if (request.headers.origin) {
|
|
@@ -154,12 +155,17 @@ var ExtensionBridge = class {
|
|
|
154
155
|
pending = /* @__PURE__ */ new Map();
|
|
155
156
|
readyWaiters = /* @__PURE__ */ new Set();
|
|
156
157
|
ready = false;
|
|
157
|
-
// Extension version captured from `bridge.ready`.
|
|
158
|
-
//
|
|
158
|
+
// Extension version + id captured from `bridge.ready`. Version feeds the
|
|
159
|
+
// cli-outdated notice; the id tells doctor WHICH extension owns the bridge
|
|
160
|
+
// (store vs unpacked dev — they race for the port when both are loaded).
|
|
159
161
|
extensionVersion;
|
|
162
|
+
extensionId;
|
|
160
163
|
getExtensionVersion() {
|
|
161
164
|
return this.extensionVersion;
|
|
162
165
|
}
|
|
166
|
+
getExtensionId() {
|
|
167
|
+
return this.extensionId;
|
|
168
|
+
}
|
|
163
169
|
handleMessage(message) {
|
|
164
170
|
if (message.type === "bridge.ready") {
|
|
165
171
|
this.handleReady(message);
|
|
@@ -182,6 +188,7 @@ var ExtensionBridge = class {
|
|
|
182
188
|
handleReady(message) {
|
|
183
189
|
this.ready = true;
|
|
184
190
|
this.extensionVersion = message.payload?.version;
|
|
191
|
+
this.extensionId = message.payload?.extensionId;
|
|
185
192
|
for (const notify of this.readyWaiters) {
|
|
186
193
|
notify();
|
|
187
194
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-relay",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
17
17
|
"test": "vitest run"
|
|
18
18
|
},
|
|
19
|
-
"description": "Your agent drives the Chrome you're signed into
|
|
19
|
+
"description": "Your agent drives the Chrome you're signed into. Read pages, click buttons, fill forms from any shell. No robot browser, no cookie export, no focus stealing.",
|
|
20
20
|
"keywords": [
|
|
21
21
|
"chrome",
|
|
22
22
|
"browser",
|