chrome-relay 0.5.8 → 0.5.10
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 +609 -458
- package/dist/index.js +1 -1
- package/dist/native-host.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,57 +1,264 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../protocol/dist/index.js
|
|
13
|
+
var dist_exports = {};
|
|
14
|
+
__export(dist_exports, {
|
|
15
|
+
CHROME_WEB_STORE_EXTENSION_ID: () => CHROME_WEB_STORE_EXTENSION_ID,
|
|
16
|
+
DEFAULT_EXTENSION_ID: () => DEFAULT_EXTENSION_ID,
|
|
17
|
+
DEFAULT_EXTENSION_IDS: () => DEFAULT_EXTENSION_IDS,
|
|
18
|
+
DEFAULT_HTTP_PORT: () => DEFAULT_HTTP_PORT,
|
|
19
|
+
LEGACY_DEV_EXTENSION_ID: () => LEGACY_DEV_EXTENSION_ID,
|
|
20
|
+
LOCAL_UNPACKED_EXTENSION_ID: () => LOCAL_UNPACKED_EXTENSION_ID,
|
|
21
|
+
NATIVE_HOST_NAME: () => NATIVE_HOST_NAME,
|
|
22
|
+
RelayError: () => RelayError,
|
|
23
|
+
TOOL_NAMES: () => TOOL_NAMES,
|
|
24
|
+
toBridgeError: () => toBridgeError
|
|
25
|
+
});
|
|
26
|
+
function toBridgeError(unknownErr, fallbackTool) {
|
|
27
|
+
if (unknownErr instanceof RelayError) {
|
|
28
|
+
const e = unknownErr.toBridgeError();
|
|
29
|
+
return fallbackTool && !e.tool ? { ...e, tool: fallbackTool } : e;
|
|
30
|
+
}
|
|
31
|
+
const message = unknownErr instanceof Error ? unknownErr.message : String(unknownErr);
|
|
32
|
+
return {
|
|
33
|
+
code: "internal_error",
|
|
34
|
+
message,
|
|
35
|
+
...fallbackTool ? { tool: fallbackTool } : {}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
var NATIVE_HOST_NAME, DEFAULT_HTTP_PORT, CHROME_WEB_STORE_EXTENSION_ID, LEGACY_DEV_EXTENSION_ID, LOCAL_UNPACKED_EXTENSION_ID, DEFAULT_EXTENSION_ID, DEFAULT_EXTENSION_IDS, TOOL_NAMES, RelayError;
|
|
39
|
+
var init_dist = __esm({
|
|
40
|
+
"../protocol/dist/index.js"() {
|
|
41
|
+
"use strict";
|
|
42
|
+
NATIVE_HOST_NAME = "dev.chrome_relay.native_host";
|
|
43
|
+
DEFAULT_HTTP_PORT = 12122;
|
|
44
|
+
CHROME_WEB_STORE_EXTENSION_ID = "cpdiapbifblhlcpnmlmfpgfjlacebokb";
|
|
45
|
+
LEGACY_DEV_EXTENSION_ID = "cdmmkpadhnpcfjljhgpdnnljhjafmhop";
|
|
46
|
+
LOCAL_UNPACKED_EXTENSION_ID = "cleiodnaklknhhfopegimjelfibjmbkc";
|
|
47
|
+
DEFAULT_EXTENSION_ID = CHROME_WEB_STORE_EXTENSION_ID;
|
|
48
|
+
DEFAULT_EXTENSION_IDS = [
|
|
49
|
+
CHROME_WEB_STORE_EXTENSION_ID,
|
|
50
|
+
LEGACY_DEV_EXTENSION_ID,
|
|
51
|
+
LOCAL_UNPACKED_EXTENSION_ID
|
|
52
|
+
];
|
|
53
|
+
TOOL_NAMES = {
|
|
54
|
+
GET_WINDOWS_AND_TABS: "get_windows_and_tabs",
|
|
55
|
+
NAVIGATE: "chrome_navigate",
|
|
56
|
+
SWITCH_TAB: "chrome_switch_tab",
|
|
57
|
+
CLOSE_TABS: "chrome_close_tabs",
|
|
58
|
+
SCREENSHOT: "chrome_screenshot",
|
|
59
|
+
READ_PAGE: "chrome_read_page",
|
|
60
|
+
CLICK: "chrome_click_element",
|
|
61
|
+
FILL: "chrome_fill_or_select",
|
|
62
|
+
KEYBOARD: "chrome_keyboard",
|
|
63
|
+
TYPE: "chrome_type",
|
|
64
|
+
EVALUATE: "chrome_evaluate",
|
|
65
|
+
// §2.2 — viewport emulation (set/preset/clear share one tool, action via args.action)
|
|
66
|
+
VIEWPORT: "chrome_viewport",
|
|
67
|
+
// chrome_self_reload — calls chrome.runtime.reload() inside the extension.
|
|
68
|
+
// Lets the dev loop refresh the extension without manually clicking reload
|
|
69
|
+
// on chrome://extensions (Chrome blocks CDP attach on chrome:// pages).
|
|
70
|
+
SELF_RELOAD: "chrome_self_reload",
|
|
71
|
+
// §2.7c — console capture. Ring-buffer per tab; actions read/clear via args.
|
|
72
|
+
CONSOLE: "chrome_console",
|
|
73
|
+
// Workspaces — named Chrome windows for parallel agent work. Single tool
|
|
74
|
+
// with action: create | list | close. Every existing tool also accepts an
|
|
75
|
+
// optional workspaceName arg that routes ops into that workspace's window.
|
|
76
|
+
// (Was "chrome_group" pre-0.4.0; renamed because "group" collides with
|
|
77
|
+
// Chrome's own tab-group UI primitive, which is now exposed separately.)
|
|
78
|
+
WORKSPACE: "chrome_workspace",
|
|
79
|
+
// Tab groups — Chrome's native colored, collapsible folder of tabs inside
|
|
80
|
+
// a single window. Single tool with action: create | list | close | add | remove.
|
|
81
|
+
// Every existing tool also accepts an optional groupName arg that routes
|
|
82
|
+
// ops to a tab inside that tab-group.
|
|
83
|
+
GROUP: "chrome_group",
|
|
84
|
+
// §2.4 — accessibility tree. ~30× smaller than DOM serialization, more
|
|
85
|
+
// semantic. click_ax pairs with it: targets by backendDOMNodeId, no CSS.
|
|
86
|
+
AX: "chrome_ax",
|
|
87
|
+
CLICK_AX: "chrome_click_ax",
|
|
88
|
+
// §2.7a — network capture. Ring-buffer per tab; actions read/clear/har/body.
|
|
89
|
+
NETWORK: "chrome_network",
|
|
90
|
+
// Hover — dispatches mouseMoved at element center (or x,y) so :hover/
|
|
91
|
+
// :focus-within styles fire before a click or screencast frame is read.
|
|
92
|
+
HOVER: "chrome_hover",
|
|
93
|
+
// Screencast — wraps CDP Page.startScreencast / stopScreencast. SW buffers
|
|
94
|
+
// base64 JPEG frames per tab between start and stop. Paint-driven (catches
|
|
95
|
+
// CSS transitions, fade-ins, focus-ring motion) — at the cost of requiring
|
|
96
|
+
// the tab to be ACTIVE (Chrome doesn't paint backgrounded tabs). See
|
|
97
|
+
// docs/recording.md for the active-tab matrix.
|
|
98
|
+
SCREENCAST: "chrome_screencast"
|
|
99
|
+
};
|
|
100
|
+
RelayError = class extends Error {
|
|
101
|
+
code;
|
|
102
|
+
tool;
|
|
103
|
+
phase;
|
|
104
|
+
details;
|
|
105
|
+
retryable;
|
|
106
|
+
constructor(spec) {
|
|
107
|
+
super(spec.message);
|
|
108
|
+
this.name = "RelayError";
|
|
109
|
+
this.code = spec.code;
|
|
110
|
+
this.tool = spec.tool;
|
|
111
|
+
this.phase = spec.phase;
|
|
112
|
+
this.details = spec.details;
|
|
113
|
+
this.retryable = spec.retryable;
|
|
114
|
+
}
|
|
115
|
+
toBridgeError() {
|
|
116
|
+
return {
|
|
117
|
+
code: this.code,
|
|
118
|
+
message: this.message,
|
|
119
|
+
...this.tool ? { tool: this.tool } : {},
|
|
120
|
+
...this.phase ? { phase: this.phase } : {},
|
|
121
|
+
...this.details ? { details: this.details } : {},
|
|
122
|
+
...this.retryable !== void 0 ? { retryable: this.retryable } : {}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
});
|
|
2
128
|
|
|
3
129
|
// src/program.ts
|
|
4
130
|
import { Command } from "commander";
|
|
5
|
-
import { writeFileSync } from "fs";
|
|
6
131
|
|
|
7
132
|
// src/index.ts
|
|
8
|
-
var CHROME_RELAY_VERSION = true ? "0.5.
|
|
133
|
+
var CHROME_RELAY_VERSION = true ? "0.5.10" : "0.0.0-dev";
|
|
9
134
|
|
|
10
|
-
// src/
|
|
11
|
-
|
|
12
|
-
import path from "path";
|
|
13
|
-
import { chmod, mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
14
|
-
import { fileURLToPath } from "url";
|
|
135
|
+
// src/commands/shared.ts
|
|
136
|
+
init_dist();
|
|
15
137
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.details = spec.details;
|
|
40
|
-
this.retryable = spec.retryable;
|
|
138
|
+
// src/client/call.ts
|
|
139
|
+
init_dist();
|
|
140
|
+
var noticePrinted = false;
|
|
141
|
+
function emitNoticeOnce(notice) {
|
|
142
|
+
if (noticePrinted) return;
|
|
143
|
+
noticePrinted = true;
|
|
144
|
+
process.stderr.write(`[chrome-relay] ${notice}
|
|
145
|
+
`);
|
|
146
|
+
}
|
|
147
|
+
async function callToolWithMeta(name, args) {
|
|
148
|
+
const response = await fetch(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/call`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: { "content-type": "application/json" },
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
name,
|
|
153
|
+
args
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
const payload = await response.json().catch(() => null);
|
|
157
|
+
const noticeString = payload?.notice ?? payload?.notices?.[0]?.message;
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
if (noticeString) emitNoticeOnce(noticeString);
|
|
160
|
+
throw rebuildError(payload, `Bridge request failed with ${response.status}`);
|
|
41
161
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
message: this.message,
|
|
46
|
-
...this.tool ? { tool: this.tool } : {},
|
|
47
|
-
...this.phase ? { phase: this.phase } : {},
|
|
48
|
-
...this.details ? { details: this.details } : {},
|
|
49
|
-
...this.retryable !== void 0 ? { retryable: this.retryable } : {}
|
|
50
|
-
};
|
|
162
|
+
if (!payload?.ok) {
|
|
163
|
+
if (noticeString) emitNoticeOnce(noticeString);
|
|
164
|
+
throw rebuildError(payload, "Bridge call failed.");
|
|
51
165
|
}
|
|
52
|
-
|
|
166
|
+
if (noticeString) emitNoticeOnce(noticeString);
|
|
167
|
+
return { data: payload.data, notice: payload.notice, notices: payload.notices };
|
|
168
|
+
}
|
|
169
|
+
function rebuildError(payload, fallbackMessage) {
|
|
170
|
+
if (payload?.errorDetails) {
|
|
171
|
+
return new RelayError(payload.errorDetails);
|
|
172
|
+
}
|
|
173
|
+
return new Error(payload?.error || fallbackMessage);
|
|
174
|
+
}
|
|
175
|
+
async function callTool(name, args) {
|
|
176
|
+
const { data } = await callToolWithMeta(name, args);
|
|
177
|
+
return data;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/commands/shared.ts
|
|
181
|
+
function tabOpt(cmd) {
|
|
182
|
+
return cmd.option("-t, --tab <id>", "target tab ID", (v) => Number(v)).option("--workspace <name>", "target the active tab in a named workspace window (see `chrome-relay workspace`)").option("--group <name>", "target the active tab in a named tab-group (see `chrome-relay group`)");
|
|
183
|
+
}
|
|
184
|
+
function makeBaseArgs(program) {
|
|
185
|
+
return function baseArgs(opts) {
|
|
186
|
+
const parentOpts = program.opts();
|
|
187
|
+
rejectIntraScopeConflict("subcommand", {
|
|
188
|
+
tab: opts.tab,
|
|
189
|
+
workspace: opts.workspace,
|
|
190
|
+
group: opts.group
|
|
191
|
+
});
|
|
192
|
+
rejectIntraScopeConflict("program-level", {
|
|
193
|
+
workspace: parentOpts.workspace,
|
|
194
|
+
group: parentOpts.group
|
|
195
|
+
});
|
|
196
|
+
if (opts.workspace && parentOpts.workspace && opts.workspace !== parentOpts.workspace) {
|
|
197
|
+
emitTargetOverride("workspace", parentOpts.workspace, opts.workspace);
|
|
198
|
+
}
|
|
199
|
+
if (opts.group && parentOpts.group && opts.group !== parentOpts.group) {
|
|
200
|
+
emitTargetOverride("group", parentOpts.group, opts.group);
|
|
201
|
+
}
|
|
202
|
+
if (opts.tab !== void 0 && (parentOpts.workspace || parentOpts.group)) {
|
|
203
|
+
const prior = parentOpts.workspace ? `workspace=${parentOpts.workspace}` : `group=${parentOpts.group}`;
|
|
204
|
+
emitTargetOverride("tab", prior, String(opts.tab));
|
|
205
|
+
}
|
|
206
|
+
const args = {};
|
|
207
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
208
|
+
const effectiveWorkspace = opts.workspace ?? parentOpts.workspace;
|
|
209
|
+
const effectiveGroup = opts.group ?? parentOpts.group;
|
|
210
|
+
if (opts.tab === void 0 && effectiveWorkspace) args.workspaceName = effectiveWorkspace;
|
|
211
|
+
if (opts.tab === void 0 && effectiveGroup) args.groupName = effectiveGroup;
|
|
212
|
+
return args;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function rejectIntraScopeConflict(scope, fields) {
|
|
216
|
+
const present = [];
|
|
217
|
+
if (fields.tab !== void 0) present.push("--tab");
|
|
218
|
+
if (fields.workspace) present.push("--workspace");
|
|
219
|
+
if (fields.group) present.push("--group");
|
|
220
|
+
if (present.length > 1) {
|
|
221
|
+
process.stderr.write(
|
|
222
|
+
`[chrome-relay] target_conflict: ${scope} flags ${present.join(" + ")} are mutually exclusive. Pass exactly one of --tab, --workspace, or --group on the same ${scope}.
|
|
223
|
+
`
|
|
224
|
+
);
|
|
225
|
+
process.exit(2);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function emitTargetOverride(kind, from, to) {
|
|
229
|
+
process.stderr.write(
|
|
230
|
+
`[chrome-relay] target_overridden: ${kind} ${from} \u2192 ${to} (subcommand-level overrides program-level)
|
|
231
|
+
`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
async function runToolImpl(name, args) {
|
|
235
|
+
try {
|
|
236
|
+
const result = await callTool(name, args);
|
|
237
|
+
if (typeof result === "string") {
|
|
238
|
+
process.stdout.write(result + "\n");
|
|
239
|
+
} else {
|
|
240
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error instanceof RelayError) {
|
|
244
|
+
process.stderr.write(error.message + "\n");
|
|
245
|
+
process.stderr.write(JSON.stringify({ relayError: error.toBridgeError() }, null, 2) + "\n");
|
|
246
|
+
} else {
|
|
247
|
+
process.stderr.write(
|
|
248
|
+
(error instanceof Error ? error.message : String(error)) + "\n"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
var runTool = runToolImpl;
|
|
53
255
|
|
|
54
256
|
// src/install/install.ts
|
|
257
|
+
init_dist();
|
|
258
|
+
import os from "os";
|
|
259
|
+
import path from "path";
|
|
260
|
+
import { chmod, mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
261
|
+
import { fileURLToPath } from "url";
|
|
55
262
|
var APP_DIR = path.join(os.homedir(), ".chrome-relay");
|
|
56
263
|
var KNOWN_EXTENSION_IDS = [
|
|
57
264
|
["Chrome Web Store", CHROME_WEB_STORE_EXTENSION_ID],
|
|
@@ -155,49 +362,21 @@ async function runDoctor() {
|
|
|
155
362
|
}
|
|
156
363
|
}
|
|
157
364
|
|
|
158
|
-
// src/client/call.ts
|
|
159
|
-
var noticePrinted = false;
|
|
160
|
-
function emitNoticeOnce(notice) {
|
|
161
|
-
if (noticePrinted) return;
|
|
162
|
-
noticePrinted = true;
|
|
163
|
-
process.stderr.write(`[chrome-relay] ${notice}
|
|
164
|
-
`);
|
|
165
|
-
}
|
|
166
|
-
async function callToolWithMeta(name, args) {
|
|
167
|
-
const response = await fetch(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/call`, {
|
|
168
|
-
method: "POST",
|
|
169
|
-
headers: { "content-type": "application/json" },
|
|
170
|
-
body: JSON.stringify({
|
|
171
|
-
name,
|
|
172
|
-
args
|
|
173
|
-
})
|
|
174
|
-
});
|
|
175
|
-
const payload = await response.json().catch(() => null);
|
|
176
|
-
const noticeString = payload?.notice ?? payload?.notices?.[0]?.message;
|
|
177
|
-
if (!response.ok) {
|
|
178
|
-
if (noticeString) emitNoticeOnce(noticeString);
|
|
179
|
-
throw rebuildError(payload, `Bridge request failed with ${response.status}`);
|
|
180
|
-
}
|
|
181
|
-
if (!payload?.ok) {
|
|
182
|
-
if (noticeString) emitNoticeOnce(noticeString);
|
|
183
|
-
throw rebuildError(payload, "Bridge call failed.");
|
|
184
|
-
}
|
|
185
|
-
if (noticeString) emitNoticeOnce(noticeString);
|
|
186
|
-
return { data: payload.data, notice: payload.notice, notices: payload.notices };
|
|
187
|
-
}
|
|
188
|
-
function rebuildError(payload, fallbackMessage) {
|
|
189
|
-
if (payload?.errorDetails) {
|
|
190
|
-
return new RelayError(payload.errorDetails);
|
|
191
|
-
}
|
|
192
|
-
return new Error(payload?.error || fallbackMessage);
|
|
193
|
-
}
|
|
194
|
-
async function callTool(name, args) {
|
|
195
|
-
const { data } = await callToolWithMeta(name, args);
|
|
196
|
-
return data;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
365
|
// src/release-notes.ts
|
|
200
366
|
var RELEASE_NOTES = {
|
|
367
|
+
"0.5.10": [
|
|
368
|
+
"Direct /call target conflict enforcement. Third-party callers posting to /call with multiple loose target fields (tabId + workspaceName, etc.) now throw `target_conflict` instead of silently applying precedence. Matches the CLI rule the CLI itself enforced since 0.5.4.",
|
|
369
|
+
"All useful plain Error throws in extension handlers converted to RelayError(invalid_arguments). Affected tools: chrome_click_element, chrome_fill_or_select, chrome_keyboard, chrome_type, chrome_evaluate, chrome_switch_tab, chrome_close_tabs, chrome_viewport (preset name + width/height), chrome_workspace (create/close), chrome_group (create/close/add/remove), chrome_network (body without --request-id), chrome_hover (no selector or x,y), chrome_click_ax (no --node), and bbox parser. Agents can now branch on `errorDetails.code === 'invalid_arguments'` for all of these.",
|
|
370
|
+
"chrome_hover with a selector that doesn't match now throws RelayError(`element_not_found`) (was plain Error).",
|
|
371
|
+
"BEHAVIOR CHANGE \u2014 `chrome-relay screencast stop --gif/--mp4` is no longer best-effort when ffmpeg is missing. Old behavior printed 'skipping' and exited 0 (agent saw success but no GIF existed). New behavior throws `external_dependency_missing` with exit 1. Pass `--allow-missing-ffmpeg` to restore the legacy skip-with-warning behavior.",
|
|
372
|
+
"Tests: +17 in handler-strict.test.ts covering the conflict + 13 missing-arg paths. Total now 372."
|
|
373
|
+
],
|
|
374
|
+
"0.5.9": [
|
|
375
|
+
"Internal refactor (code-quality-hardening PR 7): program.ts and tools.ts split into per-domain modules. Pure code motion, no behavior change.",
|
|
376
|
+
"CLI: packages/cli/src/program.ts shrank from 1041 \u2192 75 lines. Per-domain modules now live in packages/cli/src/commands/{install-update,navigation,input,capture,sessions}.ts.",
|
|
377
|
+
"Extension: apps/extension/src/browser/tools.ts shrank from 891 \u2192 34 lines. Per-domain handler modules live in apps/extension/src/browser/handlers/{target,navigation,input,capture,sessions}.ts.",
|
|
378
|
+
"All 355 tests still pass without modification \u2014 the dispatcher contract (runTool name dispatch) is unchanged."
|
|
379
|
+
],
|
|
201
380
|
"0.5.8": [
|
|
202
381
|
"Internal refactor (code-quality-hardening PR 6, first cut): shared CLI helpers moved out of program.ts into packages/cli/src/commands/shared.ts.",
|
|
203
382
|
"tabOpt(), makeBaseArgs(program), and runTool() are now importable from `./commands/shared.js`. program.ts dropped ~100 lines.",
|
|
@@ -273,104 +452,8 @@ function listReleaseNotesSince(since) {
|
|
|
273
452
|
return Object.keys(RELEASE_NOTES).filter((v) => compareSemver(v, since) > 0).sort((a, b) => compareSemver(a, b)).map((version) => ({ version, bullets: RELEASE_NOTES[version] }));
|
|
274
453
|
}
|
|
275
454
|
|
|
276
|
-
// src/commands/
|
|
277
|
-
function
|
|
278
|
-
return cmd.option("-t, --tab <id>", "target tab ID", (v) => Number(v)).option("--workspace <name>", "target the active tab in a named workspace window (see `chrome-relay workspace`)").option("--group <name>", "target the active tab in a named tab-group (see `chrome-relay group`)");
|
|
279
|
-
}
|
|
280
|
-
function makeBaseArgs(program) {
|
|
281
|
-
return function baseArgs(opts) {
|
|
282
|
-
const parentOpts = program.opts();
|
|
283
|
-
rejectIntraScopeConflict("subcommand", {
|
|
284
|
-
tab: opts.tab,
|
|
285
|
-
workspace: opts.workspace,
|
|
286
|
-
group: opts.group
|
|
287
|
-
});
|
|
288
|
-
rejectIntraScopeConflict("program-level", {
|
|
289
|
-
workspace: parentOpts.workspace,
|
|
290
|
-
group: parentOpts.group
|
|
291
|
-
});
|
|
292
|
-
if (opts.workspace && parentOpts.workspace && opts.workspace !== parentOpts.workspace) {
|
|
293
|
-
emitTargetOverride("workspace", parentOpts.workspace, opts.workspace);
|
|
294
|
-
}
|
|
295
|
-
if (opts.group && parentOpts.group && opts.group !== parentOpts.group) {
|
|
296
|
-
emitTargetOverride("group", parentOpts.group, opts.group);
|
|
297
|
-
}
|
|
298
|
-
if (opts.tab !== void 0 && (parentOpts.workspace || parentOpts.group)) {
|
|
299
|
-
const prior = parentOpts.workspace ? `workspace=${parentOpts.workspace}` : `group=${parentOpts.group}`;
|
|
300
|
-
emitTargetOverride("tab", prior, String(opts.tab));
|
|
301
|
-
}
|
|
302
|
-
const args = {};
|
|
303
|
-
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
304
|
-
const effectiveWorkspace = opts.workspace ?? parentOpts.workspace;
|
|
305
|
-
const effectiveGroup = opts.group ?? parentOpts.group;
|
|
306
|
-
if (opts.tab === void 0 && effectiveWorkspace) args.workspaceName = effectiveWorkspace;
|
|
307
|
-
if (opts.tab === void 0 && effectiveGroup) args.groupName = effectiveGroup;
|
|
308
|
-
return args;
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
function rejectIntraScopeConflict(scope, fields) {
|
|
312
|
-
const present = [];
|
|
313
|
-
if (fields.tab !== void 0) present.push("--tab");
|
|
314
|
-
if (fields.workspace) present.push("--workspace");
|
|
315
|
-
if (fields.group) present.push("--group");
|
|
316
|
-
if (present.length > 1) {
|
|
317
|
-
process.stderr.write(
|
|
318
|
-
`[chrome-relay] target_conflict: ${scope} flags ${present.join(" + ")} are mutually exclusive. Pass exactly one of --tab, --workspace, or --group on the same ${scope}.
|
|
319
|
-
`
|
|
320
|
-
);
|
|
321
|
-
process.exit(2);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
function emitTargetOverride(kind, from, to) {
|
|
325
|
-
process.stderr.write(
|
|
326
|
-
`[chrome-relay] target_overridden: ${kind} ${from} \u2192 ${to} (subcommand-level overrides program-level)
|
|
327
|
-
`
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
async function runTool(name, args) {
|
|
331
|
-
try {
|
|
332
|
-
const result = await callTool(name, args);
|
|
333
|
-
if (typeof result === "string") {
|
|
334
|
-
process.stdout.write(result + "\n");
|
|
335
|
-
} else {
|
|
336
|
-
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
337
|
-
}
|
|
338
|
-
} catch (error) {
|
|
339
|
-
if (error instanceof RelayError) {
|
|
340
|
-
process.stderr.write(error.message + "\n");
|
|
341
|
-
process.stderr.write(JSON.stringify({ relayError: error.toBridgeError() }, null, 2) + "\n");
|
|
342
|
-
} else {
|
|
343
|
-
process.stderr.write(
|
|
344
|
-
(error instanceof Error ? error.message : String(error)) + "\n"
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
process.exit(1);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// src/program.ts
|
|
352
|
-
function buildProgram() {
|
|
353
|
-
const program = new Command();
|
|
354
|
-
program.name("chrome-relay").description("Connect your local Chrome browser to coding agents through a local bridge.").version(CHROME_RELAY_VERSION).showHelpAfterError().option("--workspace <name>", "target the active tab in a named workspace window (works at top level too)").option("--group <name>", "target the active tab in a named tab-group (works at top level too)").enablePositionalOptions().addHelpText(
|
|
355
|
-
"after",
|
|
356
|
-
`
|
|
357
|
-
|
|
358
|
-
Common agent flow:
|
|
359
|
-
chrome-relay tabs
|
|
360
|
-
chrome-relay navigate --tab <tabId> "https://example.com"
|
|
361
|
-
chrome-relay read --tab <tabId> -i
|
|
362
|
-
chrome-relay click --tab <tabId> "<selector>"
|
|
363
|
-
chrome-relay fill --tab <tabId> "<selector>" "value"
|
|
364
|
-
chrome-relay type --tab <tabId> -s "<selector>" "text into rich editor"
|
|
365
|
-
chrome-relay keys --tab <tabId> Enter
|
|
366
|
-
chrome-relay js --tab <tabId> "return document.title"
|
|
367
|
-
chrome-relay screenshot --tab <tabId> -o evidence.png
|
|
368
|
-
|
|
369
|
-
Notes:
|
|
370
|
-
navigate takes a URL. Use --tab to target an existing tab.
|
|
371
|
-
Tools attach via CDP and run on backgrounded tabs without stealing focus.
|
|
372
|
-
`
|
|
373
|
-
);
|
|
455
|
+
// src/commands/install-update.ts
|
|
456
|
+
function registerInstallUpdate(program) {
|
|
374
457
|
program.command("install").description("Install and register the local Chrome Relay host.").action(async () => {
|
|
375
458
|
await runInstall();
|
|
376
459
|
});
|
|
@@ -459,8 +542,11 @@ Notes:
|
|
|
459
542
|
changes
|
|
460
543
|
}, null, 2) + "\n");
|
|
461
544
|
});
|
|
462
|
-
|
|
463
|
-
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/commands/navigation.ts
|
|
548
|
+
function registerNavigation(ctx) {
|
|
549
|
+
const { program, baseArgs, run } = ctx;
|
|
464
550
|
program.command("tabs [verb]").description("List open Chrome windows and tabs. (verb 'list' is accepted as alias)").action(async (verb) => {
|
|
465
551
|
if (verb && verb !== "list") {
|
|
466
552
|
process.stderr.write(`unknown tabs verb: ${verb}. Use 'tabs' or 'tabs list'.
|
|
@@ -495,6 +581,127 @@ Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate
|
|
|
495
581
|
if (opts.inactive) args.active = false;
|
|
496
582
|
await run("chrome_navigate", args);
|
|
497
583
|
});
|
|
584
|
+
program.command("switch <tabId>").description("Activate a tab by ID.").action(async (tabId) => {
|
|
585
|
+
await run("chrome_switch_tab", { tabId: Number(tabId) });
|
|
586
|
+
});
|
|
587
|
+
program.command("close <tabIds...>").description("Close one or more tabs by ID.").action(async (tabIds) => {
|
|
588
|
+
await run("chrome_close_tabs", { tabIds: tabIds.map(Number) });
|
|
589
|
+
});
|
|
590
|
+
program.command("call <tool> [json]").description("Call any Chrome Relay tool with raw JSON args.").action(async (tool, json) => {
|
|
591
|
+
const args = json ? JSON.parse(json) : {};
|
|
592
|
+
await run(tool, args);
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/commands/input.ts
|
|
597
|
+
function registerInput(ctx) {
|
|
598
|
+
const { program, baseArgs, run } = ctx;
|
|
599
|
+
tabOpt(
|
|
600
|
+
program.command("click <selector>").description("Click an element by CSS selector.")
|
|
601
|
+
).action(async (selector, opts) => {
|
|
602
|
+
const args = { selector };
|
|
603
|
+
Object.assign(args, baseArgs(opts));
|
|
604
|
+
await run("chrome_click_element", args);
|
|
605
|
+
});
|
|
606
|
+
tabOpt(
|
|
607
|
+
program.command("fill <selector> <value>").description("Fill an input or textarea.")
|
|
608
|
+
).action(async (selector, value, opts) => {
|
|
609
|
+
const args = { selector, value };
|
|
610
|
+
Object.assign(args, baseArgs(opts));
|
|
611
|
+
await run("chrome_fill_or_select", args);
|
|
612
|
+
});
|
|
613
|
+
tabOpt(
|
|
614
|
+
program.command("keys <keys>").description("Press a single key or chord via trusted CDP input (e.g. Enter, Cmd+K).").addHelpText(
|
|
615
|
+
"after",
|
|
616
|
+
`
|
|
617
|
+
|
|
618
|
+
Examples:
|
|
619
|
+
chrome-relay keys Enter
|
|
620
|
+
chrome-relay keys Tab
|
|
621
|
+
chrome-relay keys Cmd+K
|
|
622
|
+
chrome-relay keys Shift+ArrowDown
|
|
623
|
+
|
|
624
|
+
For typing text into a field, use \`chrome-relay type\` instead.
|
|
625
|
+
`
|
|
626
|
+
)
|
|
627
|
+
).action(async (keys, opts) => {
|
|
628
|
+
const args = { keys };
|
|
629
|
+
Object.assign(args, baseArgs(opts));
|
|
630
|
+
await run("chrome_keyboard", args);
|
|
631
|
+
});
|
|
632
|
+
tabOpt(
|
|
633
|
+
program.command("type <text>").description("Insert text via trusted CDP input. Works in contenteditable / Draft.js / Lexical.").option("-s, --selector <selector>", "focus this element first").addHelpText(
|
|
634
|
+
"after",
|
|
635
|
+
`
|
|
636
|
+
|
|
637
|
+
Examples:
|
|
638
|
+
chrome-relay type --selector "[data-testid=tweetTextarea_0]" "hello world"
|
|
639
|
+
chrome-relay type "appended into already-focused element"
|
|
640
|
+
|
|
641
|
+
When to pick which:
|
|
642
|
+
fill \u2014 plain <input>, <textarea>, <select>, React-controlled inputs (atomic write).
|
|
643
|
+
type \u2014 contenteditable, Draft.js, Lexical, ProseMirror (trusted text commit).
|
|
644
|
+
keys \u2014 Enter, Tab, Esc, arrows, modifier chords (single key press).
|
|
645
|
+
js \u2014 anything else.
|
|
646
|
+
`
|
|
647
|
+
)
|
|
648
|
+
).action(async (text, opts) => {
|
|
649
|
+
const args = { text };
|
|
650
|
+
Object.assign(args, baseArgs(opts));
|
|
651
|
+
if (opts.selector) args.selector = opts.selector;
|
|
652
|
+
await run("chrome_type", args);
|
|
653
|
+
});
|
|
654
|
+
tabOpt(
|
|
655
|
+
program.command("js <code>").description("Evaluate JavaScript in the page MAIN world. Use `return` for the value.").option("--timeout-ms <ms>", "execution timeout in milliseconds (default 15000)", (v) => Number(v)).addHelpText(
|
|
656
|
+
"after",
|
|
657
|
+
`
|
|
658
|
+
|
|
659
|
+
Examples:
|
|
660
|
+
chrome-relay js "return document.title"
|
|
661
|
+
chrome-relay js "return await fetch('/api/me').then(r => r.json())"
|
|
662
|
+
chrome-relay js --tab 12345 "return document.querySelectorAll('article').length"
|
|
663
|
+
|
|
664
|
+
Notes:
|
|
665
|
+
Code is wrapped in an async IIFE. Top-level await works. Use \`return\` to send a value back.
|
|
666
|
+
Returned value is JSON-serialized. DOM nodes and functions become {}.
|
|
667
|
+
Runs in MAIN world: page globals, framework internals, and shadow roots are reachable.
|
|
668
|
+
`
|
|
669
|
+
)
|
|
670
|
+
).action(async (code, opts) => {
|
|
671
|
+
const args = { code };
|
|
672
|
+
Object.assign(args, baseArgs(opts));
|
|
673
|
+
if (typeof opts.timeoutMs === "number") args.timeoutMs = opts.timeoutMs;
|
|
674
|
+
await run("chrome_evaluate", args);
|
|
675
|
+
});
|
|
676
|
+
tabOpt(
|
|
677
|
+
program.command("hover [selector]").description("Move the pointer over an element or coordinates. Fires :hover styles.").option("--x <px>", "explicit x coordinate (CSS pixels)", (v) => Number(v)).option("--y <px>", "explicit y coordinate (CSS pixels)", (v) => Number(v)).addHelpText(
|
|
678
|
+
"after",
|
|
679
|
+
`
|
|
680
|
+
|
|
681
|
+
Examples:
|
|
682
|
+
chrome-relay hover --tab 123 'button[title="Install runner"]'
|
|
683
|
+
chrome-relay hover --tab 123 --x 1327 --y 771
|
|
684
|
+
|
|
685
|
+
Use before screencast to capture hover-driven micro-states (button glow,
|
|
686
|
+
tooltip appearance, etc.) that a bare click would skip past too quickly.
|
|
687
|
+
`
|
|
688
|
+
)
|
|
689
|
+
).action(async (selector, opts) => {
|
|
690
|
+
const args = {};
|
|
691
|
+
Object.assign(args, baseArgs(opts));
|
|
692
|
+
if (selector) args.selector = selector;
|
|
693
|
+
if (typeof opts.x === "number" && typeof opts.y === "number") {
|
|
694
|
+
args.x = opts.x;
|
|
695
|
+
args.y = opts.y;
|
|
696
|
+
}
|
|
697
|
+
await run("chrome_hover", args);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/commands/capture.ts
|
|
702
|
+
import { writeFileSync } from "fs";
|
|
703
|
+
function registerCapture(ctx) {
|
|
704
|
+
const { program, baseArgs, run } = ctx;
|
|
498
705
|
tabOpt(
|
|
499
706
|
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(
|
|
500
707
|
"after",
|
|
@@ -549,92 +756,217 @@ full-tab screenshot when an agent only needs to see one component.
|
|
|
549
756
|
await run("chrome_read_page", args);
|
|
550
757
|
});
|
|
551
758
|
tabOpt(
|
|
552
|
-
program.command("
|
|
553
|
-
).action(async (selector, opts) => {
|
|
554
|
-
const args = { selector };
|
|
555
|
-
Object.assign(args, baseArgs(opts));
|
|
556
|
-
await run("chrome_click_element", args);
|
|
557
|
-
});
|
|
558
|
-
tabOpt(
|
|
559
|
-
program.command("fill <selector> <value>").description("Fill an input or textarea.")
|
|
560
|
-
).action(async (selector, value, opts) => {
|
|
561
|
-
const args = { selector, value };
|
|
562
|
-
Object.assign(args, baseArgs(opts));
|
|
563
|
-
await run("chrome_fill_or_select", args);
|
|
564
|
-
});
|
|
565
|
-
tabOpt(
|
|
566
|
-
program.command("keys <keys>").description("Press a single key or chord via trusted CDP input (e.g. Enter, Cmd+K).").addHelpText(
|
|
759
|
+
program.command("ax").description("Extract the accessibility tree \u2014 ~30\xD7 smaller than `read` and more semantic.").option("-i, --interactive-only", "filter to actionable roles (button, link, textbox, ...)").option("--root <role>", "start from the first node matching this role (e.g. 'main')").option("--include-subframes", "walk subframes too (default: top frame only)").addHelpText(
|
|
567
760
|
"after",
|
|
568
761
|
`
|
|
569
762
|
|
|
570
763
|
Examples:
|
|
571
|
-
chrome-relay
|
|
572
|
-
chrome-relay
|
|
573
|
-
chrome-relay
|
|
574
|
-
chrome-relay keys Shift+ArrowDown
|
|
764
|
+
chrome-relay ax --tab 123
|
|
765
|
+
chrome-relay ax --tab 123 --interactive-only
|
|
766
|
+
chrome-relay ax --tab 123 --root main --interactive-only
|
|
575
767
|
|
|
576
|
-
|
|
768
|
+
Notes:
|
|
769
|
+
Each node carries an "id" \u2014 that's the backendDOMNodeId. Pass it to
|
|
770
|
+
\`chrome-relay click-ax --node <id>\` to click without a CSS selector.
|
|
577
771
|
`
|
|
578
772
|
)
|
|
579
|
-
).action(async (
|
|
580
|
-
const args =
|
|
581
|
-
|
|
582
|
-
|
|
773
|
+
).action(async (opts) => {
|
|
774
|
+
const args = baseArgs(opts);
|
|
775
|
+
if (opts.interactiveOnly) args.interactiveOnly = true;
|
|
776
|
+
if (opts.root) args.rootRole = opts.root;
|
|
777
|
+
if (opts.includeSubframes) args.includeSubframes = true;
|
|
778
|
+
await run("chrome_ax", args);
|
|
583
779
|
});
|
|
584
780
|
tabOpt(
|
|
585
|
-
program.command("
|
|
781
|
+
program.command("click-ax").description("Click an element by its backendDOMNodeId from a previous `ax` call.").requiredOption("--node <id>", "backendDOMNodeId from `chrome-relay ax`", (v) => Number(v)).addHelpText(
|
|
586
782
|
"after",
|
|
587
783
|
`
|
|
588
784
|
|
|
589
785
|
Examples:
|
|
590
|
-
chrome-relay
|
|
591
|
-
chrome-relay type "appended into already-focused element"
|
|
786
|
+
chrome-relay click-ax --tab 123 --node 456
|
|
592
787
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
keys \u2014 Enter, Tab, Esc, arrows, modifier chords (single key press).
|
|
597
|
-
js \u2014 anything else.
|
|
788
|
+
Notes:
|
|
789
|
+
Throws explicitly if the node id is stale (page mutated since you called
|
|
790
|
+
\`ax\`). Re-run \`ax\` and pass the fresh id.
|
|
598
791
|
`
|
|
599
792
|
)
|
|
600
|
-
).action(async (
|
|
601
|
-
const args =
|
|
793
|
+
).action(async (opts) => {
|
|
794
|
+
const args = baseArgs(opts);
|
|
795
|
+
args.node = opts.node;
|
|
796
|
+
await run("chrome_click_ax", args);
|
|
797
|
+
});
|
|
798
|
+
const screencast = program.command("screencast").description("Record a tab via CDP (paint-driven). Requires an active tab.").addHelpText(
|
|
799
|
+
"after",
|
|
800
|
+
`
|
|
801
|
+
|
|
802
|
+
Examples:
|
|
803
|
+
chrome-relay screencast start --tab 123 --quality 80 --max-width 900
|
|
804
|
+
# ... drive the interaction (hover, click, etc.) ...
|
|
805
|
+
chrome-relay screencast stop --tab 123 --out /tmp/recording
|
|
806
|
+
|
|
807
|
+
# The --out path becomes a directory of frame_NNNN.jpg files. If ffmpeg
|
|
808
|
+
# is on PATH and --gif is also passed, an animated GIF is written next to
|
|
809
|
+
# the frames at /tmp/recording.gif.
|
|
810
|
+
|
|
811
|
+
Notes:
|
|
812
|
+
Frames buffer in the extension service worker. A 10-second capture at
|
|
813
|
+
default settings (jpeg q=60, ~15fps, full viewport) lands ~2-3 MB.
|
|
814
|
+
Pass --max-width to downscale and lighten the buffer.
|
|
815
|
+
Each frame is base64 JPEG; the CLI decodes them when --out is given.
|
|
816
|
+
`
|
|
817
|
+
);
|
|
818
|
+
tabOpt(
|
|
819
|
+
screencast.command("start").description("Begin screencast capture on a tab.").option("--format <fmt>", "jpeg | png (default jpeg)").option("--quality <n>", "jpeg quality 0-100 (default 80)", (v) => Number(v)).option("--max-width <px>", "downscale; aspect preserved", (v) => Number(v)).option("--max-height <px>", "downscale; aspect preserved", (v) => Number(v)).option("--every-nth <n>", "throttle: keep 1 in N frames (default 1)", (v) => Number(v))
|
|
820
|
+
).action(async (opts) => {
|
|
821
|
+
const args = { action: "start" };
|
|
602
822
|
Object.assign(args, baseArgs(opts));
|
|
603
|
-
if (opts.
|
|
604
|
-
|
|
823
|
+
if (opts.format) args.format = opts.format;
|
|
824
|
+
if (typeof opts.quality === "number") args.quality = opts.quality;
|
|
825
|
+
if (typeof opts.maxWidth === "number") args.maxWidth = opts.maxWidth;
|
|
826
|
+
if (typeof opts.maxHeight === "number") args.maxHeight = opts.maxHeight;
|
|
827
|
+
if (typeof opts.everyNth === "number") args.everyNthFrame = opts.everyNth;
|
|
828
|
+
await run("chrome_screencast", args);
|
|
829
|
+
});
|
|
830
|
+
tabOpt(
|
|
831
|
+
screencast.command("stop").description("Stop the screencast and emit frames (or write to disk).").option("-o, --out <dir>", "write frames as JPEGs into this directory (created if missing)").option("--gif", "after writing frames, ffmpeg them into <dir>.gif (requires ffmpeg on PATH)").option("--mp4", "after writing frames, ffmpeg them into <dir>.mp4 (requires ffmpeg on PATH)").option("--fps <n>", "assumed framerate when invoking ffmpeg (default 15)", (v) => Number(v)).option("--no-dedupe", "keep raw frames; default collapses consecutive identical frames via SHA-256").option("--allow-missing-ffmpeg", "with --gif/--mp4: skip ffmpeg step (and emit a warning) if ffmpeg isn't on PATH, instead of failing with external_dependency_missing")
|
|
832
|
+
).action(async (opts) => {
|
|
833
|
+
const args = { action: "stop" };
|
|
834
|
+
Object.assign(args, baseArgs(opts));
|
|
835
|
+
try {
|
|
836
|
+
const result = await callTool("chrome_screencast", args);
|
|
837
|
+
if (!opts.out) {
|
|
838
|
+
const { frames, ...summary } = result;
|
|
839
|
+
process.stdout.write(JSON.stringify({ ...summary, framesOmitted: frames.length, hint: "pass --out <dir> to save" }, null, 2) + "\n");
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const { mkdirSync, writeFileSync: wf, renameSync, unlinkSync } = await import("fs");
|
|
843
|
+
const path2 = await import("path");
|
|
844
|
+
const { createHash } = await import("crypto");
|
|
845
|
+
mkdirSync(opts.out, { recursive: true });
|
|
846
|
+
result.frames.forEach((f, i) => {
|
|
847
|
+
const name = `frame_${String(i + 1).padStart(4, "0")}.jpg`;
|
|
848
|
+
wf(path2.join(opts.out, name), Buffer.from(f.data, "base64"));
|
|
849
|
+
});
|
|
850
|
+
process.stdout.write(`Wrote ${result.frames.length} frames to ${opts.out}
|
|
851
|
+
`);
|
|
852
|
+
const dedupeOn = opts.dedupe !== false;
|
|
853
|
+
if (dedupeOn && result.frames.length > 1) {
|
|
854
|
+
const hashes = result.frames.map(
|
|
855
|
+
(f) => createHash("sha256").update(Buffer.from(f.data, "base64")).digest("hex")
|
|
856
|
+
);
|
|
857
|
+
const kept = [];
|
|
858
|
+
let prev = "";
|
|
859
|
+
hashes.forEach((h, i) => {
|
|
860
|
+
if (h !== prev) kept.push(i);
|
|
861
|
+
prev = h;
|
|
862
|
+
});
|
|
863
|
+
const dropped = result.frames.length - kept.length;
|
|
864
|
+
if (dropped > 0) {
|
|
865
|
+
for (let i = 0; i < result.frames.length; i++) {
|
|
866
|
+
const src = path2.join(opts.out, `frame_${String(i + 1).padStart(4, "0")}.jpg`);
|
|
867
|
+
try {
|
|
868
|
+
unlinkSync(src);
|
|
869
|
+
} catch {
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
kept.forEach((srcIdx, newIdx) => {
|
|
873
|
+
const tmp = path2.join(opts.out, `tmp_${String(newIdx + 1).padStart(4, "0")}.jpg`);
|
|
874
|
+
wf(tmp, Buffer.from(result.frames[srcIdx].data, "base64"));
|
|
875
|
+
});
|
|
876
|
+
kept.forEach((_, newIdx) => {
|
|
877
|
+
const tmp = path2.join(opts.out, `tmp_${String(newIdx + 1).padStart(4, "0")}.jpg`);
|
|
878
|
+
const final = path2.join(opts.out, `frame_${String(newIdx + 1).padStart(4, "0")}.jpg`);
|
|
879
|
+
renameSync(tmp, final);
|
|
880
|
+
});
|
|
881
|
+
process.stdout.write(`Deduped: dropped ${dropped} identical frames, ${kept.length} remain.
|
|
882
|
+
`);
|
|
883
|
+
} else {
|
|
884
|
+
process.stdout.write(`Deduped: no consecutive duplicates found.
|
|
885
|
+
`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (opts.gif || opts.mp4) {
|
|
889
|
+
const fps = typeof opts.fps === "number" ? opts.fps : 15;
|
|
890
|
+
const { spawnSync } = await import("child_process");
|
|
891
|
+
const which = spawnSync("which", ["ffmpeg"]);
|
|
892
|
+
if (which.status !== 0) {
|
|
893
|
+
if (opts.allowMissingFfmpeg) {
|
|
894
|
+
process.stderr.write("[chrome-relay] ffmpeg not on PATH \u2014 skipping --gif/--mp4 (allow-missing-ffmpeg).\n");
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const { RelayError: RelayError2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
898
|
+
const err = new RelayError2({
|
|
899
|
+
code: "external_dependency_missing",
|
|
900
|
+
message: "chrome-relay screencast stop --gif/--mp4 requires ffmpeg on PATH. Install ffmpeg (brew install ffmpeg) or pass --allow-missing-ffmpeg to skip the stitch step with a warning.",
|
|
901
|
+
tool: "chrome_screencast",
|
|
902
|
+
phase: "ffmpeg_stitch",
|
|
903
|
+
details: { dependency: "ffmpeg", outputs: { gif: !!opts.gif, mp4: !!opts.mp4 } },
|
|
904
|
+
retryable: false
|
|
905
|
+
});
|
|
906
|
+
process.stderr.write(err.message + "\n");
|
|
907
|
+
process.stderr.write(JSON.stringify({ relayError: err.toBridgeError() }, null, 2) + "\n");
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
if (opts.gif) {
|
|
911
|
+
const gifOut = `${opts.out.replace(/\/$/, "")}.gif`;
|
|
912
|
+
const r = spawnSync("ffmpeg", [
|
|
913
|
+
"-y",
|
|
914
|
+
"-framerate",
|
|
915
|
+
String(fps),
|
|
916
|
+
"-i",
|
|
917
|
+
path2.join(opts.out, "frame_%04d.jpg"),
|
|
918
|
+
"-vf",
|
|
919
|
+
`fps=${fps},split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse`,
|
|
920
|
+
"-loop",
|
|
921
|
+
"0",
|
|
922
|
+
gifOut
|
|
923
|
+
], { stdio: "inherit" });
|
|
924
|
+
if (r.status === 0) process.stdout.write(`Wrote ${gifOut}
|
|
925
|
+
`);
|
|
926
|
+
}
|
|
927
|
+
if (opts.mp4) {
|
|
928
|
+
const mp4Out = `${opts.out.replace(/\/$/, "")}.mp4`;
|
|
929
|
+
const r = spawnSync("ffmpeg", [
|
|
930
|
+
"-y",
|
|
931
|
+
"-framerate",
|
|
932
|
+
String(fps),
|
|
933
|
+
"-i",
|
|
934
|
+
path2.join(opts.out, "frame_%04d.jpg"),
|
|
935
|
+
"-c:v",
|
|
936
|
+
"libx264",
|
|
937
|
+
"-pix_fmt",
|
|
938
|
+
"yuv420p",
|
|
939
|
+
"-crf",
|
|
940
|
+
"20",
|
|
941
|
+
mp4Out
|
|
942
|
+
], { stdio: "inherit" });
|
|
943
|
+
if (r.status === 0) process.stdout.write(`Wrote ${mp4Out}
|
|
944
|
+
`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
} catch (error) {
|
|
948
|
+
process.stderr.write(
|
|
949
|
+
(error instanceof Error ? error.message : String(error)) + "\n"
|
|
950
|
+
);
|
|
951
|
+
process.exit(1);
|
|
952
|
+
}
|
|
605
953
|
});
|
|
606
|
-
|
|
607
|
-
program.command("js <code>").description("Evaluate JavaScript in the page MAIN world. Use `return` for the value.").option("--timeout-ms <ms>", "execution timeout in milliseconds (default 15000)", (v) => Number(v)).addHelpText(
|
|
608
|
-
"after",
|
|
609
|
-
`
|
|
610
|
-
|
|
611
|
-
Examples:
|
|
612
|
-
chrome-relay js "return document.title"
|
|
613
|
-
chrome-relay js "return await fetch('/api/me').then(r => r.json())"
|
|
614
|
-
chrome-relay js --tab 12345 "return document.querySelectorAll('article').length"
|
|
954
|
+
}
|
|
615
955
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
});
|
|
631
|
-
program.command("close <tabIds...>").description("Close one or more tabs by ID.").action(async (tabIds) => {
|
|
632
|
-
await run("chrome_close_tabs", { tabIds: tabIds.map(Number) });
|
|
633
|
-
});
|
|
634
|
-
program.command("call <tool> [json]").description("Call any Chrome Relay tool with raw JSON args.").action(async (tool, json) => {
|
|
635
|
-
const args = json ? JSON.parse(json) : {};
|
|
636
|
-
await run(tool, args);
|
|
637
|
-
});
|
|
956
|
+
// src/commands/sessions.ts
|
|
957
|
+
function netFilterOpts(cmd) {
|
|
958
|
+
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));
|
|
959
|
+
}
|
|
960
|
+
function netFilterArgs(opts) {
|
|
961
|
+
const a = {};
|
|
962
|
+
if (opts.filter) a.filter = opts.filter;
|
|
963
|
+
if (opts.status) a.status = opts.status;
|
|
964
|
+
if (opts.method) a.method = opts.method;
|
|
965
|
+
if (typeof opts.limit === "number") a.limit = opts.limit;
|
|
966
|
+
return a;
|
|
967
|
+
}
|
|
968
|
+
function registerSessions(ctx) {
|
|
969
|
+
const { program, baseArgs, run } = ctx;
|
|
638
970
|
const viewport = program.command("viewport").description("Emulate device viewport, DPR, mobile flag, touch, and user agent.").addHelpText(
|
|
639
971
|
"after",
|
|
640
972
|
`
|
|
@@ -683,46 +1015,6 @@ Notes:
|
|
|
683
1015
|
program.command("self-reload").description("Restart the chrome-relay extension's service worker (picks up newly built code).").action(async () => {
|
|
684
1016
|
await run("chrome_self_reload", {});
|
|
685
1017
|
});
|
|
686
|
-
tabOpt(
|
|
687
|
-
program.command("ax").description("Extract the accessibility tree \u2014 ~30\xD7 smaller than `read` and more semantic.").option("-i, --interactive-only", "filter to actionable roles (button, link, textbox, ...)").option("--root <role>", "start from the first node matching this role (e.g. 'main')").option("--include-subframes", "walk subframes too (default: top frame only)").addHelpText(
|
|
688
|
-
"after",
|
|
689
|
-
`
|
|
690
|
-
|
|
691
|
-
Examples:
|
|
692
|
-
chrome-relay ax --tab 123
|
|
693
|
-
chrome-relay ax --tab 123 --interactive-only
|
|
694
|
-
chrome-relay ax --tab 123 --root main --interactive-only
|
|
695
|
-
|
|
696
|
-
Notes:
|
|
697
|
-
Each node carries an "id" \u2014 that's the backendDOMNodeId. Pass it to
|
|
698
|
-
\`chrome-relay click-ax --node <id>\` to click without a CSS selector.
|
|
699
|
-
`
|
|
700
|
-
)
|
|
701
|
-
).action(async (opts) => {
|
|
702
|
-
const args = baseArgs(opts);
|
|
703
|
-
if (opts.interactiveOnly) args.interactiveOnly = true;
|
|
704
|
-
if (opts.root) args.rootRole = opts.root;
|
|
705
|
-
if (opts.includeSubframes) args.includeSubframes = true;
|
|
706
|
-
await run("chrome_ax", args);
|
|
707
|
-
});
|
|
708
|
-
tabOpt(
|
|
709
|
-
program.command("click-ax").description("Click an element by its backendDOMNodeId from a previous `ax` call.").requiredOption("--node <id>", "backendDOMNodeId from `chrome-relay ax`", (v) => Number(v)).addHelpText(
|
|
710
|
-
"after",
|
|
711
|
-
`
|
|
712
|
-
|
|
713
|
-
Examples:
|
|
714
|
-
chrome-relay click-ax --tab 123 --node 456
|
|
715
|
-
|
|
716
|
-
Notes:
|
|
717
|
-
Throws explicitly if the node id is stale (page mutated since you called
|
|
718
|
-
\`ax\`). Re-run \`ax\` and pass the fresh id.
|
|
719
|
-
`
|
|
720
|
-
)
|
|
721
|
-
).action(async (opts) => {
|
|
722
|
-
const args = baseArgs(opts);
|
|
723
|
-
args.node = opts.node;
|
|
724
|
-
await run("chrome_click_ax", args);
|
|
725
|
-
});
|
|
726
1018
|
const workspace = program.command("workspace").description("Manage named Chrome windows so multiple agents can drive separate windows.").addHelpText(
|
|
727
1019
|
"after",
|
|
728
1020
|
`
|
|
@@ -796,17 +1088,6 @@ Notes:
|
|
|
796
1088
|
const tabIds = String(opts.tabs).split(",").map((s) => Number(s.trim())).filter(Number.isFinite);
|
|
797
1089
|
await run("chrome_group", { action: "remove", tabIds });
|
|
798
1090
|
});
|
|
799
|
-
function netFilterOpts(cmd) {
|
|
800
|
-
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));
|
|
801
|
-
}
|
|
802
|
-
function netFilterArgs(opts) {
|
|
803
|
-
const a = {};
|
|
804
|
-
if (opts.filter) a.filter = opts.filter;
|
|
805
|
-
if (opts.status) a.status = opts.status;
|
|
806
|
-
if (opts.method) a.method = opts.method;
|
|
807
|
-
if (typeof opts.limit === "number") a.limit = opts.limit;
|
|
808
|
-
return a;
|
|
809
|
-
}
|
|
810
1091
|
const network = tabOpt(netFilterOpts(
|
|
811
1092
|
program.command("network").description("Capture HTTP request/response metadata. Ring buffer, last 200 per tab.")
|
|
812
1093
|
)).addHelpText(
|
|
@@ -896,171 +1177,41 @@ Notes:
|
|
|
896
1177
|
if (typeof opts.limit === "number") args.limit = opts.limit;
|
|
897
1178
|
await run("chrome_console", args);
|
|
898
1179
|
});
|
|
899
|
-
|
|
900
|
-
program.command("hover [selector]").description("Move the pointer over an element or coordinates. Fires :hover styles.").option("--x <px>", "explicit x coordinate (CSS pixels)", (v) => Number(v)).option("--y <px>", "explicit y coordinate (CSS pixels)", (v) => Number(v)).addHelpText(
|
|
901
|
-
"after",
|
|
902
|
-
`
|
|
903
|
-
|
|
904
|
-
Examples:
|
|
905
|
-
chrome-relay hover --tab 123 'button[title="Install runner"]'
|
|
906
|
-
chrome-relay hover --tab 123 --x 1327 --y 771
|
|
1180
|
+
}
|
|
907
1181
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
).action(async (selector, opts) => {
|
|
913
|
-
const args = {};
|
|
914
|
-
Object.assign(args, baseArgs(opts));
|
|
915
|
-
if (selector) args.selector = selector;
|
|
916
|
-
if (typeof opts.x === "number" && typeof opts.y === "number") {
|
|
917
|
-
args.x = opts.x;
|
|
918
|
-
args.y = opts.y;
|
|
919
|
-
}
|
|
920
|
-
await run("chrome_hover", args);
|
|
921
|
-
});
|
|
922
|
-
const screencast = program.command("screencast").description("Record a tab via CDP (paint-driven). Requires an active tab.").addHelpText(
|
|
1182
|
+
// src/program.ts
|
|
1183
|
+
function buildProgram() {
|
|
1184
|
+
const program = new Command();
|
|
1185
|
+
program.name("chrome-relay").description("Connect your local Chrome browser to coding agents through a local bridge.").version(CHROME_RELAY_VERSION).showHelpAfterError().option("--workspace <name>", "target the active tab in a named workspace window (works at top level too)").option("--group <name>", "target the active tab in a named tab-group (works at top level too)").enablePositionalOptions().addHelpText(
|
|
923
1186
|
"after",
|
|
924
1187
|
`
|
|
925
1188
|
|
|
926
|
-
|
|
927
|
-
chrome-relay
|
|
928
|
-
|
|
929
|
-
chrome-relay
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1189
|
+
Common agent flow:
|
|
1190
|
+
chrome-relay tabs
|
|
1191
|
+
chrome-relay navigate --tab <tabId> "https://example.com"
|
|
1192
|
+
chrome-relay read --tab <tabId> -i
|
|
1193
|
+
chrome-relay click --tab <tabId> "<selector>"
|
|
1194
|
+
chrome-relay fill --tab <tabId> "<selector>" "value"
|
|
1195
|
+
chrome-relay type --tab <tabId> -s "<selector>" "text into rich editor"
|
|
1196
|
+
chrome-relay keys --tab <tabId> Enter
|
|
1197
|
+
chrome-relay js --tab <tabId> "return document.title"
|
|
1198
|
+
chrome-relay screenshot --tab <tabId> -o evidence.png
|
|
934
1199
|
|
|
935
1200
|
Notes:
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
Pass --max-width to downscale and lighten the buffer.
|
|
939
|
-
Each frame is base64 JPEG; the CLI decodes them when --out is given.
|
|
1201
|
+
navigate takes a URL. Use --tab to target an existing tab.
|
|
1202
|
+
Tools attach via CDP and run on backgrounded tabs without stealing focus.
|
|
940
1203
|
`
|
|
941
1204
|
);
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
await run("chrome_screencast", args);
|
|
953
|
-
});
|
|
954
|
-
tabOpt(
|
|
955
|
-
screencast.command("stop").description("Stop the screencast and emit frames (or write to disk).").option("-o, --out <dir>", "write frames as JPEGs into this directory (created if missing)").option("--gif", "after writing frames, ffmpeg them into <dir>.gif").option("--mp4", "after writing frames, ffmpeg them into <dir>.mp4").option("--fps <n>", "assumed framerate when invoking ffmpeg (default 15)", (v) => Number(v)).option("--no-dedupe", "keep raw frames; default collapses consecutive identical frames via SHA-256")
|
|
956
|
-
).action(async (opts) => {
|
|
957
|
-
const args = { action: "stop" };
|
|
958
|
-
Object.assign(args, baseArgs(opts));
|
|
959
|
-
try {
|
|
960
|
-
const result = await callTool("chrome_screencast", args);
|
|
961
|
-
if (!opts.out) {
|
|
962
|
-
const { frames, ...summary } = result;
|
|
963
|
-
process.stdout.write(JSON.stringify({ ...summary, framesOmitted: frames.length, hint: "pass --out <dir> to save" }, null, 2) + "\n");
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
const { mkdirSync, writeFileSync: writeFileSync2, renameSync, unlinkSync } = await import("fs");
|
|
967
|
-
const path2 = await import("path");
|
|
968
|
-
const { createHash } = await import("crypto");
|
|
969
|
-
mkdirSync(opts.out, { recursive: true });
|
|
970
|
-
result.frames.forEach((f, i) => {
|
|
971
|
-
const name = `frame_${String(i + 1).padStart(4, "0")}.jpg`;
|
|
972
|
-
writeFileSync2(path2.join(opts.out, name), Buffer.from(f.data, "base64"));
|
|
973
|
-
});
|
|
974
|
-
process.stdout.write(`Wrote ${result.frames.length} frames to ${opts.out}
|
|
975
|
-
`);
|
|
976
|
-
const dedupeOn = opts.dedupe !== false;
|
|
977
|
-
if (dedupeOn && result.frames.length > 1) {
|
|
978
|
-
const hashes = result.frames.map(
|
|
979
|
-
(f) => createHash("sha256").update(Buffer.from(f.data, "base64")).digest("hex")
|
|
980
|
-
);
|
|
981
|
-
const kept = [];
|
|
982
|
-
let prev = "";
|
|
983
|
-
hashes.forEach((h, i) => {
|
|
984
|
-
if (h !== prev) kept.push(i);
|
|
985
|
-
prev = h;
|
|
986
|
-
});
|
|
987
|
-
const dropped = result.frames.length - kept.length;
|
|
988
|
-
if (dropped > 0) {
|
|
989
|
-
for (let i = 0; i < result.frames.length; i++) {
|
|
990
|
-
const src = path2.join(opts.out, `frame_${String(i + 1).padStart(4, "0")}.jpg`);
|
|
991
|
-
try {
|
|
992
|
-
unlinkSync(src);
|
|
993
|
-
} catch {
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
kept.forEach((srcIdx, newIdx) => {
|
|
997
|
-
const tmp = path2.join(opts.out, `tmp_${String(newIdx + 1).padStart(4, "0")}.jpg`);
|
|
998
|
-
writeFileSync2(tmp, Buffer.from(result.frames[srcIdx].data, "base64"));
|
|
999
|
-
});
|
|
1000
|
-
kept.forEach((_, newIdx) => {
|
|
1001
|
-
const tmp = path2.join(opts.out, `tmp_${String(newIdx + 1).padStart(4, "0")}.jpg`);
|
|
1002
|
-
const final = path2.join(opts.out, `frame_${String(newIdx + 1).padStart(4, "0")}.jpg`);
|
|
1003
|
-
renameSync(tmp, final);
|
|
1004
|
-
});
|
|
1005
|
-
process.stdout.write(`Deduped: dropped ${dropped} identical frames, ${kept.length} remain.
|
|
1006
|
-
`);
|
|
1007
|
-
} else {
|
|
1008
|
-
process.stdout.write(`Deduped: no consecutive duplicates found.
|
|
1009
|
-
`);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
if (opts.gif || opts.mp4) {
|
|
1013
|
-
const fps = typeof opts.fps === "number" ? opts.fps : 15;
|
|
1014
|
-
const { spawnSync } = await import("child_process");
|
|
1015
|
-
const which = spawnSync("which", ["ffmpeg"]);
|
|
1016
|
-
if (which.status !== 0) {
|
|
1017
|
-
process.stderr.write("[chrome-relay] ffmpeg not on PATH \u2014 skipping --gif/--mp4.\n");
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
if (opts.gif) {
|
|
1021
|
-
const gifOut = `${opts.out.replace(/\/$/, "")}.gif`;
|
|
1022
|
-
const r = spawnSync("ffmpeg", [
|
|
1023
|
-
"-y",
|
|
1024
|
-
"-framerate",
|
|
1025
|
-
String(fps),
|
|
1026
|
-
"-i",
|
|
1027
|
-
path2.join(opts.out, "frame_%04d.jpg"),
|
|
1028
|
-
"-vf",
|
|
1029
|
-
`fps=${fps},split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse`,
|
|
1030
|
-
"-loop",
|
|
1031
|
-
"0",
|
|
1032
|
-
gifOut
|
|
1033
|
-
], { stdio: "inherit" });
|
|
1034
|
-
if (r.status === 0) process.stdout.write(`Wrote ${gifOut}
|
|
1035
|
-
`);
|
|
1036
|
-
}
|
|
1037
|
-
if (opts.mp4) {
|
|
1038
|
-
const mp4Out = `${opts.out.replace(/\/$/, "")}.mp4`;
|
|
1039
|
-
const r = spawnSync("ffmpeg", [
|
|
1040
|
-
"-y",
|
|
1041
|
-
"-framerate",
|
|
1042
|
-
String(fps),
|
|
1043
|
-
"-i",
|
|
1044
|
-
path2.join(opts.out, "frame_%04d.jpg"),
|
|
1045
|
-
"-c:v",
|
|
1046
|
-
"libx264",
|
|
1047
|
-
"-pix_fmt",
|
|
1048
|
-
"yuv420p",
|
|
1049
|
-
"-crf",
|
|
1050
|
-
"20",
|
|
1051
|
-
mp4Out
|
|
1052
|
-
], { stdio: "inherit" });
|
|
1053
|
-
if (r.status === 0) process.stdout.write(`Wrote ${mp4Out}
|
|
1054
|
-
`);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
} catch (error) {
|
|
1058
|
-
process.stderr.write(
|
|
1059
|
-
(error instanceof Error ? error.message : String(error)) + "\n"
|
|
1060
|
-
);
|
|
1061
|
-
process.exit(1);
|
|
1062
|
-
}
|
|
1063
|
-
});
|
|
1205
|
+
const ctx = {
|
|
1206
|
+
program,
|
|
1207
|
+
baseArgs: makeBaseArgs(program),
|
|
1208
|
+
run: runTool
|
|
1209
|
+
};
|
|
1210
|
+
registerInstallUpdate(program);
|
|
1211
|
+
registerNavigation(ctx);
|
|
1212
|
+
registerInput(ctx);
|
|
1213
|
+
registerCapture(ctx);
|
|
1214
|
+
registerSessions(ctx);
|
|
1064
1215
|
return program;
|
|
1065
1216
|
}
|
|
1066
1217
|
|
package/dist/index.js
CHANGED
package/dist/native-host.js
CHANGED
|
@@ -48,7 +48,7 @@ function toBridgeError(unknownErr, fallbackTool) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// src/index.ts
|
|
51
|
-
var CHROME_RELAY_VERSION = true ? "0.5.
|
|
51
|
+
var CHROME_RELAY_VERSION = true ? "0.5.10" : "0.0.0-dev";
|
|
52
52
|
|
|
53
53
|
// src/release-notes.ts
|
|
54
54
|
function compareSemver(a, b) {
|