@runalabs/rill-cli 0.1.15 → 0.1.16

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.
Files changed (3) hide show
  1. package/README.md +20 -3
  2. package/dist/cli.js +121 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -25,6 +25,10 @@ The following local-recording instructions remain available through `record star
25
25
 
26
26
  ## Install and verify
27
27
 
28
+ Check `node --version`, `node -p "process.platform"`, and `rill --version` before installing. Reuse an installed compatible CLI. Commands execute in the agent's environment, which may be a separate sandbox from the user's desktop. Chrome must be accessible there, and the agent must be able to attach over CDP; doctor does not verify browser-control capability.
29
+
30
+ If the CLI is missing, check npm with `npm ping --fetch-timeout=5000 --fetch-retries=0` before installation. See the [canonical skill](https://userill.dev/skill.md) for the bounded Rill connectivity check and Windows Terminal fallback. Report blocked network access instead of repeating installation attempts.
31
+
28
32
  Browser recording runs on macOS, Linux, and Windows. Install Node.js 22+ and Google Chrome or Chromium. Rill encodes recordings remotely by default; local FFmpeg is optional. Chrome stays on your machine and can access localhost. Raw frames upload after recording stops.
29
33
 
30
34
  Then install the scoped package:
@@ -42,7 +46,7 @@ Do not use `npx rill`: the unscoped package belongs to a different product. Doct
42
46
 
43
47
  ### Connect with pollable approval
44
48
 
45
- `rill whoami` verifies the current account without starting sign-in. `rill connect --email you@example.com` waits for human approval by default. For agents that cannot hold a foreground command open:
49
+ `rill whoami` shows a short account summary without starting sign-in. Agents should use `rill whoami --json` for verified identity fields. `rill connect --email you@example.com` waits for human approval by default. For agents that cannot hold a foreground command open:
46
50
 
47
51
  ```sh
48
52
  rill connect --email you@example.com --no-wait
@@ -221,7 +225,20 @@ Use `rill --help` or `<command> --help` for supported flags.
221
225
 
222
226
  ## Output and errors
223
227
 
224
- Data commands emit JSON on stdout and human progress on stderr. Use global `--pretty` for indented JSON. Help and version commands print plain text.
228
+ `whoami` shows a concise human summary by default. Use `rill whoami --json` for the existing schema-versioned identity response, including email, workspace, scopes and expiry. Its exit codes are unchanged: missing credentials exit 1; connected and anonymous access exit 0.
229
+
230
+ Doctor and local recording commands retain JSON on stdout by default. Use `--human` for readable doctor, login, recording start/stop/status/retry summaries and errors. Doctor shows the command execution OS, named prerequisite checks, recovery guidance, and a pass/fail/warning count. Recording summaries show the ID, status, duration when available, share URL when returned, and the exact next command. Pending processing is distinct from a ready recording; a private recording may have no share URL.
231
+
232
+ Explicit `--json` or `--pretty` takes precedence over `--human`. Use `--pretty` for indented JSON. Human output uses restrained terminal color only on a TTY, respects `NO_COLOR` and `TERM=dumb`, and retains status symbols and text without color. Human errors go to stderr; JSON errors remain on stdout. Help and version commands print plain text. Feedback and attachment metadata remain available in JSON; selecting a human view does not disable workspace feedback.
233
+
234
+ ```sh
235
+ rill whoami
236
+ rill whoami --json
237
+ rill --human doctor
238
+ rill --human record start --url https://example.com --title "Smoke test" --headed
239
+ rill --human record stop <recording-id>
240
+ rill --human record status <recording-id>
241
+ ```
225
242
 
226
243
  ```sh
227
244
  rill --pretty record status <recording-id>
@@ -357,7 +374,7 @@ Use `rill record list` to list accessible recordings and `rill inspect <recordin
357
374
 
358
375
  ### Check the connected account
359
376
 
360
- Run `rill whoami` (or `rill --pretty whoami`) to verify your current identity. JSON output includes status, account email when available, workspace, scopes, and access expiry. It uses RILL_TOKEN when set, otherwise the saved OS credential. Existing access can renew automatically; it never creates an identity or opens an approval flow. Missing credentials return `not_connected` and exit 1. Invalid or revoked credentials return an error.
377
+ Run `rill whoami` for a short human account summary, or `rill whoami --json` (also `rill --pretty whoami`) for structured identity fields. JSON includes status, account email when available, workspace, scopes, and access expiry. It uses RILL_TOKEN when set, otherwise the saved OS credential. Existing access can renew automatically; it never creates an identity or opens an approval flow. Missing credentials return `not_connected` in JSON and exit 1. Invalid or revoked credentials return an error.
361
378
 
362
379
  ### Capture time and upload progress (0.1.14)
363
380
 
package/dist/cli.js CHANGED
@@ -442,7 +442,7 @@ var ControlPlaneClient = class {
442
442
  return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
443
443
  }
444
444
  health() {
445
- return this.request("/api/health", {}, false);
445
+ return this.request("/api/health", { signal: AbortSignal.timeout(5e3) }, false);
446
446
  }
447
447
  agentSession() {
448
448
  return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
@@ -15244,7 +15244,7 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
15244
15244
  // src/cli.ts
15245
15245
  import { spawn as spawn8 } from "child_process";
15246
15246
  import { hostname as hostname4, tmpdir as tmpdir3 } from "os";
15247
- import { Command } from "commander";
15247
+ import { Command, CommanderError } from "commander";
15248
15248
  import { basename, dirname as dirname2, join as join11 } from "path";
15249
15249
  import { readdir as readdir2, rm as rm7, stat as stat5 } from "fs/promises";
15250
15250
 
@@ -16647,6 +16647,72 @@ function fail(error51) {
16647
16647
  process.exitCode = 1;
16648
16648
  }
16649
16649
 
16650
+ // src/human-output.ts
16651
+ function terminalColor(enabled = Boolean(process.stdout.isTTY) && !("NO_COLOR" in process.env) && process.env.TERM !== "dumb") {
16652
+ return (text, code) => enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
16653
+ }
16654
+ function formatIdentity(identity, color = terminalColor()) {
16655
+ if (identity.status === "not_connected") return `${color("! Not connected to Rill", 33)}
16656
+
16657
+ Sign in: ${cliCommand(identity.apiUrl, "login")}
16658
+ `;
16659
+ if (identity.status === "anonymous") return `${color("\u2713 Using Rill anonymously", 32)}
16660
+
16661
+ Connect your account: ${cliCommand(identity.apiUrl, "connect", "--email", "<your-email>")}
16662
+ `;
16663
+ return `${color("\u2713 Connected to Rill", 32)}
16664
+ Account: ${identity.email ?? "Email unavailable"}
16665
+ `;
16666
+ }
16667
+ function formatDoctor(result, color = terminalColor()) {
16668
+ const labels = { node: "Node.js", api: "API connectivity", cli_version: "CLI version", authentication: "Authentication", chrome: "Browser", ffmpeg: "FFmpeg", encoding: "Encoding", recorder_transport: "Recorder transport" };
16669
+ const symbols = { pass: "\u2713", fail: "\xD7", warn: "!", skip: "\u2013" };
16670
+ const colors = { pass: 32, fail: 31, warn: 33, skip: 90 };
16671
+ const lines = [color("Rill CLI Doctor", 34), `Execution environment: ${result.environment.platform}`, ""];
16672
+ for (const check2 of result.checks) {
16673
+ lines.push(color(`${symbols[check2.status]} ${labels[check2.name]}: ${check2.message}`, colors[check2.status]));
16674
+ if (check2.recovery) lines.push(` \u2192 ${check2.recovery}`);
16675
+ }
16676
+ const warnings = result.checks.filter((c) => c.status === "warn").length;
16677
+ lines.push(
16678
+ "",
16679
+ "Browser control: not verified. Your agent must attach to the returned CDP endpoint.",
16680
+ "",
16681
+ `${result.checks.filter((c) => c.status === "pass").length} passed \xB7 ${result.checks.filter((c) => c.status === "fail").length} failed \xB7 ${warnings} ${warnings === 1 ? "warning" : "warnings"}`
16682
+ );
16683
+ return lines.join("\n") + "\n";
16684
+ }
16685
+ function formatRecording(value, apiUrl, color = terminalColor()) {
16686
+ const artifact = value.result && typeof value.result === "object" ? value.result : {};
16687
+ const id = String(value.recordingId ?? value.id ?? "");
16688
+ const status = String(value.status ?? "unknown");
16689
+ const titles = { recording: "Recording started", ready: "Recording ready", captured: "Captured locally \xB7 upload required", uploading: "Uploading recording", queued: "Recording queued for encoding", encoding: "Encoding recording", processing: "Processing recording", finalizing: "Finalizing recording", failed: "Recording failed", cancelled: "Recording cancelled", expired: "Recording expired" };
16690
+ const lines = [color(`${["recording", "ready"].includes(status) ? "\u2713" : ["failed", "expired"].includes(status) ? "\xD7" : "\u2013"} ${titles[status] ?? `Recording status: ${status}`}`, ["failed", "expired"].includes(status) ? 31 : status === "ready" || status === "recording" ? 32 : 33)];
16691
+ if (id) lines.push(`ID: ${id}`);
16692
+ const duration3 = value.durationSeconds ?? value.duration_seconds ?? artifact.durationSeconds;
16693
+ if (typeof duration3 === "number" && Number.isFinite(duration3)) lines.push(`Duration: ${Math.floor(duration3 / 60)}m ${Math.floor(duration3 % 60)}s`);
16694
+ if (typeof value.remainingSeconds === "number") lines.push(`Time remaining: ${Math.max(0, Math.ceil(value.remainingSeconds))}s`);
16695
+ if (typeof value.cdpUrl === "string") lines.push(`Browser connection: ${value.cdpUrl}`);
16696
+ if (typeof value.tabId === "string") lines.push(`Recorded tab: ${value.tabId}`);
16697
+ if (typeof value.shareUrl === "string" && value.shareUrl) lines.push("", `Share: ${value.shareUrl}`);
16698
+ else if (status === "ready") lines.push("", "Ready. No share link returned; open this recording in your Rill account.");
16699
+ const message = value.message ?? value.error;
16700
+ if (typeof message === "string" && message) lines.push("", message);
16701
+ const next = typeof value.nextCommand === "string" ? value.nextCommand : status === "recording" && id ? cliCommand(apiUrl, "record", "stop", id) : void 0;
16702
+ if (next) lines.push("", status === "recording" ? "Perform your test, then stop and upload:" : "Next:", ` ${next}`);
16703
+ return lines.join("\n") + "\n";
16704
+ }
16705
+ function formatFailure(error51, color = terminalColor()) {
16706
+ const { error: failure } = failureValue(error51);
16707
+ let message = failure.message.startsWith(`${failure.code}: `) ? failure.message.slice(failure.code.length + 2) : failure.message;
16708
+ if (failure.recovery && message.endsWith(`: ${failure.recovery}`)) message = message.slice(0, -failure.recovery.length - 2);
16709
+ const lines = [color(`\xD7 ${message}`, 31)];
16710
+ if (typeof failure.details?.recordingId === "string") lines.push(`ID: ${failure.details.recordingId}`);
16711
+ if (failure.recovery) lines.push("", `Next: ${failure.recovery}`);
16712
+ else if (typeof failure.details?.nextCommand === "string") lines.push("", `Next: ${failure.details.nextCommand}`);
16713
+ return lines.join("\n") + "\n";
16714
+ }
16715
+
16650
16716
  // src/feedback-protocol.ts
16651
16717
  function suggestedFeedbackAction(recordingId) {
16652
16718
  return {
@@ -16741,9 +16807,11 @@ async function runDoctor(apiUrl, overrides = {}) {
16741
16807
  if (!health.ok || health.service !== "rill") throw new Error("The endpoint did not identify itself as Rill.");
16742
16808
  checks.push({ name: "api", status: "pass", message: `Rill API is reachable at ${apiUrl}.` });
16743
16809
  } catch (error51) {
16744
- checks.push({ name: "api", status: "fail", message: `Rill API is unreachable: ${errorMessage(error51)}`, recovery: `Check the network and API URL, then rerun \`rill --api-url ${apiUrl} doctor\`.` });
16810
+ checks.push({ name: "api", status: "fail", message: `Rill API is unreachable: ${errorMessage(error51)}`, recovery: `Check network access in this ${dependencies.platform} execution environment and the API URL, then rerun \`rill --api-url ${apiUrl} doctor\`. An agent sandbox may have different network access from your desktop.` });
16745
16811
  }
16746
- if (!health?.minimumCliVersion || !health.recommendedCliVersion) {
16812
+ if (!health) {
16813
+ checks.push({ name: "cli_version", status: "skip", message: `CLI ${dependencies.cliVersion} compatibility was not checked because the API is unreachable.`, version: dependencies.cliVersion });
16814
+ } else if (!health.minimumCliVersion || !health.recommendedCliVersion) {
16747
16815
  checks.push({ name: "cli_version", status: "fail", message: `CLI ${dependencies.cliVersion} could not be checked against the API compatibility policy.`, version: dependencies.cliVersion, recovery: "Deploy a Rill API that advertises minimumCliVersion and recommendedCliVersion." });
16748
16816
  } else if (compareVersions(dependencies.cliVersion, health.minimumCliVersion) < 0) {
16749
16817
  checks.push({ name: "cli_version", status: "fail", message: `CLI ${dependencies.cliVersion} is older than the minimum supported version ${health.minimumCliVersion}.`, version: dependencies.cliVersion, recovery: `Run \`npm install --global @runalabs/rill-cli@${health.recommendedCliVersion}\`.` });
@@ -16778,20 +16846,23 @@ async function runDoctor(apiUrl, overrides = {}) {
16778
16846
  }
16779
16847
  }
16780
16848
  }
16781
- checks.push(await executableCheck("chrome", dependencies.chromePath, ["--version"], dependencies.commandVersion, "Install Google Chrome or Chromium, or set RILL_CHROME_PATH to its executable."));
16849
+ checks.push(await executableCheck("chrome", dependencies.chromePath, ["--version"], dependencies.commandVersion, `Chrome must be available in this ${dependencies.platform} execution environment. Install Chrome/Chromium here or set RILL_CHROME_PATH. A browser on a separate desktop is not automatically accessible; use that desktop's terminal to record locally.`));
16782
16850
  if (dependencies.encoding === "local") {
16783
16851
  checks.push(await executableCheck("ffmpeg", dependencies.ffmpegPath, ["-version"], dependencies.commandVersion, dependencies.platform === "darwin" ? "Install ffmpeg with `brew install ffmpeg`, or set RILL_FFMPEG." : "Install ffmpeg using the system package manager, or set RILL_FFMPEG."));
16784
16852
  } else {
16785
16853
  checks.push({ name: "ffmpeg", status: "skip", message: "Remote encoding does not require local FFmpeg." });
16786
- try {
16787
- const token = await dependencies.loadCredential(apiUrl);
16788
- if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
16789
- checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
16790
- } catch (error51) {
16791
- checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "Run doctor --encoding local to check local FFmpeg, then record start --encoding local, or ask the workspace owner to enable remote encoding." });
16854
+ if (!health) checks.push({ name: "encoding", status: "skip", message: "Remote encoding was not checked because the API is unreachable." });
16855
+ else {
16856
+ try {
16857
+ const token = await dependencies.loadCredential(apiUrl);
16858
+ if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
16859
+ checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
16860
+ } catch (error51) {
16861
+ checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "Run doctor --encoding local to check local FFmpeg, then record start --encoding local, or ask the workspace owner to enable remote encoding." });
16862
+ }
16792
16863
  }
16793
16864
  }
16794
- return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
16865
+ return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, environment: { platform: dependencies.platform }, checks, ...runFeedbackFields(feedbackEnabled) };
16795
16866
  }
16796
16867
  async function executableCheck(name, path, args, run2, recovery) {
16797
16868
  try {
@@ -16974,13 +17045,23 @@ function watchEncoding(apiUrl, id) {
16974
17045
  child.unref();
16975
17046
  }
16976
17047
  var program = new Command();
16977
- program.name("rill").description("Record, inspect, and share agent browser runs.").version(CLI_VERSION).option("--api-url <url>", "Rill control plane", controlPlaneUrl(process.env.RILL_API_URL)).option("--pretty", "Pretty-print JSON output", false).option("--json", "Output cloud run results as JSON", false);
17048
+ program.name("rill").description("Record, inspect, and share agent browser runs.").version(CLI_VERSION).option("--api-url <url>", "Rill control plane", controlPlaneUrl(process.env.RILL_API_URL)).option("--pretty", "Pretty-print JSON output", false).option("--json", "Output machine-readable JSON (including whoami)", false).option("--human", "Readable doctor, login, whoami and recording start/stop/status/retry output", false);
17049
+ program.configureOutput({ outputError: () => void 0 }).exitOverride();
17050
+ function humanOutput(identity = false) {
17051
+ const options = program.opts();
17052
+ return !options.json && !options.pretty && (identity || options.human);
17053
+ }
17054
+ function emitRecording(value, pretty) {
17055
+ if (humanOutput()) process.stdout.write(formatRecording(value, program.opts().apiUrl));
17056
+ else emit(value, pretty);
17057
+ }
16978
17058
  var activeTelemetry = null;
16979
17059
  program.command("daemon", { hidden: true }).action(() => runDaemon());
16980
17060
  program.command("doctor").description("Validate Rill prerequisites before recording.").option("--encoding <mode>", "remote or local", "remote").action(async (commandOptions) => {
16981
17061
  const options = program.opts();
16982
17062
  const result = await runDoctor(options.apiUrl, { encoding: commandOptions.encoding });
16983
- emit(result, options.pretty);
17063
+ if (humanOutput()) process.stdout.write(formatDoctor(result));
17064
+ else emit(result, options.pretty);
16984
17065
  if (result.status === "error") process.exitCode = 1;
16985
17066
  });
16986
17067
  async function authorize(quiet = false) {
@@ -17001,15 +17082,19 @@ async function authorize(quiet = false) {
17001
17082
  if (result.status === "pending") continue;
17002
17083
  if (result.status !== "approved" || !result.token) throw new Error(`Device authorization ${result.status}.`);
17003
17084
  await saveCredential(options.apiUrl, result.token);
17004
- if (!quiet) emit({ schemaVersion: 1, status: "authenticated", expiresAt: result.expiresAt, apiUrl: options.apiUrl }, options.pretty);
17085
+ if (!quiet) {
17086
+ if (humanOutput()) process.stdout.write("\u2713 Signed in to Rill\nCheck your account: rill whoami\n");
17087
+ else emit({ schemaVersion: 1, status: "authenticated", expiresAt: result.expiresAt, apiUrl: options.apiUrl }, options.pretty);
17088
+ }
17005
17089
  return result.token;
17006
17090
  }
17007
17091
  throw new Error("Device authorization expired.");
17008
17092
  }
17009
- program.command("whoami").description("Show the current verified agent identity without starting sign-in.").action(async () => {
17093
+ program.command("whoami").description("Show the current verified identity without starting sign-in.").addHelpText("after", "\nShows a short account summary by default. Use rill whoami --json for agents, or rill --pretty whoami for indented JSON.").action(async () => {
17010
17094
  const options = program.opts();
17011
17095
  const identity = await whoami(options.apiUrl);
17012
- emit(identity, options.pretty);
17096
+ if (humanOutput(true)) process.stdout.write(formatIdentity(identity));
17097
+ else emit(identity, options.pretty);
17013
17098
  if (!identity.authenticated) process.exitCode = 1;
17014
17099
  });
17015
17100
  var connect = program.command("connect").description("Connect this agent once, with renewable account access.").option("--email <email>", "Email of the Rill account approving access").option("--no-wait", "Return approval instructions immediately; poll with connect status").action(async (input) => {
@@ -17101,7 +17186,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
17101
17186
  }).catch(() => void 0);
17102
17187
  return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, ...local.tabId ? { tabId: local.tabId } : {}, browserAttachment: browserAttachment(local.cdpUrl, local.tabId), startedAt: local.startedAt, stopsAt: local.stopsAt, elapsedSeconds: local.elapsedSeconds, remainingSeconds: local.remainingSeconds, bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
17103
17188
  });
17104
- emit(result, options.pretty);
17189
+ emitRecording(result, options.pretty);
17105
17190
  });
17106
17191
  record2.command("stop").argument("<recording-id>").option("--no-wait").option("--no-feedback", "Disable feedback only when the user explicitly requests it").action(async (recordingId, commandOptions) => {
17107
17192
  const options = program.opts();
@@ -17123,7 +17208,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
17123
17208
  await client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics);
17124
17209
  const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
17125
17210
  const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
17126
- emit({ schemaVersion: 1, ...result2, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
17211
+ emitRecording({ schemaVersion: 1, ...result2, recordingId, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
17127
17212
  if (result2.status === "ready") await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
17128
17213
  else watchEncoding(options.apiUrl, recordingId);
17129
17214
  return;
@@ -17145,7 +17230,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
17145
17230
  ...recordingNextStep(options.apiUrl, recordingId, remote ? "ready" : "processing"),
17146
17231
  ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
17147
17232
  };
17148
- emit(result, options.pretty);
17233
+ emitRecording(result, options.pretty);
17149
17234
  if (remote) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
17150
17235
  });
17151
17236
  record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
@@ -17166,7 +17251,7 @@ record2.command("retry").argument("<recording-id>").option("--no-wait").action(a
17166
17251
  if (!accepted) throw new Error("Encoding job is unavailable.");
17167
17252
  if (["expired", "cancelled"].includes(accepted.status)) throw new Error(`Recording ${accepted.status}.`);
17168
17253
  const result = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
17169
- emit({ schemaVersion: 1, ...result, ...recordingNextStep(options.apiUrl, recordingId, result.status, { resumableFrames: true }) }, options.pretty);
17254
+ emitRecording({ schemaVersion: 1, ...result, recordingId, ...recordingNextStep(options.apiUrl, recordingId, result.status, { resumableFrames: true }) }, options.pretty);
17170
17255
  if (result.status === "ready") await removeLocalCapture(recordingId);
17171
17256
  else watchEncoding(options.apiUrl, recordingId);
17172
17257
  });
@@ -17208,7 +17293,7 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
17208
17293
  try {
17209
17294
  const encoding = await client.encodingStatus(recordingId);
17210
17295
  const remote = encoding.status === "ready" ? await client.recordingStatus(recordingId) : {};
17211
- emit({ schemaVersion: 1, ...encoding, ...remote, recordingId, source: "remote", ...recordingNextStep(options.apiUrl, recordingId, encoding.status, { resumableFrames: true }) }, options.pretty);
17296
+ emitRecording({ schemaVersion: 1, ...encoding, ...remote, recordingId, source: "remote", ...recordingNextStep(options.apiUrl, recordingId, encoding.status, { resumableFrames: true }) }, options.pretty);
17212
17297
  if (encoding.status === "ready") await removeLocalCapture(recordingId);
17213
17298
  return;
17214
17299
  } catch (error51) {
@@ -17221,7 +17306,7 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
17221
17306
  },
17222
17307
  remoteStatus: (id) => client.recordingStatus(id)
17223
17308
  }, options.apiUrl);
17224
- emit({ schemaVersion: 1, ...status }, options.pretty);
17309
+ emitRecording({ schemaVersion: 1, ...status, recordingId }, options.pretty);
17225
17310
  });
17226
17311
  record2.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
17227
17312
  const options = program.opts();
@@ -17301,8 +17386,20 @@ program.hook("postAction", async () => {
17301
17386
  await finishCliTelemetry(process.exitCode ? new Error("command_reported_failure: Command returned a failure status.") : null);
17302
17387
  });
17303
17388
  program.parseAsync().catch(async (error51) => {
17389
+ if (error51 instanceof CommanderError && error51.exitCode === 0) return;
17390
+ if (error51 instanceof CommanderError) {
17391
+ const action = program.args[0] === "record" ? program.args[1] : void 0;
17392
+ const missingRecording = error51.code === "commander.missingArgument" && ["stop", "status", "retry", "cancel"].includes(action ?? "");
17393
+ error51 = Object.assign(new Error(`validation_failed: ${missingRecording ? "A recording ID is required." : error51.message.replace(/^error: /, "")}`), {
17394
+ code: "validation_failed",
17395
+ recovery: missingRecording ? `Run rill record ${action} <recording-id>. Use rill record list to find the ID.` : "Run rill --help for available commands and options."
17396
+ });
17397
+ }
17304
17398
  await finishCliTelemetry(error51);
17305
- fail(error51);
17399
+ if (humanOutput(program.args[0] === "whoami")) {
17400
+ process.stderr.write(formatFailure(error51));
17401
+ process.exitCode = 1;
17402
+ } else fail(error51);
17306
17403
  });
17307
17404
  function commandPath(actionCommand) {
17308
17405
  const path = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runalabs/rill-cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Record, inspect, and share agent-controlled browser runs with Rill.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {