chrome-relay 0.5.2 → 0.5.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/dist/cli.js CHANGED
@@ -4,15 +4,6 @@
4
4
  import { Command } from "commander";
5
5
  import { writeFileSync } from "fs";
6
6
 
7
- // src/index.ts
8
- var CHROME_RELAY_VERSION = true ? "0.5.2" : "0.0.0-dev";
9
-
10
- // src/install/install.ts
11
- import os from "os";
12
- import path from "path";
13
- import { chmod, mkdir, readFile, stat, writeFile } from "fs/promises";
14
- import { fileURLToPath } from "url";
15
-
16
7
  // ../protocol/dist/index.js
17
8
  var NATIVE_HOST_NAME = "dev.chrome_relay.native_host";
18
9
  var DEFAULT_HTTP_PORT = 12122;
@@ -24,8 +15,41 @@ var DEFAULT_EXTENSION_IDS = [
24
15
  LEGACY_DEV_EXTENSION_ID,
25
16
  LOCAL_UNPACKED_EXTENSION_ID
26
17
  ];
18
+ var RelayError = class extends Error {
19
+ code;
20
+ tool;
21
+ phase;
22
+ details;
23
+ retryable;
24
+ constructor(spec) {
25
+ super(spec.message);
26
+ this.name = "RelayError";
27
+ this.code = spec.code;
28
+ this.tool = spec.tool;
29
+ this.phase = spec.phase;
30
+ this.details = spec.details;
31
+ this.retryable = spec.retryable;
32
+ }
33
+ toBridgeError() {
34
+ return {
35
+ code: this.code,
36
+ message: this.message,
37
+ ...this.tool ? { tool: this.tool } : {},
38
+ ...this.phase ? { phase: this.phase } : {},
39
+ ...this.details ? { details: this.details } : {},
40
+ ...this.retryable !== void 0 ? { retryable: this.retryable } : {}
41
+ };
42
+ }
43
+ };
44
+
45
+ // src/index.ts
46
+ var CHROME_RELAY_VERSION = true ? "0.5.4" : "0.0.0-dev";
27
47
 
28
48
  // src/install/install.ts
49
+ import os from "os";
50
+ import path from "path";
51
+ import { chmod, mkdir, readFile, stat, writeFile } from "fs/promises";
52
+ import { fileURLToPath } from "url";
29
53
  var APP_DIR = path.join(os.homedir(), ".chrome-relay");
30
54
  var KNOWN_EXTENSION_IDS = [
31
55
  ["Chrome Web Store", CHROME_WEB_STORE_EXTENSION_ID],
@@ -140,25 +164,30 @@ function emitNoticeOnce(notice) {
140
164
  async function callToolWithMeta(name, args) {
141
165
  const response = await fetch(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/call`, {
142
166
  method: "POST",
143
- headers: {
144
- "content-type": "application/json"
145
- },
167
+ headers: { "content-type": "application/json" },
146
168
  body: JSON.stringify({
147
169
  name,
148
170
  args
149
171
  })
150
172
  });
151
173
  const payload = await response.json().catch(() => null);
174
+ const noticeString = payload?.notice ?? payload?.notices?.[0]?.message;
152
175
  if (!response.ok) {
153
- if (payload?.notice) emitNoticeOnce(payload.notice);
154
- throw new Error(payload?.error || `Bridge request failed with ${response.status}`);
176
+ if (noticeString) emitNoticeOnce(noticeString);
177
+ throw rebuildError(payload, `Bridge request failed with ${response.status}`);
155
178
  }
156
179
  if (!payload?.ok) {
157
- if (payload?.notice) emitNoticeOnce(payload.notice);
158
- throw new Error(payload?.error || "Bridge call failed.");
180
+ if (noticeString) emitNoticeOnce(noticeString);
181
+ throw rebuildError(payload, "Bridge call failed.");
159
182
  }
160
- if (payload.notice) emitNoticeOnce(payload.notice);
161
- return { data: payload.data, notice: payload.notice };
183
+ if (noticeString) emitNoticeOnce(noticeString);
184
+ return { data: payload.data, notice: payload.notice, notices: payload.notices };
185
+ }
186
+ function rebuildError(payload, fallbackMessage) {
187
+ if (payload?.errorDetails) {
188
+ return new RelayError(payload.errorDetails);
189
+ }
190
+ return new Error(payload?.error || fallbackMessage);
162
191
  }
163
192
  async function callTool(name, args) {
164
193
  const { data } = await callToolWithMeta(name, args);
@@ -167,6 +196,22 @@ async function callTool(name, args) {
167
196
 
168
197
  // src/release-notes.ts
169
198
  var RELEASE_NOTES = {
199
+ "0.5.4": [
200
+ "Strict target routing (code-quality-hardening PR 2). Within a single scope, --tab / --workspace / --group are mutually exclusive \u2014 passing more than one on the same subcommand (or both at the program level) now fails with `target_conflict` and exit code 2.",
201
+ "Cross-scope override is still allowed but visible: `chrome-relay --workspace W <cmd> --workspace W2` works, but stderr prints a `target_overridden: workspace W \u2192 W2` notice so the agent (or user) knows what happened.",
202
+ "Fixed silent drops: `viewport set` and `console` previously hand-rolled their args and ignored global --workspace/--group. They now route through baseArgs() like every other targetable command.",
203
+ "New target-routing test matrix (55 tests) proves every targetable subcommand forwards --tab, --workspace, and --group correctly \u2014 and that the strict-conflict + override behavior holds. If you add a new targetable command to the CLI, add it to TARGETABLE_COMMANDS in packages/cli/test/target-routing.test.ts.",
204
+ "New TargetSelector type in @chrome-relay/protocol (future-proofing). Wire still carries the three loose fields; a future PR migrates the extension to read a single structured `target` field."
205
+ ],
206
+ "0.5.3": [
207
+ "Structured errors and notices (code-quality-hardening PR 1). New `BridgeError` and `BridgeNotice` types in @chrome-relay/protocol carry a code, tool, phase, and details \u2014 agents can branch on `errorDetails.code === 'invalid_arguments'` instead of regex-matching message strings.",
208
+ "Tool result JSON now carries BOTH the legacy fields (`error: string`, `notice: string`) AND the new structured fields (`errorDetails: BridgeError`, `notices: BridgeNotice[]`). Old consumers keep working; new consumers prefer the structured shape.",
209
+ "The cli-outdated notice is now a `BridgeNotice` with `code:'cli_outdated'`, `details.currentVersion`, `details.expectedVersion`, and an `action.command` field.",
210
+ "Every strict parser (PR 0 strict enums) now throws `RelayError` with `code:'invalid_arguments'`, the offending tool, the phase that failed, and the list of valid choices.",
211
+ "All action-validator throws (chrome_console, chrome_network, chrome_viewport, chrome_workspace, chrome_group, chrome_screencast) carry the same structured shape.",
212
+ "Unknown tool dispatch now returns `code:'unsupported_tool'`.",
213
+ "CLI: when a RelayError flows back, stderr prints the human message AND a `relayError` JSON object so agents can parse the structured details from stderr without needing a separate flag."
214
+ ],
170
215
  "0.5.2": [
171
216
  "Strict input parsers (code-quality-hardening PR 0). Invalid console levels, network status filters, tab-group colors, and tab-id lists now throw instead of being silently dropped \u2014 an agent that asks for `errors` (typo of `error`) gets a precise error rather than all levels back.",
172
217
  "Affected tools: chrome_console (levels), chrome_network (status), chrome_group (color, tabIds), chrome_screencast (format, action), chrome_network (action).",
@@ -280,9 +325,14 @@ Notes:
280
325
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
281
326
  }
282
327
  } catch (error) {
283
- process.stderr.write(
284
- (error instanceof Error ? error.message : String(error)) + "\n"
285
- );
328
+ if (error instanceof RelayError) {
329
+ process.stderr.write(error.message + "\n");
330
+ process.stderr.write(JSON.stringify({ relayError: error.toBridgeError() }, null, 2) + "\n");
331
+ } else {
332
+ process.stderr.write(
333
+ (error instanceof Error ? error.message : String(error)) + "\n"
334
+ );
335
+ }
286
336
  process.exit(1);
287
337
  }
288
338
  }
@@ -290,15 +340,53 @@ Notes:
290
340
  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`)");
291
341
  }
292
342
  function baseArgs(opts) {
343
+ const parentOpts = program.opts();
344
+ rejectIntraScopeConflict("subcommand", {
345
+ tab: opts.tab,
346
+ workspace: opts.workspace,
347
+ group: opts.group
348
+ });
349
+ rejectIntraScopeConflict("program-level", {
350
+ workspace: parentOpts.workspace,
351
+ group: parentOpts.group
352
+ });
353
+ if (opts.workspace && parentOpts.workspace && opts.workspace !== parentOpts.workspace) {
354
+ emitTargetOverride("workspace", parentOpts.workspace, opts.workspace);
355
+ }
356
+ if (opts.group && parentOpts.group && opts.group !== parentOpts.group) {
357
+ emitTargetOverride("group", parentOpts.group, opts.group);
358
+ }
359
+ if (opts.tab !== void 0 && (parentOpts.workspace || parentOpts.group)) {
360
+ const prior = parentOpts.workspace ? `workspace=${parentOpts.workspace}` : `group=${parentOpts.group}`;
361
+ emitTargetOverride("tab", prior, String(opts.tab));
362
+ }
293
363
  const args = {};
294
364
  if (opts.tab !== void 0) args.tabId = opts.tab;
295
- const parentOpts = program.opts();
296
365
  const effectiveWorkspace = opts.workspace ?? parentOpts.workspace;
297
366
  const effectiveGroup = opts.group ?? parentOpts.group;
298
- if (effectiveWorkspace) args.workspaceName = effectiveWorkspace;
299
- if (effectiveGroup) args.groupName = effectiveGroup;
367
+ if (opts.tab === void 0 && effectiveWorkspace) args.workspaceName = effectiveWorkspace;
368
+ if (opts.tab === void 0 && effectiveGroup) args.groupName = effectiveGroup;
300
369
  return args;
301
370
  }
371
+ function rejectIntraScopeConflict(scope, fields) {
372
+ const present = [];
373
+ if (fields.tab !== void 0) present.push("--tab");
374
+ if (fields.workspace) present.push("--workspace");
375
+ if (fields.group) present.push("--group");
376
+ if (present.length > 1) {
377
+ process.stderr.write(
378
+ `[chrome-relay] target_conflict: ${scope} flags ${present.join(" + ")} are mutually exclusive. Pass exactly one of --tab, --workspace, or --group on the same ${scope}.
379
+ `
380
+ );
381
+ process.exit(2);
382
+ }
383
+ }
384
+ function emitTargetOverride(kind, from, to) {
385
+ process.stderr.write(
386
+ `[chrome-relay] target_overridden: ${kind} ${from} \u2192 ${to} (subcommand-level overrides program-level)
387
+ `
388
+ );
389
+ }
302
390
  program.command("tabs [verb]").description("List open Chrome windows and tabs. (verb 'list' is accepted as alias)").action(async (verb) => {
303
391
  if (verb && verb !== "list") {
304
392
  process.stderr.write(`unknown tabs verb: ${verb}. Use 'tabs' or 'tabs list'.
@@ -494,7 +582,7 @@ Notes:
494
582
  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")
495
583
  ).action(async (opts) => {
496
584
  const args = { action: "set", width: opts.width, height: opts.height };
497
- if (opts.tab !== void 0) args.tabId = opts.tab;
585
+ Object.assign(args, baseArgs(opts));
498
586
  if (opts.dpr !== void 0) args.dpr = opts.dpr;
499
587
  if (opts.mobile) args.mobile = true;
500
588
  if (opts.touch) args.hasTouch = true;
@@ -726,8 +814,7 @@ Notes:
726
814
  `
727
815
  )
728
816
  ).action(async (opts) => {
729
- const args = {};
730
- if (opts.tab !== void 0) args.tabId = opts.tab;
817
+ const args = baseArgs(opts);
731
818
  if (opts.clear) args.action = "clear";
732
819
  if (opts.level) args.levels = opts.level;
733
820
  if (typeof opts.since === "number") args.since = opts.since;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- var CHROME_RELAY_VERSION = true ? "0.5.2" : "0.0.0-dev";
2
+ var CHROME_RELAY_VERSION = true ? "0.5.4" : "0.0.0-dev";
3
3
  export {
4
4
  CHROME_RELAY_VERSION
5
5
  };
@@ -8,9 +8,47 @@ import Fastify from "fastify";
8
8
 
9
9
  // ../protocol/dist/index.js
10
10
  var DEFAULT_HTTP_PORT = 12122;
11
+ var RelayError = class extends Error {
12
+ code;
13
+ tool;
14
+ phase;
15
+ details;
16
+ retryable;
17
+ constructor(spec) {
18
+ super(spec.message);
19
+ this.name = "RelayError";
20
+ this.code = spec.code;
21
+ this.tool = spec.tool;
22
+ this.phase = spec.phase;
23
+ this.details = spec.details;
24
+ this.retryable = spec.retryable;
25
+ }
26
+ toBridgeError() {
27
+ return {
28
+ code: this.code,
29
+ message: this.message,
30
+ ...this.tool ? { tool: this.tool } : {},
31
+ ...this.phase ? { phase: this.phase } : {},
32
+ ...this.details ? { details: this.details } : {},
33
+ ...this.retryable !== void 0 ? { retryable: this.retryable } : {}
34
+ };
35
+ }
36
+ };
37
+ function toBridgeError(unknownErr, fallbackTool) {
38
+ if (unknownErr instanceof RelayError) {
39
+ const e = unknownErr.toBridgeError();
40
+ return fallbackTool && !e.tool ? { ...e, tool: fallbackTool } : e;
41
+ }
42
+ const message = unknownErr instanceof Error ? unknownErr.message : String(unknownErr);
43
+ return {
44
+ code: "internal_error",
45
+ message,
46
+ ...fallbackTool ? { tool: fallbackTool } : {}
47
+ };
48
+ }
11
49
 
12
50
  // src/index.ts
13
- var CHROME_RELAY_VERSION = true ? "0.5.2" : "0.0.0-dev";
51
+ var CHROME_RELAY_VERSION = true ? "0.5.4" : "0.0.0-dev";
14
52
 
15
53
  // src/release-notes.ts
16
54
  function compareSemver(a, b) {
@@ -30,7 +68,20 @@ function buildOutdatedNotice(bridge2) {
30
68
  const extVersion = bridge2.getExtensionVersion();
31
69
  if (!extVersion) return void 0;
32
70
  if (compareSemver(CHROME_RELAY_VERSION, extVersion) >= 0) return void 0;
33
- return `cli-outdated: ${CHROME_RELAY_VERSION} < extension ${extVersion}; run \`chrome-relay update\``;
71
+ return {
72
+ code: "cli_outdated",
73
+ message: `cli-outdated: ${CHROME_RELAY_VERSION} < extension ${extVersion}; run \`chrome-relay update\``,
74
+ details: {
75
+ currentVersion: CHROME_RELAY_VERSION,
76
+ expectedVersion: extVersion
77
+ },
78
+ action: { command: "chrome-relay update" }
79
+ };
80
+ }
81
+ function attachNotices(payload, notice) {
82
+ if (!notice) return;
83
+ payload.notice = notice.message;
84
+ payload.notices = [notice];
34
85
  }
35
86
  var RelayHttpServer = class {
36
87
  constructor(bridge2, port = DEFAULT_HTTP_PORT) {
@@ -63,15 +114,19 @@ var RelayHttpServer = class {
63
114
  body.args ?? {}
64
115
  );
65
116
  const notice = buildOutdatedNotice(this.bridge);
66
- reply.send(notice ? { ok: true, data, notice } : { ok: true, data });
117
+ const payload = { ok: true, data };
118
+ attachNotices(payload, notice);
119
+ reply.send(payload);
67
120
  } catch (error) {
68
121
  const notice = buildOutdatedNotice(this.bridge);
69
- const body2 = {
122
+ const errorDetails = error instanceof RelayError ? error.toBridgeError() : toBridgeError(error, body.name);
123
+ const payload = {
70
124
  ok: false,
71
- error: error instanceof Error ? error.message : String(error)
125
+ error: errorDetails.message,
126
+ errorDetails
72
127
  };
73
- if (notice) body2.notice = notice;
74
- reply.code(500).send(body2);
128
+ attachNotices(payload, notice);
129
+ reply.code(500).send(payload);
75
130
  }
76
131
  });
77
132
  await this.app.listen({ port: this.port, host: "127.0.0.1" });
@@ -135,7 +190,12 @@ var ExtensionBridge = class {
135
190
  pending.resolve(message.payload.data);
136
191
  return;
137
192
  }
138
- pending.reject(new Error(message.payload.error));
193
+ const details = message.payload.errorDetails;
194
+ if (details) {
195
+ pending.reject(new RelayError(details));
196
+ } else {
197
+ pending.reject(new Error(message.payload.error));
198
+ }
139
199
  }
140
200
  async waitUntilReady(timeoutMs = 15e3) {
141
201
  if (this.ready) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-relay",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",