chrome-relay 0.2.2 → 0.2.4
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/README.md +8 -0
- package/dist/cli.js +271 -123
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/native-host.js +0 -0
- package/package.json +18 -10
package/README.md
CHANGED
|
@@ -12,6 +12,14 @@ chrome-relay doctor
|
|
|
12
12
|
|
|
13
13
|
Then load the Chrome Relay extension in Chrome.
|
|
14
14
|
|
|
15
|
+
The native host installer allowlists the published Chrome Web Store extension ID:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
cpdiapbifblhlcpnmlmfpgfjlacebokb
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`chrome-relay doctor` prints the supported extension IDs and warns if the native-host manifest is stale.
|
|
22
|
+
|
|
15
23
|
## Usage
|
|
16
24
|
|
|
17
25
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/
|
|
3
|
+
// src/program.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { writeFileSync } from "fs";
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
|
-
var CHROME_RELAY_VERSION = "0.2.
|
|
8
|
+
var CHROME_RELAY_VERSION = "0.2.3";
|
|
9
9
|
|
|
10
10
|
// src/install/install.ts
|
|
11
11
|
import os from "os";
|
|
@@ -16,10 +16,31 @@ import { fileURLToPath } from "url";
|
|
|
16
16
|
// ../protocol/dist/index.js
|
|
17
17
|
var NATIVE_HOST_NAME = "dev.chrome_relay.native_host";
|
|
18
18
|
var DEFAULT_HTTP_PORT = 12122;
|
|
19
|
-
var
|
|
19
|
+
var CHROME_WEB_STORE_EXTENSION_ID = "cpdiapbifblhlcpnmlmfpgfjlacebokb";
|
|
20
|
+
var LEGACY_DEV_EXTENSION_ID = "cdmmkpadhnpcfjljhgpdnnljhjafmhop";
|
|
21
|
+
var LOCAL_UNPACKED_EXTENSION_ID = "cleiodnaklknhhfopegimjelfibjmbkc";
|
|
22
|
+
var DEFAULT_EXTENSION_IDS = [
|
|
23
|
+
CHROME_WEB_STORE_EXTENSION_ID,
|
|
24
|
+
LEGACY_DEV_EXTENSION_ID,
|
|
25
|
+
LOCAL_UNPACKED_EXTENSION_ID
|
|
26
|
+
];
|
|
20
27
|
|
|
21
28
|
// src/install/install.ts
|
|
22
29
|
var APP_DIR = path.join(os.homedir(), ".chrome-relay");
|
|
30
|
+
var KNOWN_EXTENSION_IDS = [
|
|
31
|
+
["Chrome Web Store", CHROME_WEB_STORE_EXTENSION_ID],
|
|
32
|
+
["legacy dev", LEGACY_DEV_EXTENSION_ID],
|
|
33
|
+
["local unpacked", LOCAL_UNPACKED_EXTENSION_ID]
|
|
34
|
+
];
|
|
35
|
+
function allowedOrigin(extensionId) {
|
|
36
|
+
return `chrome-extension://${extensionId}/`;
|
|
37
|
+
}
|
|
38
|
+
function getDefaultAllowedOrigins() {
|
|
39
|
+
return DEFAULT_EXTENSION_IDS.map(allowedOrigin);
|
|
40
|
+
}
|
|
41
|
+
function formatKnownExtensionIds() {
|
|
42
|
+
return KNOWN_EXTENSION_IDS.map(([label, id]) => `${label}: ${id}`).join(", ");
|
|
43
|
+
}
|
|
23
44
|
function getChromeManifestDir() {
|
|
24
45
|
if (process.platform === "darwin") {
|
|
25
46
|
return path.join(
|
|
@@ -54,7 +75,7 @@ async function writeManifest(wrapperPath) {
|
|
|
54
75
|
description: "Native host for Chrome Relay",
|
|
55
76
|
path: wrapperPath,
|
|
56
77
|
type: "stdio",
|
|
57
|
-
allowed_origins:
|
|
78
|
+
allowed_origins: getDefaultAllowedOrigins()
|
|
58
79
|
};
|
|
59
80
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
60
81
|
`, "utf8");
|
|
@@ -69,6 +90,7 @@ async function runInstall() {
|
|
|
69
90
|
console.log(`Wrapper: ${wrapperPath}`);
|
|
70
91
|
console.log(`Manifest: ${manifestPath}`);
|
|
71
92
|
console.log(`Local bridge port: ${DEFAULT_HTTP_PORT}`);
|
|
93
|
+
console.log(`Allowed extension IDs: ${formatKnownExtensionIds()}`);
|
|
72
94
|
}
|
|
73
95
|
async function runDoctor() {
|
|
74
96
|
try {
|
|
@@ -77,6 +99,10 @@ async function runDoctor() {
|
|
|
77
99
|
await stat(wrapperPath);
|
|
78
100
|
await stat(manifestPath);
|
|
79
101
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
102
|
+
const allowedOrigins = Array.isArray(manifest.allowed_origins) ? manifest.allowed_origins : [];
|
|
103
|
+
const missingOrigins = getDefaultAllowedOrigins().filter(
|
|
104
|
+
(origin) => !allowedOrigins.includes(origin)
|
|
105
|
+
);
|
|
80
106
|
let serverReachable = false;
|
|
81
107
|
try {
|
|
82
108
|
const response = await fetch(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/ping`);
|
|
@@ -86,7 +112,12 @@ async function runDoctor() {
|
|
|
86
112
|
}
|
|
87
113
|
console.log(`Wrapper present: yes`);
|
|
88
114
|
console.log(`Manifest present: yes`);
|
|
89
|
-
console.log(`Allowed
|
|
115
|
+
console.log(`Allowed extension IDs: ${formatKnownExtensionIds()}`);
|
|
116
|
+
console.log(`Allowed origins: ${(manifest.allowed_origins ?? ["missing"]).join(", ")}`);
|
|
117
|
+
if (missingOrigins.length > 0) {
|
|
118
|
+
console.log(`Manifest missing origins: ${missingOrigins.join(", ")}`);
|
|
119
|
+
console.log(`Tip: run "chrome-relay install" to refresh the native host manifest.`);
|
|
120
|
+
}
|
|
90
121
|
console.log(`Local bridge reachable: ${serverReachable ? "yes" : "no"}`);
|
|
91
122
|
if (!serverReachable) {
|
|
92
123
|
console.log(`Tip: load the extension so it can launch the native host.`);
|
|
@@ -120,11 +151,12 @@ async function callTool(name, args) {
|
|
|
120
151
|
return payload.data;
|
|
121
152
|
}
|
|
122
153
|
|
|
123
|
-
// src/
|
|
124
|
-
|
|
125
|
-
program
|
|
126
|
-
"
|
|
127
|
-
|
|
154
|
+
// src/program.ts
|
|
155
|
+
function buildProgram() {
|
|
156
|
+
const program = new Command();
|
|
157
|
+
program.name("chrome-relay").description("Connect your local Chrome browser to coding agents through a local bridge.").version(CHROME_RELAY_VERSION).showHelpAfterError().addHelpText(
|
|
158
|
+
"after",
|
|
159
|
+
`
|
|
128
160
|
|
|
129
161
|
Common agent flow:
|
|
130
162
|
chrome-relay tabs
|
|
@@ -132,140 +164,256 @@ Common agent flow:
|
|
|
132
164
|
chrome-relay read --tab <tabId> -i
|
|
133
165
|
chrome-relay click --tab <tabId> "<selector>"
|
|
134
166
|
chrome-relay fill --tab <tabId> "<selector>" "value"
|
|
167
|
+
chrome-relay type --tab <tabId> -s "<selector>" "text into rich editor"
|
|
168
|
+
chrome-relay keys --tab <tabId> Enter
|
|
169
|
+
chrome-relay js --tab <tabId> "return document.title"
|
|
135
170
|
chrome-relay screenshot --tab <tabId> -o evidence.png
|
|
136
171
|
|
|
137
172
|
Notes:
|
|
138
173
|
navigate takes a URL. Use --tab to target an existing tab.
|
|
139
|
-
|
|
174
|
+
Tools attach via CDP and run on backgrounded tabs without stealing focus.
|
|
140
175
|
`
|
|
141
|
-
);
|
|
142
|
-
program.command("install").description("Install and register the local Chrome Relay host.").action(async () => {
|
|
143
|
-
|
|
144
|
-
});
|
|
145
|
-
program.command("doctor").description("Validate the local Chrome Relay installation.").action(async () => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
149
|
-
async function run(name, args) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
176
|
+
);
|
|
177
|
+
program.command("install").description("Install and register the local Chrome Relay host.").action(async () => {
|
|
178
|
+
await runInstall();
|
|
179
|
+
});
|
|
180
|
+
program.command("doctor").description("Validate the local Chrome Relay installation.").action(async () => {
|
|
181
|
+
const ok = await runDoctor();
|
|
182
|
+
process.exit(ok ? 0 : 1);
|
|
183
|
+
});
|
|
184
|
+
async function run(name, args) {
|
|
185
|
+
try {
|
|
186
|
+
const result = await callTool(name, args);
|
|
187
|
+
if (typeof result === "string") {
|
|
188
|
+
process.stdout.write(result + "\n");
|
|
189
|
+
} else {
|
|
190
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
process.stderr.write(
|
|
194
|
+
(error instanceof Error ? error.message : String(error)) + "\n"
|
|
195
|
+
);
|
|
196
|
+
process.exit(1);
|
|
156
197
|
}
|
|
157
|
-
} catch (error) {
|
|
158
|
-
process.stderr.write(
|
|
159
|
-
(error instanceof Error ? error.message : String(error)) + "\n"
|
|
160
|
-
);
|
|
161
|
-
process.exit(1);
|
|
162
198
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
`
|
|
199
|
+
function tabOpt(cmd) {
|
|
200
|
+
return cmd.option("-t, --tab <id>", "target tab ID", (v) => Number(v));
|
|
201
|
+
}
|
|
202
|
+
program.command("tabs").description("List open Chrome windows and tabs.").action(async () => {
|
|
203
|
+
await run("get_windows_and_tabs", {});
|
|
204
|
+
});
|
|
205
|
+
tabOpt(
|
|
206
|
+
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("--inactive", "do not activate the tab").addHelpText(
|
|
207
|
+
"after",
|
|
208
|
+
`
|
|
174
209
|
|
|
175
210
|
Examples:
|
|
176
211
|
chrome-relay navigate "https://example.com"
|
|
177
212
|
chrome-relay navigate --tab 123456789 "https://example.com"
|
|
178
213
|
chrome-relay navigate "https://example.com" --new --inactive
|
|
179
214
|
`
|
|
180
|
-
|
|
181
|
-
).action(async (url, opts) => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
215
|
+
)
|
|
216
|
+
).action(async (url, opts) => {
|
|
217
|
+
if (/^\d+$/.test(url)) {
|
|
218
|
+
process.stderr.write(
|
|
219
|
+
`navigate expects a URL, but "${url}" looks like a tab ID.
|
|
185
220
|
Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate --tab ${url} https://example.com" to navigate it.
|
|
186
221
|
`
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
196
|
-
tabOpt(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
222
|
+
);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const args = { url };
|
|
226
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
227
|
+
if (opts.new) args.newTab = true;
|
|
228
|
+
if (opts.inactive) args.active = false;
|
|
229
|
+
await run("chrome_navigate", args);
|
|
230
|
+
});
|
|
231
|
+
tabOpt(
|
|
232
|
+
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("-o, --out <path>", "save image to path (base64 PNG decoded)").addHelpText(
|
|
233
|
+
"after",
|
|
234
|
+
`
|
|
200
235
|
|
|
201
236
|
Examples:
|
|
202
237
|
chrome-relay screenshot -o active-tab.png
|
|
203
238
|
chrome-relay screenshot --tab 123456789 -o evidence.png
|
|
239
|
+
chrome-relay screenshot --tab 123456789 --full -o full-page.png
|
|
240
|
+
chrome-relay screenshot --tab 123456789 --bbox 0,0,1280,80 -o header.png
|
|
241
|
+
chrome-relay screenshot --tab 123456789 --selector "header" -o header.png
|
|
242
|
+
chrome-relay screenshot --tab 123456789 --selector ".card:nth-child(3)" --padding 8 -o card.png
|
|
204
243
|
|
|
205
|
-
|
|
244
|
+
Region screenshots (--bbox / --selector) are ~10x cheaper in tokens than a
|
|
245
|
+
full-tab screenshot when an agent only needs to see one component.
|
|
206
246
|
`
|
|
207
|
-
|
|
208
|
-
).action(async (opts) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (opts.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
247
|
+
)
|
|
248
|
+
).action(async (opts) => {
|
|
249
|
+
const args = {};
|
|
250
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
251
|
+
if (opts.full) args.fullPage = true;
|
|
252
|
+
if (opts.bbox) args.bbox = opts.bbox;
|
|
253
|
+
if (opts.selector) args.selector = opts.selector;
|
|
254
|
+
if (typeof opts.padding === "number") args.padding = opts.padding;
|
|
255
|
+
try {
|
|
256
|
+
const result = await callTool("chrome_screenshot", args);
|
|
257
|
+
if (opts.out && result && typeof result === "object") {
|
|
258
|
+
const data = result.dataUrl ?? result.data;
|
|
259
|
+
if (typeof data === "string") {
|
|
260
|
+
const b64 = data.includes(",") ? data.split(",")[1] : data;
|
|
261
|
+
writeFileSync(opts.out, Buffer.from(b64, "base64"));
|
|
262
|
+
process.stdout.write(`Saved screenshot to ${opts.out}
|
|
220
263
|
`);
|
|
221
|
-
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
222
266
|
}
|
|
267
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
268
|
+
} catch (error) {
|
|
269
|
+
process.stderr.write(
|
|
270
|
+
(error instanceof Error ? error.message : String(error)) + "\n"
|
|
271
|
+
);
|
|
272
|
+
process.exit(1);
|
|
223
273
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
});
|
|
240
|
-
tabOpt(
|
|
241
|
-
|
|
242
|
-
).action(async (selector, opts) => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
});
|
|
247
|
-
tabOpt(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
program.command("
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
});
|
|
275
|
+
tabOpt(
|
|
276
|
+
program.command("read").description("Extract page structure and interactive elements.").option("-i, --interactive", "return only interactive elements")
|
|
277
|
+
).action(async (opts) => {
|
|
278
|
+
const args = {};
|
|
279
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
280
|
+
if (opts.interactive) args.interactiveOnly = true;
|
|
281
|
+
await run("chrome_read_page", args);
|
|
282
|
+
});
|
|
283
|
+
tabOpt(
|
|
284
|
+
program.command("click <selector>").description("Click an element by CSS selector.")
|
|
285
|
+
).action(async (selector, opts) => {
|
|
286
|
+
const args = { selector };
|
|
287
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
288
|
+
await run("chrome_click_element", args);
|
|
289
|
+
});
|
|
290
|
+
tabOpt(
|
|
291
|
+
program.command("fill <selector> <value>").description("Fill an input or textarea.")
|
|
292
|
+
).action(async (selector, value, opts) => {
|
|
293
|
+
const args = { selector, value };
|
|
294
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
295
|
+
await run("chrome_fill_or_select", args);
|
|
296
|
+
});
|
|
297
|
+
tabOpt(
|
|
298
|
+
program.command("keys <keys>").description("Press a single key or chord via trusted CDP input (e.g. Enter, Cmd+K).").addHelpText(
|
|
299
|
+
"after",
|
|
300
|
+
`
|
|
301
|
+
|
|
302
|
+
Examples:
|
|
303
|
+
chrome-relay keys Enter
|
|
304
|
+
chrome-relay keys Tab
|
|
305
|
+
chrome-relay keys Cmd+K
|
|
306
|
+
chrome-relay keys Shift+ArrowDown
|
|
307
|
+
|
|
308
|
+
For typing text into a field, use \`chrome-relay type\` instead.
|
|
309
|
+
`
|
|
310
|
+
)
|
|
311
|
+
).action(async (keys, opts) => {
|
|
312
|
+
const args = { keys };
|
|
313
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
314
|
+
await run("chrome_keyboard", args);
|
|
315
|
+
});
|
|
316
|
+
tabOpt(
|
|
317
|
+
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(
|
|
318
|
+
"after",
|
|
319
|
+
`
|
|
320
|
+
|
|
321
|
+
Examples:
|
|
322
|
+
chrome-relay type --selector "[data-testid=tweetTextarea_0]" "hello world"
|
|
323
|
+
chrome-relay type "appended into already-focused element"
|
|
324
|
+
|
|
325
|
+
When to pick which:
|
|
326
|
+
fill \u2014 plain <input>, <textarea>, <select>, React-controlled inputs (atomic write).
|
|
327
|
+
type \u2014 contenteditable, Draft.js, Lexical, ProseMirror (trusted text commit).
|
|
328
|
+
keys \u2014 Enter, Tab, Esc, arrows, modifier chords (single key press).
|
|
329
|
+
js \u2014 anything else.
|
|
330
|
+
`
|
|
331
|
+
)
|
|
332
|
+
).action(async (text, opts) => {
|
|
333
|
+
const args = { text };
|
|
334
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
335
|
+
if (opts.selector) args.selector = opts.selector;
|
|
336
|
+
await run("chrome_type", args);
|
|
337
|
+
});
|
|
338
|
+
tabOpt(
|
|
339
|
+
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(
|
|
340
|
+
"after",
|
|
341
|
+
`
|
|
342
|
+
|
|
343
|
+
Examples:
|
|
344
|
+
chrome-relay js "return document.title"
|
|
345
|
+
chrome-relay js "return await fetch('/api/me').then(r => r.json())"
|
|
346
|
+
chrome-relay js --tab 12345 "return document.querySelectorAll('article').length"
|
|
347
|
+
|
|
348
|
+
Notes:
|
|
349
|
+
Code is wrapped in an async IIFE. Top-level await works. Use \`return\` to send a value back.
|
|
350
|
+
Returned value is JSON-serialized. DOM nodes and functions become {}.
|
|
351
|
+
Runs in MAIN world: page globals, framework internals, and shadow roots are reachable.
|
|
352
|
+
`
|
|
353
|
+
)
|
|
354
|
+
).action(async (code, opts) => {
|
|
355
|
+
const args = { code };
|
|
356
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
357
|
+
if (typeof opts.timeoutMs === "number") args.timeoutMs = opts.timeoutMs;
|
|
358
|
+
await run("chrome_evaluate", args);
|
|
359
|
+
});
|
|
360
|
+
program.command("switch <tabId>").description("Activate a tab by ID.").action(async (tabId) => {
|
|
361
|
+
await run("chrome_switch_tab", { tabId: Number(tabId) });
|
|
362
|
+
});
|
|
363
|
+
program.command("close <tabIds...>").description("Close one or more tabs by ID.").action(async (tabIds) => {
|
|
364
|
+
await run("chrome_close_tabs", { tabIds: tabIds.map(Number) });
|
|
365
|
+
});
|
|
366
|
+
program.command("call <tool> [json]").description("Call any Chrome Relay tool with raw JSON args.").action(async (tool, json) => {
|
|
367
|
+
const args = json ? JSON.parse(json) : {};
|
|
368
|
+
await run(tool, args);
|
|
369
|
+
});
|
|
370
|
+
const viewport = program.command("viewport").description("Emulate device viewport, DPR, mobile flag, touch, and user agent.").addHelpText(
|
|
371
|
+
"after",
|
|
372
|
+
`
|
|
373
|
+
|
|
374
|
+
Examples:
|
|
375
|
+
chrome-relay viewport preset iphone-14 --tab 123
|
|
376
|
+
chrome-relay viewport preset desktop-1440 --tab 123
|
|
377
|
+
chrome-relay viewport set --tab 123 --width 414 --height 896 --mobile --dpr 3
|
|
378
|
+
chrome-relay viewport clear --tab 123
|
|
379
|
+
chrome-relay viewport list
|
|
380
|
+
|
|
381
|
+
Notes:
|
|
382
|
+
The override survives navigations within the tab but is wiped when the
|
|
383
|
+
debugger detaches (e.g. another extension takes over). Closing the tab
|
|
384
|
+
clears it. Re-run after detach if the page snaps back to its default size.
|
|
385
|
+
`
|
|
386
|
+
);
|
|
387
|
+
tabOpt(
|
|
388
|
+
viewport.command("set").description("Apply explicit viewport dimensions.").requiredOption("--width <px>", "viewport width in CSS pixels", (v) => Number(v)).requiredOption("--height <px>", "viewport height in CSS pixels", (v) => Number(v)).option("--dpr <ratio>", "device pixel ratio (1, 2, 3...)", (v) => Number(v)).option("--mobile", "set the mobile flag (affects meta viewport interpretation)").option("--touch", "enable touch event emulation").option("--user-agent <ua>", "override the User-Agent header")
|
|
389
|
+
).action(async (opts) => {
|
|
390
|
+
const args = { action: "set", width: opts.width, height: opts.height };
|
|
391
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
392
|
+
if (opts.dpr !== void 0) args.dpr = opts.dpr;
|
|
393
|
+
if (opts.mobile) args.mobile = true;
|
|
394
|
+
if (opts.touch) args.hasTouch = true;
|
|
395
|
+
if (opts.userAgent) args.userAgent = opts.userAgent;
|
|
396
|
+
await run("chrome_viewport", args);
|
|
397
|
+
});
|
|
398
|
+
tabOpt(
|
|
399
|
+
viewport.command("preset <name>").description("Apply a named device preset (iphone-14, pixel-7, desktop-1440, etc).")
|
|
400
|
+
).action(async (name, opts) => {
|
|
401
|
+
const args = { action: "preset", name };
|
|
402
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
403
|
+
await run("chrome_viewport", args);
|
|
404
|
+
});
|
|
405
|
+
tabOpt(
|
|
406
|
+
viewport.command("clear").description("Drop the viewport override and return the tab to its native size.")
|
|
407
|
+
).action(async (opts) => {
|
|
408
|
+
const args = { action: "clear" };
|
|
409
|
+
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
410
|
+
await run("chrome_viewport", args);
|
|
411
|
+
});
|
|
412
|
+
viewport.command("list").description("List available presets.").action(async () => {
|
|
413
|
+
await run("chrome_viewport", { action: "list" });
|
|
414
|
+
});
|
|
415
|
+
return program;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/cli.ts
|
|
419
|
+
buildProgram().parseAsync(process.argv);
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/native-host.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-relay",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"dist"
|
|
12
12
|
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "tsup",
|
|
15
|
-
"lint": "tsc -p tsconfig.json --noEmit",
|
|
16
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
17
|
-
},
|
|
18
13
|
"description": "Connect your local Chrome browser to coding agents through a local bridge.",
|
|
19
|
-
"keywords": [
|
|
14
|
+
"keywords": [
|
|
15
|
+
"chrome",
|
|
16
|
+
"browser",
|
|
17
|
+
"automation",
|
|
18
|
+
"agents",
|
|
19
|
+
"native-messaging"
|
|
20
|
+
],
|
|
20
21
|
"license": "MIT",
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"chalk": "^5.4.1",
|
|
@@ -24,7 +25,14 @@
|
|
|
24
25
|
"fastify": "^5.3.2"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
28
|
+
"tsup": "^8.4.0",
|
|
29
|
+
"vitest": "^3.0.0",
|
|
30
|
+
"@chrome-relay/protocol": "0.2.4"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
36
|
+
"test": "vitest run"
|
|
29
37
|
}
|
|
30
|
-
}
|
|
38
|
+
}
|