@rynx-ai/cli 0.1.11-beta.3 → 0.1.11-beta.31

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.
@@ -1,5 +1,5 @@
1
1
  import type { RuntimeBrowserBootstrapCredential } from "@rynx-ai/protocol/runtime-browser-bootstrap";
2
- export type BrowserCliSubcommand = "install" | "update" | "version" | "clean" | "open" | "status" | "pages" | "endpoint" | "snapshot" | "navigate" | "click" | "type" | "screenshot" | "close";
2
+ export type BrowserCliSubcommand = "install" | "update" | "version" | "clean" | "open" | "status" | "pages" | "endpoint" | "snapshot" | "navigate" | "click" | "type" | "screenshot" | "headers" | "close";
3
3
  export interface BrowserCliArgs {
4
4
  subcommand: BrowserCliSubcommand;
5
5
  json: boolean;
@@ -17,6 +17,12 @@ export interface BrowserCliArgs {
17
17
  quality?: number;
18
18
  x?: number;
19
19
  y?: number;
20
+ headersAction?: "get" | "set";
21
+ headers?: string[];
22
+ expectedRevision?: string;
23
+ headersEnabled?: boolean;
24
+ clearHeaders?: boolean;
25
+ showHeaderValues?: boolean;
20
26
  }
21
27
  export interface BrowserCliTarget {
22
28
  runtimeSelector: string;
@@ -29,17 +29,23 @@ const SPECS = {
29
29
  values: ["--output", "--format", "--quality", "--session"],
30
30
  flags: ["--json"],
31
31
  },
32
+ headers: {
33
+ values: ["--session", "--runtime", "--header", "--expected-revision"],
34
+ flags: ["--json", "--enable", "--disable", "--clear", "--show-values"],
35
+ positionalAction: true,
36
+ },
32
37
  close: { values: ["--session", "--runtime"], flags: ["--json"] },
33
38
  };
34
39
  export function parseBrowserCliArgs(args) {
35
40
  const subcommand = args[0];
36
41
  if (!isBrowserSubcommand(subcommand)) {
37
42
  throw new Error("browser: expected install, update, version, clean, open, status, pages, endpoint, " +
38
- "snapshot, navigate, click, type, screenshot, or close");
43
+ "snapshot, navigate, click, type, screenshot, headers, or close");
39
44
  }
40
45
  const spec = SPECS[subcommand];
41
46
  const values = new Map();
42
47
  const flags = new Set();
48
+ const headerValues = [];
43
49
  const positionals = [];
44
50
  for (let index = 1; index < args.length; index += 1) {
45
51
  const arg = args[index];
@@ -51,13 +57,17 @@ export function parseBrowserCliArgs(args) {
51
57
  continue;
52
58
  }
53
59
  if (spec.values.includes(arg)) {
54
- if (values.has(arg))
60
+ if (arg !== "--header" && values.has(arg)) {
55
61
  throw new Error(`browser ${subcommand}: duplicate option ${arg}`);
62
+ }
56
63
  const value = args[index + 1];
57
64
  if (!value || value.startsWith("--")) {
58
65
  throw new Error(`browser ${subcommand}: ${arg} requires a value`);
59
66
  }
60
- values.set(arg, value);
67
+ if (arg === "--header")
68
+ headerValues.push(value);
69
+ else
70
+ values.set(arg, value);
61
71
  index += 1;
62
72
  continue;
63
73
  }
@@ -65,13 +75,13 @@ export function parseBrowserCliArgs(args) {
65
75
  }
66
76
  positionals.push(arg);
67
77
  }
68
- if (!spec.positionalUrl && positionals.length > 0) {
78
+ if (!spec.positionalUrl && !spec.positionalAction && positionals.length > 0) {
69
79
  throw new Error(`browser ${subcommand}: unexpected argument ${positionals[0]}`);
70
80
  }
71
81
  if (positionals.length > 1) {
72
82
  throw new Error(`browser ${subcommand}: unexpected extra argument ${positionals[1]}`);
73
83
  }
74
- if (positionals.length === 1 && values.has("--url")) {
84
+ if (spec.positionalUrl && positionals.length === 1 && values.has("--url")) {
75
85
  throw new Error("browser open: pass the URL once, either positionally or with --url");
76
86
  }
77
87
  const result = {
@@ -82,10 +92,45 @@ export function parseBrowserCliArgs(args) {
82
92
  ...(values.has("--version") ? { version: values.get("--version") } : {}),
83
93
  ...(values.has("--runtime") ? { runtime: values.get("--runtime") } : {}),
84
94
  ...(values.has("--session") ? { session: values.get("--session") } : {}),
85
- ...(values.has("--url") || positionals[0]
95
+ ...(spec.positionalUrl && (values.has("--url") || positionals[0])
86
96
  ? { url: values.get("--url") ?? positionals[0] }
87
97
  : {}),
88
98
  };
99
+ if (subcommand === "headers") {
100
+ const action = positionals[0];
101
+ if (action !== "get" && action !== "set") {
102
+ throw new Error("browser headers: expected get or set");
103
+ }
104
+ if (flags.has("--enable") && flags.has("--disable")) {
105
+ throw new Error("browser headers set: choose either --enable or --disable");
106
+ }
107
+ result.headersAction = action;
108
+ if (headerValues.length > 0)
109
+ result.headers = headerValues;
110
+ if (values.has("--expected-revision")) {
111
+ result.expectedRevision = nonEmpty(values.get("--expected-revision"), "--expected-revision");
112
+ }
113
+ if (flags.has("--enable"))
114
+ result.headersEnabled = true;
115
+ if (flags.has("--disable"))
116
+ result.headersEnabled = false;
117
+ if (flags.has("--clear"))
118
+ result.clearHeaders = true;
119
+ if (flags.has("--show-values"))
120
+ result.showHeaderValues = true;
121
+ if (action === "get" && (headerValues.length > 0 ||
122
+ result.expectedRevision !== undefined ||
123
+ result.headersEnabled !== undefined ||
124
+ result.clearHeaders)) {
125
+ throw new Error("browser headers get: update options are not allowed");
126
+ }
127
+ if (action === "set" && result.showHeaderValues) {
128
+ throw new Error("browser headers set: --show-values is only valid with get");
129
+ }
130
+ if (action === "set" && headerValues.length > 0 && result.clearHeaders) {
131
+ throw new Error("browser headers set: --clear cannot be combined with --header");
132
+ }
133
+ }
89
134
  if (values.has("--ref"))
90
135
  result.ref = nonEmpty(values.get("--ref"), "--ref");
91
136
  if (values.has("--selector"))
@@ -1 +1,5 @@
1
+ import type { RuntimeBrowserRequestHeadersPolicy } from "@rynx-ai/protocol/runtime-browser";
1
2
  export declare function runBrowserCommand(args: readonly string[]): Promise<number>;
3
+ export declare function projectRequestHeadersPolicyForOutput(policy: RuntimeBrowserRequestHeadersPolicy, showValues: boolean): RuntimeBrowserRequestHeadersPolicy & {
4
+ valuesRedacted: boolean;
5
+ };
@@ -4,6 +4,7 @@ import { connectBrowserAutomation, } from "@rynx-ai/browser-cdp";
4
4
  import WebSocket from "ws";
5
5
  import { parseBrowserCliArgs, resolveBrowserCliTarget, } from "../browser-cli-args.js";
6
6
  import { callManagedRuntimeBrowser, callResidentRuntime, getResidentRuntimeLocalBrowserAutomationAccess, getResidentRuntimeLocalBrowserEndpoint, readOptionalManagedRuntimeBrowserCredential, } from "../control-client.js";
7
+ import { createProgressDisplay } from "../progress-display.js";
7
8
  import { fail } from "./errors.js";
8
9
  export async function runBrowserCommand(args) {
9
10
  let parsed;
@@ -20,9 +21,18 @@ export async function runBrowserCommand(args) {
20
21
  const channel = browserReleaseChannel(parsed.channel);
21
22
  const { createBrowserArtifactManagementService } = await import("@rynx-ai/daemon/browser-artifacts");
22
23
  const artifacts = createBrowserArtifactManagementService();
23
- const result = parsed.subcommand === "install"
24
- ? await artifacts.install(parsed.version ? { version: parsed.version } : { channel }, parsed.json ? {} : { onProgress: (message) => console.error(message) })
25
- : await artifacts.update({ channel }, parsed.json ? {} : { onProgress: (message) => console.error(message) });
24
+ const progress = parsed.json ? undefined : createProgressDisplay();
25
+ progress?.start(parsed.subcommand === "install" ? "正在安装 Rynx Browser" : "正在更新 Rynx Browser");
26
+ let result;
27
+ try {
28
+ result = parsed.subcommand === "install"
29
+ ? await artifacts.install(parsed.version ? { version: parsed.version } : { channel }, progress ? { onProgress: (message) => progress.update(message) } : {})
30
+ : await artifacts.update({ channel }, progress ? { onProgress: (message) => progress.update(message) } : {});
31
+ }
32
+ catch (error) {
33
+ progress?.clear();
34
+ throw error;
35
+ }
26
36
  if (parsed.json) {
27
37
  console.log(JSON.stringify({
28
38
  resolved: result.resolved,
@@ -30,9 +40,7 @@ export async function runBrowserCommand(args) {
30
40
  }, null, 2));
31
41
  }
32
42
  else {
33
- for (const message of result.messages)
34
- console.log(message);
35
- console.log(`Active Browser engine: Chrome for Testing ${result.installed.version}.`);
43
+ progress.succeed(`Rynx Browser 已就绪:Chrome for Testing ${result.installed.version}`);
36
44
  }
37
45
  return 0;
38
46
  }
@@ -75,6 +83,30 @@ export async function runBrowserCommand(args) {
75
83
  return 0;
76
84
  }
77
85
  const target = await resolveBrowserTarget(parsed);
86
+ if (parsed.subcommand === "headers") {
87
+ const current = await callBrowserRuntime(target, "browser.request-headers.get", {
88
+ sessionId: target.sessionId,
89
+ });
90
+ if (parsed.headersAction === "get") {
91
+ if (parsed.showHeaderValues && target.credential) {
92
+ fail("browser headers get: --show-values is unavailable inside a managed Session; " +
93
+ "use the Session settings UI");
94
+ }
95
+ printRequestHeadersPolicy(current, parsed.json, parsed.showHeaderValues === true);
96
+ return 0;
97
+ }
98
+ const headers = parsed.clearHeaders
99
+ ? []
100
+ : parsed.headers?.map(parseCliRequestHeader) ?? current.headers;
101
+ const updated = await callBrowserRuntime(target, "browser.request-headers.set", {
102
+ sessionId: target.sessionId,
103
+ expectedRevision: parsed.expectedRevision ?? current.revision,
104
+ enabled: parsed.headersEnabled ?? current.enabled,
105
+ headers,
106
+ });
107
+ printRequestHeadersPolicy(updated, parsed.json, false);
108
+ return 0;
109
+ }
78
110
  if (isAutomationCommand(parsed.subcommand)) {
79
111
  return runBrowserAutomation(parsed, target);
80
112
  }
@@ -244,6 +276,43 @@ function printBrowserPages(state) {
244
276
  console.log(`${marker} ${page.pageId} ${page.url}${title}`);
245
277
  }
246
278
  }
279
+ function parseCliRequestHeader(value) {
280
+ const separator = value.indexOf(":");
281
+ if (separator <= 0) {
282
+ fail("browser headers set: --header must use 'Name: value'");
283
+ }
284
+ const name = value.slice(0, separator).trim();
285
+ const rawValue = value.slice(separator + 1);
286
+ return {
287
+ name,
288
+ value: rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue,
289
+ };
290
+ }
291
+ function printRequestHeadersPolicy(policy, json, showValues) {
292
+ const output = projectRequestHeadersPolicyForOutput(policy, showValues);
293
+ if (json) {
294
+ console.log(JSON.stringify(output, null, 2));
295
+ return;
296
+ }
297
+ console.log(`Browser request headers ${output.enabled ? "enabled" : "disabled"} · revision ${output.revision}`);
298
+ if (output.headers.length === 0) {
299
+ console.log(" (no configured headers)");
300
+ return;
301
+ }
302
+ for (const header of output.headers)
303
+ console.log(` ${header.name}: ${header.value}`);
304
+ }
305
+ export function projectRequestHeadersPolicyForOutput(policy, showValues) {
306
+ return {
307
+ enabled: policy.enabled,
308
+ revision: policy.revision,
309
+ valuesRedacted: !showValues,
310
+ headers: policy.headers.map((header) => ({
311
+ name: header.name,
312
+ value: showValues ? header.value : "[redacted]",
313
+ })),
314
+ };
315
+ }
247
316
  function isAutomationCommand(subcommand) {
248
317
  return subcommand === "snapshot"
249
318
  || subcommand === "navigate"
@@ -1,14 +1,25 @@
1
1
  import { createInterface } from "node:readline/promises";
2
- import { cancelResidentPluginInstallation, commitResidentPluginInstallation, invokeResidentPluginCommand, listResidentPlugins, prepareResidentPluginInstallation, setResidentPluginEnabled, uninstallResidentPlugin, } from "../control-client.js";
2
+ import { cancelResidentPluginInstallation, commitResidentPluginInstallation, prepareResidentPluginInstallation, setResidentPluginEnabled, uninstallResidentPlugin, } from "../control-client.js";
3
3
  import { fail } from "./errors.js";
4
- const PLUGIN_STDIN_MAX_BYTES = 256 * 1024;
5
4
  const PLUGIN_DIGEST_PATTERN = /^sha256-[A-Za-z0-9+/]{43}={0,2}$/;
6
5
  const CANONICAL_PLUGIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,62}@[a-z0-9][a-z0-9-]{0,62}(?:\/[a-z0-9][a-z0-9._-]{0,99})?$/;
6
+ const PLUGIN_USAGE = `Usage: rynx plugin <command|plugin-id>
7
+
8
+ Management:
9
+ list
10
+ install <source|plugin@market> [--force] [--expect-digest <sha256-...>]
11
+ update <plugin@market> [--expect-digest <sha256-...>]
12
+ enable|disable|uninstall <plugin@market>
13
+
14
+ Plugin commands:
15
+ <plugin-id> <command> [args...]
16
+ <plugin-id> --help`;
7
17
  export async function runPluginManageCommand(args, options = {}) {
8
18
  const [subcommand, pluginId] = args;
9
19
  switch (subcommand) {
10
20
  case "list": {
11
- const plugins = await listResidentPlugins();
21
+ const { listInstalledPlugins } = await import("@rynx-ai/daemon/plugin-cli");
22
+ const plugins = listInstalledPlugins();
12
23
  console.log("Plugins");
13
24
  if (plugins.length === 0) {
14
25
  console.log(" (none)");
@@ -126,23 +137,50 @@ export async function runPluginCommand(args) {
126
137
  const [pluginId, command] = args;
127
138
  if (!pluginId)
128
139
  fail("plugin: missing plugin id");
140
+ if (pluginId === "--help" || pluginId === "-h" || pluginId === "help") {
141
+ console.log(PLUGIN_USAGE);
142
+ return 0;
143
+ }
129
144
  if (["list", "install", "update", "uninstall", "enable", "disable"].includes(pluginId)) {
130
145
  return runPluginManageCommand(args);
131
146
  }
132
147
  if (!command)
133
148
  fail(`plugin ${pluginId}: missing command`);
134
- const stdin = process.stdin.isTTY
135
- ? undefined
136
- : await readStdinBounded(PLUGIN_STDIN_MAX_BYTES);
137
- const result = await invokeResidentPluginCommand(pluginId, args.slice(1), stdin === undefined ? {} : { stdin });
138
- if (result.stdout)
139
- process.stdout.write(result.stdout);
140
- if (result.stderr)
141
- process.stderr.write(result.stderr);
142
- if (result.truncated) {
143
- process.stderr.write("rynx: plugin command output was truncated by the daemon\n");
149
+ const pluginCli = await import("@rynx-ai/daemon/plugin-cli");
150
+ if (command === "--help" || command === "-h") {
151
+ console.log(pluginCli.installedPluginCommandUsage(pluginId));
152
+ return 0;
153
+ }
154
+ return runPluginCliWithForwardedSignals((signal) => pluginCli.runPluginCliCommand(pluginId, args.slice(1), {
155
+ stdio: "inherit",
156
+ signal,
157
+ }));
158
+ }
159
+ async function runPluginCliWithForwardedSignals(run) {
160
+ const controller = new AbortController();
161
+ let interruptedBy;
162
+ const interrupt = (signal) => {
163
+ interruptedBy ??= signal;
164
+ if (!controller.signal.aborted) {
165
+ controller.abort(new Error(`plugin command interrupted by ${signal}`));
166
+ }
167
+ };
168
+ const onSigint = () => interrupt("SIGINT");
169
+ const onSigterm = () => interrupt("SIGTERM");
170
+ process.on("SIGINT", onSigint);
171
+ process.on("SIGTERM", onSigterm);
172
+ try {
173
+ return (await run(controller.signal)).code;
174
+ }
175
+ catch (error) {
176
+ if (interruptedBy)
177
+ return interruptedBy === "SIGINT" ? 130 : 143;
178
+ throw error;
179
+ }
180
+ finally {
181
+ process.off("SIGINT", onSigint);
182
+ process.off("SIGTERM", onSigterm);
144
183
  }
145
- return result.code;
146
184
  }
147
185
  function parseInstallationArgs(operation, args) {
148
186
  const target = args[0];
@@ -219,15 +257,3 @@ async function confirmInTerminal(message) {
219
257
  prompt.close();
220
258
  }
221
259
  }
222
- async function readStdinBounded(maxBytes) {
223
- const chunks = [];
224
- let bytes = 0;
225
- for await (const chunk of process.stdin) {
226
- const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
227
- bytes += value.byteLength;
228
- if (bytes > maxBytes)
229
- fail(`plugin stdin exceeds ${maxBytes} bytes`);
230
- chunks.push(value);
231
- }
232
- return Buffer.concat(chunks, bytes).toString("utf8");
233
- }
@@ -3,10 +3,13 @@ import { chmodSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, re
3
3
  import path from "node:path";
4
4
  import * as prompts from "@clack/prompts";
5
5
  import { AGENT_RUNTIME_IDS, getRuntimeProfile, loadConfig, rynxConfigFile, } from "@rynx-ai/core";
6
- import { inspectDaemonDiagnostics, prepareBundledPlugins, } from "@rynx-ai/daemon/setup-service";
6
+ import { inspectDaemonDiagnostics, inspectSystemDependencies, prepareBundledPlugins, } from "@rynx-ai/daemon/setup-service";
7
+ import { BUNDLED_TMUX_UNAVAILABLE_MESSAGE, resolveBundledTmux, } from "@rynx-ai/tmux";
8
+ import { createProgressDisplay } from "../progress-display.js";
7
9
  import { fail } from "./errors.js";
8
10
  export async function runSetupCommand(args) {
9
11
  const options = parseSetupOptions(args);
12
+ const progress = options.json ? undefined : createProgressDisplay();
10
13
  const interactive = !options.nonInteractive &&
11
14
  !options.hasConfigurationArguments &&
12
15
  Boolean(process.stdin.isTTY && process.stdout.isTTY);
@@ -50,23 +53,30 @@ export async function runSetupCommand(args) {
50
53
  },
51
54
  };
52
55
  let failed = false;
56
+ progress?.start("正在准备 Rynx 内置插件");
53
57
  try {
54
58
  const plugins = await prepareBundledPlugins();
55
59
  result.plugins = {
56
60
  status: plugins.status,
57
61
  changed: [...plugins.installed, ...plugins.updated],
58
62
  };
63
+ progress?.succeed("Rynx 内置插件已就绪");
59
64
  }
60
65
  catch (error) {
61
66
  failed = true;
67
+ progress?.clear();
68
+ const detail = errorMessage(error);
62
69
  result.plugins = {
63
70
  status: "error",
64
- detail: errorMessage(error),
71
+ detail,
65
72
  };
73
+ console.error(`Rynx 内置插件准备失败:${detail}`);
66
74
  }
75
+ progress?.start("正在检查 Rynx Browser");
67
76
  const diagnostics = await inspectDaemonDiagnostics();
68
77
  if (!diagnostics.browser.supported) {
69
78
  result.browser = { action: options.browser, status: "unsupported" };
79
+ progress?.succeed("当前平台不支持 Rynx Browser");
70
80
  }
71
81
  else if (diagnostics.browser.installedVersion) {
72
82
  result.browser = {
@@ -74,29 +84,39 @@ export async function runSetupCommand(args) {
74
84
  status: "ready",
75
85
  version: diagnostics.browser.installedVersion,
76
86
  };
87
+ progress?.succeed(`Rynx Browser 已就绪:${diagnostics.browser.installedVersion}`);
77
88
  }
78
89
  else if (options.browser !== "skip") {
90
+ progress?.update("正在安装 Rynx Browser");
79
91
  try {
80
92
  const { createBrowserArtifactManagementService } = await import("@rynx-ai/daemon/browser-artifacts");
81
- const installed = await createBrowserArtifactManagementService().install({ channel: "stable" }, options.json
82
- ? {}
83
- : { onProgress: (message) => console.error(message) });
93
+ const installed = await createBrowserArtifactManagementService().install({ channel: "stable" }, progress ? { onProgress: (message) => progress.update(message) } : {});
84
94
  result.browser = {
85
95
  action: options.browser,
86
96
  status: "ready",
87
97
  version: installed.installed.version,
88
98
  };
99
+ progress?.succeed(`Rynx Browser 已就绪:${installed.installed.version}`);
89
100
  }
90
101
  catch (error) {
91
102
  failed = true;
103
+ progress?.clear();
104
+ const detail = errorMessage(error);
92
105
  result.browser = {
93
106
  action: options.browser,
94
107
  status: "error",
95
- detail: errorMessage(error),
108
+ detail,
96
109
  };
110
+ console.error(`Rynx Browser 安装失败:${detail}`);
97
111
  }
98
112
  }
99
- if (options.json) {
113
+ else {
114
+ progress?.succeed("已跳过 Rynx Browser 安装");
115
+ }
116
+ if (options.resultFile) {
117
+ writeJsonAtomically(options.resultFile, result);
118
+ }
119
+ else if (options.json) {
100
120
  console.log(JSON.stringify(result, null, 2));
101
121
  }
102
122
  else if (interactive) {
@@ -140,13 +160,25 @@ export async function runDoctorCommand(args) {
140
160
  return { id, installed: !probe.error && probe.status === 0 };
141
161
  });
142
162
  const daemon = await inspectDaemonDiagnostics();
163
+ const tmuxBin = resolveBundledTmux();
164
+ const systemDependencies = tmuxBin
165
+ ? await inspectSystemDependencies({ tmuxBin })
166
+ : {
167
+ tmux: {
168
+ installed: false,
169
+ detail: BUNDLED_TMUX_UNAVAILABLE_MESSAGE,
170
+ },
171
+ };
143
172
  const result = {
144
- ok: configError === undefined && runtimes.some((runtime) => runtime.installed),
173
+ ok: configError === undefined &&
174
+ runtimes.some((runtime) => runtime.installed) &&
175
+ systemDependencies.tmux.installed,
145
176
  configPath: rynxConfigFile(),
146
177
  config,
147
178
  configError,
148
179
  runtimes,
149
180
  daemon,
181
+ systemDependencies,
150
182
  };
151
183
  if (json)
152
184
  console.log(JSON.stringify(result, null, 2));
@@ -159,6 +191,7 @@ export async function runDoctorCommand(args) {
159
191
  for (const runtime of runtimes) {
160
192
  console.log(`${runtime.id}: ${runtime.installed ? "installed" : "not installed"}`);
161
193
  }
194
+ console.log(`tmux: ${systemDependencies.tmux.installed ? "installed" : "not installed"}`);
162
195
  if (daemon.browser.supported) {
163
196
  console.log(`browser: ${daemon.browser.installedVersion ?? daemon.browser.error ?? "not installed"}`);
164
197
  }
@@ -186,6 +219,17 @@ function parseSetupOptions(args) {
186
219
  options.nonInteractive = true;
187
220
  continue;
188
221
  }
222
+ if (arg === "--result-file") {
223
+ if (options.resultFile !== undefined)
224
+ fail("setup: duplicate option --result-file");
225
+ const value = args[index + 1];
226
+ if (!value || value.startsWith("--"))
227
+ fail("setup: --result-file requires a value");
228
+ options.resultFile = path.resolve(value);
229
+ options.nonInteractive = true;
230
+ index += 1;
231
+ continue;
232
+ }
189
233
  if (arg === "--install-browser" || arg === "--skip-browser") {
190
234
  if (options.browser !== "auto")
191
235
  fail("setup: choose only one Browser action");
@@ -225,6 +269,9 @@ function parseSetupOptions(args) {
225
269
  fail(`setup: unknown option ${arg}`);
226
270
  }
227
271
  }
272
+ if (options.json && options.resultFile) {
273
+ fail("setup: choose either --json or --result-file");
274
+ }
228
275
  return options;
229
276
  }
230
277
  async function collectInteractiveSetup(config) {
@@ -290,14 +337,16 @@ function readRawConfig() {
290
337
  }
291
338
  }
292
339
  function writeConfigAtomically(config) {
293
- const file = rynxConfigFile();
340
+ writeJsonAtomically(rynxConfigFile(), config);
341
+ }
342
+ function writeJsonAtomically(file, value) {
294
343
  const directory = path.dirname(file);
295
344
  mkdirSync(directory, { recursive: true, mode: 0o700 });
296
345
  const temporary = path.join(directory, `.${path.basename(file)}.${process.pid}.tmp`);
297
346
  let descriptor;
298
347
  try {
299
348
  descriptor = openSync(temporary, "wx", 0o600);
300
- writeFileSync(descriptor, `${JSON.stringify(config, null, 2)}\n`, "utf8");
349
+ writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`, "utf8");
301
350
  closeSync(descriptor);
302
351
  descriptor = undefined;
303
352
  renameSync(temporary, file);
@@ -1,8 +1,8 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { isSkillPathComponent } from "@rynx-ai/core";
4
5
  import { fail } from "./errors.js";
5
- const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
6
6
  const MAX_SKILL_BYTES = 256 * 1024;
7
7
  const BUILTIN_SKILL_NAMES = ["browser", "emulator"];
8
8
  export async function runSkillsCommand(args) {
@@ -55,7 +55,7 @@ export async function listBuiltinSkills() {
55
55
  return guides.filter((guide) => guide !== null);
56
56
  }
57
57
  export async function readBuiltinSkill(name, full = false) {
58
- if (!SKILL_NAME_PATTERN.test(name))
58
+ if (!isSkillPathComponent(name))
59
59
  return null;
60
60
  if (!BUILTIN_SKILL_NAMES.includes(name))
61
61
  return null;
@@ -437,6 +437,8 @@ const MANAGED_BROWSER_RPC_METHODS = new Set([
437
437
  "browser.page.back",
438
438
  "browser.page.forward",
439
439
  "browser.page.reload",
440
+ "browser.request-headers.get",
441
+ "browser.request-headers.set",
440
442
  ]);
441
443
  /**
442
444
  * Invoke Browser control for the caller's own managed Session. Unlike the
@@ -1,14 +1,36 @@
1
- import { DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_PENDING_COMMANDS, DESKTOP_BROWSER_HOST_PATH, DESKTOP_BROWSER_HOST_PROTOCOL_VERSION, encodeDesktopBrowserHostSurfaceFrame, parseDesktopBrowserHostClientFrame, parseDesktopBrowserHostServerFrame, } from "@rynx-ai/protocol/desktop-browser-host";
1
+ import { DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES, DESKTOP_BROWSER_HOST_MAX_PENDING_COMMANDS, DESKTOP_BROWSER_HOST_MINIMUM_PROTOCOL_VERSION, DESKTOP_BROWSER_HOST_PATH, DESKTOP_BROWSER_HOST_PROTOCOL_VERSION, encodeDesktopBrowserHostSurfaceFrame, parseDesktopBrowserHostClientFrame, parseDesktopBrowserHostServerFrame, } from "@rynx-ai/protocol/desktop-browser-host";
2
2
  import { WebSocket } from "ws";
3
3
  import { ensureDaemonControlEndpoint } from "./control-endpoint.js";
4
4
  const CONNECT_TIMEOUT_MS = 5_000;
5
5
  const MAX_BUFFERED_BYTES = DESKTOP_BROWSER_HOST_MAX_BINARY_FRAME_BYTES + DESKTOP_BROWSER_HOST_MAX_CONTROL_FRAME_BYTES;
6
6
  /** Open the sole resident daemon's authenticated loopback Desktop Host lease. */
7
7
  export async function connectResidentDesktopBrowserHost(options) {
8
+ options.signal?.throwIfAborted();
9
+ const endpoint = options.endpoint
10
+ ?? await ensureDaemonControlEndpoint({ signal: options.signal });
11
+ assertDesktopBrowserHostEndpoint(endpoint);
12
+ try {
13
+ return await connectResidentDesktopBrowserHostVersion(options, endpoint, DESKTOP_BROWSER_HOST_PROTOCOL_VERSION);
14
+ }
15
+ catch (error) {
16
+ options.signal?.throwIfAborted();
17
+ if (!isLegacyProtocolRejection(error))
18
+ throw error;
19
+ // V1 daemons reject a V2 hello before issuing a lease. Reconnect once
20
+ // with their protocol so App and daemon can roll independently. Any
21
+ // failure after a lease is returned stays on that negotiated connection.
22
+ return connectResidentDesktopBrowserHostVersion(options, endpoint, DESKTOP_BROWSER_HOST_MINIMUM_PROTOCOL_VERSION).catch((legacyError) => {
23
+ throw new Error("Desktop Browser Host could not negotiate with the daemon", {
24
+ cause: new AggregateError([error, legacyError]),
25
+ });
26
+ });
27
+ }
28
+ }
29
+ async function connectResidentDesktopBrowserHostVersion(options, endpoint, protocolVersion) {
8
30
  options.signal?.throwIfAborted();
9
31
  const hello = parseDesktopBrowserHostClientFrame({
10
32
  type: "desktop.browser.host.hello",
11
- protocolVersion: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
33
+ protocolVersion,
12
34
  hostInstanceId: options.hostInstanceId,
13
35
  capabilities: {
14
36
  semanticPageBinding: options.capabilities.semanticPageBinding,
@@ -19,9 +41,6 @@ export async function connectResidentDesktopBrowserHost(options) {
19
41
  if (hello.type !== "desktop.browser.host.hello") {
20
42
  throw new Error("Desktop Browser Host hello parser returned the wrong frame");
21
43
  }
22
- const endpoint = options.endpoint
23
- ?? await ensureDaemonControlEndpoint({ signal: options.signal });
24
- assertDesktopBrowserHostEndpoint(endpoint);
25
44
  const url = new URL(DESKTOP_BROWSER_HOST_PATH, `${endpoint.origin}/`);
26
45
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
27
46
  const socket = new WebSocket(url, {
@@ -139,7 +158,7 @@ export async function connectResidentDesktopBrowserHost(options) {
139
158
  socket.once("close", (code, reason) => {
140
159
  options.signal?.removeEventListener("abort", onAbort);
141
160
  if (!terminalError && !closing) {
142
- terminalError = new Error(`Desktop Browser Host lease closed (${code})${reason.length > 0 ? `: ${reason.toString("utf8")}` : ""}`);
161
+ terminalError = new DesktopBrowserHostLeaseClosedError(code, `Desktop Browser Host lease closed (${code})${reason.length > 0 ? `: ${reason.toString("utf8")}` : ""}`);
143
162
  }
144
163
  if (!lease && terminalError)
145
164
  leaseReject(terminalError);
@@ -224,7 +243,7 @@ export async function connectResidentDesktopBrowserHost(options) {
224
243
  signal: controller.signal,
225
244
  reply: (result) => settle(command, {
226
245
  type: "desktop.browser.host.reply",
227
- protocolVersion: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
246
+ protocolVersion: activeLease.protocolVersion,
228
247
  leaseId: activeLease.leaseId,
229
248
  commandId: command.commandId,
230
249
  ok: true,
@@ -232,7 +251,7 @@ export async function connectResidentDesktopBrowserHost(options) {
232
251
  }),
233
252
  reject: (failure) => settle(command, {
234
253
  type: "desktop.browser.host.reply",
235
- protocolVersion: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
254
+ protocolVersion: activeLease.protocolVersion,
236
255
  leaseId: activeLease.leaseId,
237
256
  commandId: command.commandId,
238
257
  ok: false,
@@ -250,7 +269,7 @@ export async function connectResidentDesktopBrowserHost(options) {
250
269
  const nextSequence = eventSequence + 1;
251
270
  const frame = parseDesktopBrowserHostClientFrame({
252
271
  type: "desktop.browser.host.event",
253
- protocolVersion: DESKTOP_BROWSER_HOST_PROTOCOL_VERSION,
272
+ protocolVersion: activeLease.protocolVersion,
254
273
  leaseId: activeLease.leaseId,
255
274
  eventSequence: nextSequence,
256
275
  event,
@@ -268,7 +287,7 @@ export async function connectResidentDesktopBrowserHost(options) {
268
287
  const encoded = encodeDesktopBrowserHostSurfaceFrame({
269
288
  ...frame,
270
289
  leaseId: activeLease.leaseId,
271
- });
290
+ }, activeLease.protocolVersion);
272
291
  await sendBinary(socket, encoded);
273
292
  }),
274
293
  close: async () => {
@@ -285,6 +304,20 @@ export async function connectResidentDesktopBrowserHost(options) {
285
304
  },
286
305
  };
287
306
  }
307
+ class DesktopBrowserHostLeaseClosedError extends Error {
308
+ closeCode;
309
+ constructor(closeCode, message) {
310
+ super(message);
311
+ this.closeCode = closeCode;
312
+ this.name = "DesktopBrowserHostLeaseClosedError";
313
+ }
314
+ }
315
+ function isLegacyProtocolRejection(error) {
316
+ return error instanceof DesktopBrowserHostLeaseClosedError &&
317
+ // 4400 is the pre-V2 daemon's generic invalid-frame response. 1002 covers
318
+ // standards-based V1 peers; 4406 is the explicit unsupported-version code.
319
+ (error.closeCode === 1002 || error.closeCode === 4400 || error.closeCode === 4406);
320
+ }
288
321
  async function sendBinary(socket, bytes) {
289
322
  if (socket.readyState !== WebSocket.OPEN)
290
323
  throw new Error("Desktop Browser Host lease is closed");
@@ -0,0 +1,7 @@
1
+ export interface ProgressDisplay {
2
+ start(message: string): void;
3
+ update(message: string): void;
4
+ succeed(message: string): void;
5
+ clear(): void;
6
+ }
7
+ export declare function createProgressDisplay(): ProgressDisplay;
@@ -0,0 +1,78 @@
1
+ import * as prompts from "@clack/prompts";
2
+ const DOWNLOAD_PERCENT_PATTERN = /下载进度:(\d{1,3})%$/u;
3
+ export function createProgressDisplay() {
4
+ if (!prompts.isTTY(process.stdout) || prompts.isCI()) {
5
+ return {
6
+ start: (message) => console.log(message),
7
+ update: (message) => console.log(message),
8
+ succeed: (message) => console.log(message),
9
+ clear: () => undefined,
10
+ };
11
+ }
12
+ const spinner = prompts.spinner({ output: process.stdout });
13
+ const progress = prompts.progress({
14
+ output: process.stdout,
15
+ max: 100,
16
+ size: 28,
17
+ style: "block",
18
+ });
19
+ let spinnerActive = false;
20
+ let progressActive = false;
21
+ let completedPercent = 0;
22
+ const clear = () => {
23
+ if (progressActive)
24
+ progress.clear();
25
+ if (spinnerActive)
26
+ spinner.clear();
27
+ progressActive = false;
28
+ spinnerActive = false;
29
+ completedPercent = 0;
30
+ };
31
+ const finishDownload = () => {
32
+ if (!progressActive)
33
+ return;
34
+ progress.stop("Chrome for Testing 下载完成");
35
+ progressActive = false;
36
+ completedPercent = 0;
37
+ };
38
+ return {
39
+ start(message) {
40
+ clear();
41
+ spinner.start(message);
42
+ spinnerActive = true;
43
+ },
44
+ update(message) {
45
+ const match = DOWNLOAD_PERCENT_PATTERN.exec(message);
46
+ if (match) {
47
+ const percent = Math.min(100, Number(match[1]));
48
+ if (spinnerActive) {
49
+ spinner.clear();
50
+ spinnerActive = false;
51
+ }
52
+ if (!progressActive) {
53
+ progress.start(message);
54
+ progressActive = true;
55
+ }
56
+ progress.advance(percent - completedPercent, message);
57
+ completedPercent = percent;
58
+ return;
59
+ }
60
+ finishDownload();
61
+ if (spinnerActive)
62
+ spinner.message(message);
63
+ else {
64
+ spinner.start(message);
65
+ spinnerActive = true;
66
+ }
67
+ },
68
+ succeed(message) {
69
+ finishDownload();
70
+ if (spinnerActive)
71
+ spinner.stop(message);
72
+ else
73
+ prompts.log.success(message);
74
+ spinnerActive = false;
75
+ },
76
+ clear,
77
+ };
78
+ }
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { resolveBundledTmux } from "@rynx-ai/tmux";
3
4
  import { installedVersion } from "./version.js";
4
5
  import { ensureDaemonControlEndpoint, resolveDaemonControlEndpoint, } from "./control-endpoint.js";
5
6
  /** Resolve the canonical Builtin Skill directory shipped by this exact CLI. */
@@ -8,6 +9,7 @@ export function standaloneBuiltinSkillsDirectory(moduleUrl = import.meta.url) {
8
9
  }
9
10
  export async function startStandaloneDaemon(options = {}) {
10
11
  const cliVersion = installedVersion();
12
+ const tmuxBin = resolveBundledTmux();
11
13
  const { startDaemon } = await import("@rynx-ai/daemon/lifecycle");
12
14
  return startDaemon({
13
15
  force: options.restart ?? false,
@@ -16,6 +18,7 @@ export async function startStandaloneDaemon(options = {}) {
16
18
  builtinSkillsDir: standaloneBuiltinSkillsDirectory(),
17
19
  cliVersion,
18
20
  productVersion: cliVersion,
21
+ ...(tmuxBin ? { tmuxBin } : {}),
19
22
  },
20
23
  });
21
24
  }
package/dist/usage.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const USAGE = "Usage: rynx <command>\n\nGeneral:\n version [--json] | --version\n print the installed Rynx version\n\nSetup:\n setup [--non-interactive] [--default-runtime <codex|traex|claude>]\n [--host <host>] [--port <port>] [--log-level <level>]\n [--install-browser|--skip-browser] [--json]\n initialize configuration and local dependencies\n doctor read-only health check\n\nLifecycle:\n start | restart | stop | status | logs\n update [version] [--check] [--json]\n\nPlugins:\n market list\n market add <git-or-local-source> [--alias <id>]\n market refresh [market-id]\n market remove <market-id>\n plugin list\n plugin install <source|plugin@market> [--force] [--expect-digest <sha256-...>]\n plugin update <plugin@market> [--expect-digest <sha256-...>]\n plugin enable|disable|uninstall <plugin@market>\n plugin <plugin@market> <command> invoke a plugin-owned command\n\nAgents:\n agent list\n agent show <id>\n agent add <id>\n agent rm <id>\n\nBuiltin Skills:\n skills list [--json]\n skills get <browser|emulator> [--full] [--json]\n\nMaintenance:\n cleanup sessions [--dry-run]\n\nEmulator:\n emulator <args...>\n\nRemote Runtime:\n runtime share --address <host|ws-url> [--label <label>] [--json]\n runtime add --pairing-code <rynx://...> [--name <name>]\n runtime list [--json]\n runtime test <local|daemon-id> [--json]\n runtime forget <daemon-id>\n runtime clients list [--json]\n runtime clients revoke <grant-id>\n\nSessions:\n session fork <session-id> [--title <title>] [--json]\n\nBrowser:\n browser install|update|version|clean [...]\n browser open [url] [--session <id>] [--runtime <local|daemon-id>] [--json]\n browser status|pages|close [--session <id>] [--runtime <local|daemon-id>] [--json]\n browser endpoint [--ensure] [--session <local-id>] [--json]\n browser snapshot [--session <local-id>] [--json]\n browser navigate <url> [--session <local-id>] [--json]\n browser click (--ref <ref>|--selector <css>|--x <n> --y <n>) [--session <local-id>] [--json]\n browser type (--ref <ref>|--selector <css>) --text <text> [--session <local-id>] [--json]\n browser screenshot --output <absolute-path> [--session <local-id>] [--json]\n";
1
+ export declare const USAGE = "Usage: rynx <command>\n\nGeneral:\n version [--json] | --version\n print the installed Rynx version\n\nSetup:\n setup [--non-interactive] [--default-runtime <codex|traex|claude>]\n [--host <host>] [--port <port>] [--log-level <level>]\n [--install-browser|--skip-browser] [--json|--result-file <path>]\n initialize configuration and local dependencies\n doctor read-only health check\n\nLifecycle:\n start | restart | stop | status | logs\n update [version] [--check] [--json]\n\nPlugins:\n market list\n market add <git-or-local-source> [--alias <id>]\n market refresh [market-id]\n market remove <market-id>\n plugin list\n plugin install <source|plugin@market> [--force] [--expect-digest <sha256-...>]\n plugin update <plugin@market> [--expect-digest <sha256-...>]\n plugin enable|disable|uninstall <plugin@market>\n plugin <plugin@market> <command> invoke a plugin-owned command\n\nAgents:\n agent list\n agent show <id>\n agent add <id>\n agent rm <id>\n\nBuiltin Skills:\n skills list [--json]\n skills get <browser|emulator> [--full] [--json]\n\nMaintenance:\n cleanup sessions [--dry-run]\n\nEmulator:\n emulator <args...>\n\nRemote Runtime:\n runtime share --address <host|ws-url> [--label <label>] [--json]\n runtime add --pairing-code <rynx://...> [--name <name>]\n runtime list [--json]\n runtime test <local|daemon-id> [--json]\n runtime forget <daemon-id>\n runtime clients list [--json]\n runtime clients revoke <grant-id>\n\nSessions:\n session fork <session-id> [--title <title>] [--json]\n\nBrowser:\n browser install|update|version|clean [...]\n browser open [url] [--session <id>] [--runtime <local|daemon-id>] [--json]\n browser status|pages|close [--session <id>] [--runtime <local|daemon-id>] [--json]\n browser endpoint [--ensure] [--session <local-id>] [--json]\n browser snapshot [--session <local-id>] [--json]\n browser navigate <url> [--session <local-id>] [--json]\n browser click (--ref <ref>|--selector <css>|--x <n> --y <n>) [--session <local-id>] [--json]\n browser type (--ref <ref>|--selector <css>) --text <text> [--session <local-id>] [--json]\n browser screenshot --output <absolute-path> [--session <local-id>] [--json]\n";
package/dist/usage.js CHANGED
@@ -7,7 +7,7 @@ General:
7
7
  Setup:
8
8
  setup [--non-interactive] [--default-runtime <codex|traex|claude>]
9
9
  [--host <host>] [--port <port>] [--log-level <level>]
10
- [--install-browser|--skip-browser] [--json]
10
+ [--install-browser|--skip-browser] [--json|--result-file <path>]
11
11
  initialize configuration and local dependencies
12
12
  doctor read-only health check
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/cli",
3
- "version": "0.1.11-beta.3",
3
+ "version": "0.1.11-beta.31",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -10,7 +10,7 @@
10
10
  "license": "MIT",
11
11
  "type": "module",
12
12
  "engines": {
13
- "node": ">=22"
13
+ "node": ">=22.16"
14
14
  },
15
15
  "publishConfig": {
16
16
  "registry": "https://registry.npmjs.org/",
@@ -51,11 +51,12 @@
51
51
  "dependencies": {
52
52
  "@clack/prompts": "^1.6.0",
53
53
  "ws": "^8.21.0",
54
- "@rynx-ai/core": "0.1.11-beta.3",
55
- "@rynx-ai/browser-cdp": "0.1.11-beta.3",
56
- "@rynx-ai/emulator": "0.1.11-beta.3",
57
- "@rynx-ai/daemon": "0.1.11-beta.3",
58
- "@rynx-ai/protocol": "0.1.11-beta.3"
54
+ "@rynx-ai/core": "0.1.11-beta.31",
55
+ "@rynx-ai/daemon": "0.1.11-beta.31",
56
+ "@rynx-ai/browser-cdp": "0.1.11-beta.31",
57
+ "@rynx-ai/emulator": "0.1.11-beta.31",
58
+ "@rynx-ai/protocol": "0.1.11-beta.31",
59
+ "@rynx-ai/tmux": "0.1.11-beta.31"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@types/ws": "^8.18.1"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: browser
3
- description: Use the bundled Rynx CLI to inspect and automate the Browser owned by the active Rynx Session with snapshots, accessibility refs, navigation, clicks, typing, screenshots, page inspection, and safe failure handling.
3
+ description: Use the bundled Rynx CLI to inspect and automate the Browser owned by the active Rynx Session, and to read or update the Session-wide Browser request-header policy, with safe failure handling.
4
4
  ---
5
5
 
6
6
  # Rynx Browser
@@ -61,6 +61,46 @@ rynx browser screenshot --output /absolute/path/page.png --json
61
61
  Do not request or connect to the raw CDP endpoint unless diagnosing Rynx itself.
62
62
  Do not close a Browser generation the user still needs.
63
63
 
64
+ ## Session-wide request headers
65
+
66
+ Inspect the current policy when needed. Header values are redacted in the
67
+ managed Session CLI so secrets do not enter the Agent transcript:
68
+
69
+ ```sh
70
+ rynx browser headers get --json
71
+ ```
72
+
73
+ Set headers for every current and future Browser page in this Rynx Session.
74
+ The policy covers document and subresource requests to every origin:
75
+
76
+ ```sh
77
+ rynx browser headers set \
78
+ --enable \
79
+ --header 'X-Environment: staging' \
80
+ --header 'X-Request-Source: rynx' \
81
+ --json
82
+ ```
83
+
84
+ Changes take effect immediately. Existing pages use the new values on their
85
+ next request or refresh; requests already in flight are unchanged. Closing and
86
+ reopening the Session Browser keeps the policy. Deleting the Session or
87
+ restarting the resident daemon removes it, and a forked Session starts with no
88
+ header policy.
89
+
90
+ `set` reads the current policy internally, so `--disable` without `--header`
91
+ stops sending headers while retaining configured rows without exposing their
92
+ values. Use `--clear` to remove all rows. When an earlier `get` is the basis for
93
+ an edited update, pass its opaque revision with
94
+ `--expected-revision <revision>`; a conflict means another actor changed the
95
+ policy, so fetch it again instead of retrying stale values.
96
+
97
+ Header names are case-insensitively unique. Browser-controlled or unsafe names
98
+ such as `Authorization`, `Cookie`, `Host`, `Origin`, `Referer`, `User-Agent`,
99
+ `Content-Length`, `Proxy-*`, and `Sec-*` are rejected. Do not put header values
100
+ in logs, chat messages, or error reports. Both text and JSON CLI output use
101
+ `[redacted]`; use the Session settings UI when a human must inspect or edit a
102
+ sensitive value. Do not attempt to bypass this boundary from an active Session.
103
+
64
104
  ## Failure handling
65
105
 
66
106
  - On endpoint access failure, follow **Sandbox boundary** and make no fallback
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: rynx-cli
3
- description: Use the bundled `rynx` CLI when a task needs to inspect or automate the Browser or Emulator owned by the active Rynx Session. Load the version-matched Browser or Emulator guide from the CLI before operating it. Prefer this over direct CDP, browser drivers, `serve-sim`, `simctl`, adb, guessed ports, or guessed App installations.
3
+ description: Use the bundled `rynx` CLI when a task needs to inspect or automate the Browser or Emulator owned by the active Rynx Session, or configure Session-wide Browser request headers. Load the version-matched Browser or Emulator guide from the CLI before operating it. Prefer this over direct CDP, browser drivers, Header-modifying extensions, `serve-sim`, `simctl`, adb, guessed ports, or guessed App installations.
4
4
  ---
5
5
 
6
6
  # Rynx CLI
@@ -21,9 +21,14 @@ rynx skills get browser
21
21
  rynx skills get emulator
22
22
  ```
23
23
 
24
- Use `browser` for Browser inspection/automation and `emulator` for mobile device
25
- inspection/control. Do not load an unrelated guide. These commands read guides
26
- shipped in the exact CLI npm package and do not contact the daemon or network.
24
+ Use `browser` for Browser inspection/automation and Session-wide Browser request
25
+ headers, and `emulator` for mobile device inspection/control. Do not load an
26
+ unrelated guide. These commands read guides shipped in the exact CLI npm
27
+ package and do not contact the daemon or network.
28
+
29
+ The managed Session CLI redacts configured Browser Header values. Do not try to
30
+ recover them through alternate endpoints or tools; a human can inspect or edit
31
+ sensitive values in Session settings.
27
32
 
28
33
  If `rynx skills get <topic>` itself cannot execute, report its exact error and
29
34
  stop. Do not scan `/Applications`, use `find` or `mdfind`, run `open`, `open -a`,