@runalabs/rill-cli 0.1.15 → 0.1.17

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 +22 -3
  2. package/dist/cli.js +150 -38
  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>
@@ -353,11 +370,13 @@ Progress runs through `uploading` → `queued` → `encoding` → `processing`
353
370
 
354
371
  Run `rill connect --email you@example.com` to authorize account access or keep your anonymous recordings. Open the displayed link, sign in, and enter the code yourself. Approval allows creation and reading of your existing and new recordings for 90 days, with automatic renewal of one-hour access tokens. Revoke the connection in Settings → Agent access to stop access and renewal. Deletion and changes to existing sharing settings are not included.
355
372
 
373
+ Browser approval does not finish the handshake: the CLI must collect and save credentials. With `--no-wait`, keep polling the returned `connect status <authorizationId>` command until `authenticated`. Starting with 0.1.17, the CLI checks the server even after its local approval deadline. Approval codes expire after ten minutes; successful approval starts a separate 30-minute collection window, with a 60-second lost-response retry window after first collection. If collection expires, run `connect --email` again with the same account to request fresh human approval while preserving the registration and recordings. Do not delete saved credentials to recover. Auth requests include the CLI version for consent-aware diagnostics.
374
+
356
375
  Use `rill record list` to list accessible recordings and `rill inspect <recording-id>` for private recording details. `rill inspect <share-url>` continues to work without account access. The existing `rill login` and `RILL_TOKEN` flows remain supported. Local credential storage requires macOS Keychain, Linux Secret Service, or Windows current-user DPAPI.
357
376
 
358
377
  ### Check the connected account
359
378
 
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.
379
+ 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
380
 
362
381
  ### Capture time and upload progress (0.1.14)
363
382
 
package/dist/cli.js CHANGED
@@ -127,6 +127,11 @@ function browserAttachment(cdpUrl, tabId) {
127
127
  };
128
128
  }
129
129
 
130
+ // src/version.ts
131
+ import { createRequire } from "module";
132
+ var packageJson = createRequire(import.meta.url)("../package.json");
133
+ var CLI_VERSION = packageJson.version;
134
+
130
135
  // src/agent-session.ts
131
136
  var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
132
137
  var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
@@ -150,7 +155,7 @@ async function request(apiUrl, path, body, form = false) {
150
155
  method: "POST",
151
156
  redirect: "error",
152
157
  signal: AbortSignal.timeout(15e3),
153
- headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
158
+ headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json", "X-Rill-CLI-Version": CLI_VERSION },
154
159
  body: form ? new URLSearchParams(Object.entries(body).map(([key2, value2]) => [key2, String(value2)])) : JSON.stringify(body)
155
160
  });
156
161
  const value = await response.json();
@@ -246,6 +251,12 @@ async function connectAgent(apiUrl, email3, options = {}) {
246
251
  async function beginConnection(apiUrl, email3) {
247
252
  if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
248
253
  let saved = parseAuthorization(await loadStoredCredential(apiUrl));
254
+ if (saved?.connection?.status === "pending" && saved.connection.expiresAt <= Date.now()) {
255
+ if (saved.connection.email !== email3) throw new Error("Use the original account to finish or recover this connection.");
256
+ const result = await pollSavedConnection(apiUrl, saved.connection.id);
257
+ if (result.status !== "expired") return result;
258
+ saved = parseAuthorization(await loadStoredCredential(apiUrl));
259
+ }
249
260
  if (saved?.connection?.status === "pending" && saved.connection.expiresAt > Date.now()) {
250
261
  if (saved.connection.email !== email3) throw new Error("A connection for another email is pending. Let it expire before starting a different connection.");
251
262
  process.stderr.write(`Open ${saved.connection.verificationUri}
@@ -259,6 +270,22 @@ Sign in as ${email3} and enter ${saved.connection.userCode}. Approve only the re
259
270
  const result = await request(apiUrl, "/agent/identity/claim", { claim_token: saved.claimToken, email: email3 });
260
271
  claim = result.claim_attempt;
261
272
  } catch (error51) {
273
+ if (error51 instanceof AgentAuthError && error51.code === "claim_already_approved") {
274
+ saved.connection ??= {
275
+ id: randomBytes(16).toString("hex"),
276
+ email: email3,
277
+ verificationUri: "",
278
+ userCode: "",
279
+ expiresAt: Date.now(),
280
+ interval: 5,
281
+ nextPollAt: 0,
282
+ status: "pending"
283
+ };
284
+ saved.connection.status = "pending";
285
+ saved.connection.nextPollAt = 0;
286
+ await save(apiUrl, saved);
287
+ return pollSavedConnection(apiUrl, saved.connection.id);
288
+ }
262
289
  if (!(error51 instanceof AgentAuthError) || error51.code !== "claim_expired") throw error51;
263
290
  }
264
291
  }
@@ -309,11 +336,6 @@ async function pollSavedConnection(apiUrl, authorizationId) {
309
336
  if (!saved?.connection || saved.connection.id !== authorizationId) throw new AgentAuthError("authorization_not_found", "No matching saved connection for this API URL. Use the latest connect response.");
310
337
  const connection = saved.connection;
311
338
  if (connection.status !== "pending") return connectionView(apiUrl, saved);
312
- if (Date.now() >= connection.expiresAt) {
313
- connection.status = "expired";
314
- await save(apiUrl, saved);
315
- return connectionView(apiUrl, saved);
316
- }
317
339
  if (Date.now() < connection.nextPollAt) return connectionView(apiUrl, saved);
318
340
  connection.nextPollAt = Date.now() + connection.interval * 1e3;
319
341
  await save(apiUrl, saved);
@@ -322,7 +344,7 @@ async function pollSavedConnection(apiUrl, authorizationId) {
322
344
  saved = withTokens({ version: 1, anonymous: false, connection: { ...connection, status: "authenticated", userCode: "" } }, tokens);
323
345
  } catch (error51) {
324
346
  if (!(error51 instanceof AgentAuthError)) throw error51;
325
- if (error51.code === "expired_token" || error51.code === "claim_expired") connection.status = "expired";
347
+ if (error51.code === "expired_token" || error51.code === "claim_expired" || error51.code === "claim_delivery_expired" || error51.code === "claimed_or_in_flight" && Date.now() >= connection.expiresAt) connection.status = "expired";
326
348
  else if (error51.code === "access_denied" || error51.code === "claim_revoked") connection.status = "denied";
327
349
  else if (error51.code === "slow_down") {
328
350
  connection.interval += 5;
@@ -442,7 +464,7 @@ var ControlPlaneClient = class {
442
464
  return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
443
465
  }
444
466
  health() {
445
- return this.request("/api/health", {}, false);
467
+ return this.request("/api/health", { signal: AbortSignal.timeout(5e3) }, false);
446
468
  }
447
469
  agentSession() {
448
470
  return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
@@ -15244,7 +15266,7 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
15244
15266
  // src/cli.ts
15245
15267
  import { spawn as spawn8 } from "child_process";
15246
15268
  import { hostname as hostname4, tmpdir as tmpdir3 } from "os";
15247
- import { Command } from "commander";
15269
+ import { Command, CommanderError } from "commander";
15248
15270
  import { basename, dirname as dirname2, join as join11 } from "path";
15249
15271
  import { readdir as readdir2, rm as rm7, stat as stat5 } from "fs/promises";
15250
15272
 
@@ -16647,6 +16669,72 @@ function fail(error51) {
16647
16669
  process.exitCode = 1;
16648
16670
  }
16649
16671
 
16672
+ // src/human-output.ts
16673
+ function terminalColor(enabled = Boolean(process.stdout.isTTY) && !("NO_COLOR" in process.env) && process.env.TERM !== "dumb") {
16674
+ return (text, code) => enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
16675
+ }
16676
+ function formatIdentity(identity, color = terminalColor()) {
16677
+ if (identity.status === "not_connected") return `${color("! Not connected to Rill", 33)}
16678
+
16679
+ Sign in: ${cliCommand(identity.apiUrl, "login")}
16680
+ `;
16681
+ if (identity.status === "anonymous") return `${color("\u2713 Using Rill anonymously", 32)}
16682
+
16683
+ Connect your account: ${cliCommand(identity.apiUrl, "connect", "--email", "<your-email>")}
16684
+ `;
16685
+ return `${color("\u2713 Connected to Rill", 32)}
16686
+ Account: ${identity.email ?? "Email unavailable"}
16687
+ `;
16688
+ }
16689
+ function formatDoctor(result, color = terminalColor()) {
16690
+ const labels = { node: "Node.js", api: "API connectivity", cli_version: "CLI version", authentication: "Authentication", chrome: "Browser", ffmpeg: "FFmpeg", encoding: "Encoding", recorder_transport: "Recorder transport" };
16691
+ const symbols = { pass: "\u2713", fail: "\xD7", warn: "!", skip: "\u2013" };
16692
+ const colors = { pass: 32, fail: 31, warn: 33, skip: 90 };
16693
+ const lines = [color("Rill CLI Doctor", 34), `Execution environment: ${result.environment.platform}`, ""];
16694
+ for (const check2 of result.checks) {
16695
+ lines.push(color(`${symbols[check2.status]} ${labels[check2.name]}: ${check2.message}`, colors[check2.status]));
16696
+ if (check2.recovery) lines.push(` \u2192 ${check2.recovery}`);
16697
+ }
16698
+ const warnings = result.checks.filter((c) => c.status === "warn").length;
16699
+ lines.push(
16700
+ "",
16701
+ "Browser control: not verified. Your agent must attach to the returned CDP endpoint.",
16702
+ "",
16703
+ `${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"}`
16704
+ );
16705
+ return lines.join("\n") + "\n";
16706
+ }
16707
+ function formatRecording(value, apiUrl, color = terminalColor()) {
16708
+ const artifact = value.result && typeof value.result === "object" ? value.result : {};
16709
+ const id = String(value.recordingId ?? value.id ?? "");
16710
+ const status = String(value.status ?? "unknown");
16711
+ 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" };
16712
+ 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)];
16713
+ if (id) lines.push(`ID: ${id}`);
16714
+ const duration3 = value.durationSeconds ?? value.duration_seconds ?? artifact.durationSeconds;
16715
+ if (typeof duration3 === "number" && Number.isFinite(duration3)) lines.push(`Duration: ${Math.floor(duration3 / 60)}m ${Math.floor(duration3 % 60)}s`);
16716
+ if (typeof value.remainingSeconds === "number") lines.push(`Time remaining: ${Math.max(0, Math.ceil(value.remainingSeconds))}s`);
16717
+ if (typeof value.cdpUrl === "string") lines.push(`Browser connection: ${value.cdpUrl}`);
16718
+ if (typeof value.tabId === "string") lines.push(`Recorded tab: ${value.tabId}`);
16719
+ if (typeof value.shareUrl === "string" && value.shareUrl) lines.push("", `Share: ${value.shareUrl}`);
16720
+ else if (status === "ready") lines.push("", "Ready. No share link returned; open this recording in your Rill account.");
16721
+ const message = value.message ?? value.error;
16722
+ if (typeof message === "string" && message) lines.push("", message);
16723
+ const next = typeof value.nextCommand === "string" ? value.nextCommand : status === "recording" && id ? cliCommand(apiUrl, "record", "stop", id) : void 0;
16724
+ if (next) lines.push("", status === "recording" ? "Perform your test, then stop and upload:" : "Next:", ` ${next}`);
16725
+ return lines.join("\n") + "\n";
16726
+ }
16727
+ function formatFailure(error51, color = terminalColor()) {
16728
+ const { error: failure } = failureValue(error51);
16729
+ let message = failure.message.startsWith(`${failure.code}: `) ? failure.message.slice(failure.code.length + 2) : failure.message;
16730
+ if (failure.recovery && message.endsWith(`: ${failure.recovery}`)) message = message.slice(0, -failure.recovery.length - 2);
16731
+ const lines = [color(`\xD7 ${message}`, 31)];
16732
+ if (typeof failure.details?.recordingId === "string") lines.push(`ID: ${failure.details.recordingId}`);
16733
+ if (failure.recovery) lines.push("", `Next: ${failure.recovery}`);
16734
+ else if (typeof failure.details?.nextCommand === "string") lines.push("", `Next: ${failure.details.nextCommand}`);
16735
+ return lines.join("\n") + "\n";
16736
+ }
16737
+
16650
16738
  // src/feedback-protocol.ts
16651
16739
  function suggestedFeedbackAction(recordingId) {
16652
16740
  return {
@@ -16705,13 +16793,6 @@ async function withRunFeedback(session, start) {
16705
16793
 
16706
16794
  // src/doctor.ts
16707
16795
  import { spawn as spawn7 } from "child_process";
16708
-
16709
- // src/version.ts
16710
- import { createRequire } from "module";
16711
- var packageJson = createRequire(import.meta.url)("../package.json");
16712
- var CLI_VERSION = packageJson.version;
16713
-
16714
- // src/doctor.ts
16715
16796
  var defaultDependencies = {
16716
16797
  platform: process.platform,
16717
16798
  encoding: "remote",
@@ -16741,9 +16822,11 @@ async function runDoctor(apiUrl, overrides = {}) {
16741
16822
  if (!health.ok || health.service !== "rill") throw new Error("The endpoint did not identify itself as Rill.");
16742
16823
  checks.push({ name: "api", status: "pass", message: `Rill API is reachable at ${apiUrl}.` });
16743
16824
  } 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\`.` });
16825
+ 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
16826
  }
16746
- if (!health?.minimumCliVersion || !health.recommendedCliVersion) {
16827
+ if (!health) {
16828
+ checks.push({ name: "cli_version", status: "skip", message: `CLI ${dependencies.cliVersion} compatibility was not checked because the API is unreachable.`, version: dependencies.cliVersion });
16829
+ } else if (!health.minimumCliVersion || !health.recommendedCliVersion) {
16747
16830
  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
16831
  } else if (compareVersions(dependencies.cliVersion, health.minimumCliVersion) < 0) {
16749
16832
  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 +16861,23 @@ async function runDoctor(apiUrl, overrides = {}) {
16778
16861
  }
16779
16862
  }
16780
16863
  }
16781
- checks.push(await executableCheck("chrome", dependencies.chromePath, ["--version"], dependencies.commandVersion, "Install Google Chrome or Chromium, or set RILL_CHROME_PATH to its executable."));
16864
+ 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
16865
  if (dependencies.encoding === "local") {
16783
16866
  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
16867
  } else {
16785
16868
  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." });
16869
+ if (!health) checks.push({ name: "encoding", status: "skip", message: "Remote encoding was not checked because the API is unreachable." });
16870
+ else {
16871
+ try {
16872
+ const token = await dependencies.loadCredential(apiUrl);
16873
+ if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
16874
+ checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
16875
+ } catch (error51) {
16876
+ 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." });
16877
+ }
16792
16878
  }
16793
16879
  }
16794
- return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
16880
+ return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, environment: { platform: dependencies.platform }, checks, ...runFeedbackFields(feedbackEnabled) };
16795
16881
  }
16796
16882
  async function executableCheck(name, path, args, run2, recovery) {
16797
16883
  try {
@@ -16974,13 +17060,23 @@ function watchEncoding(apiUrl, id) {
16974
17060
  child.unref();
16975
17061
  }
16976
17062
  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);
17063
+ 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);
17064
+ program.configureOutput({ outputError: () => void 0 }).exitOverride();
17065
+ function humanOutput(identity = false) {
17066
+ const options = program.opts();
17067
+ return !options.json && !options.pretty && (identity || options.human);
17068
+ }
17069
+ function emitRecording(value, pretty) {
17070
+ if (humanOutput()) process.stdout.write(formatRecording(value, program.opts().apiUrl));
17071
+ else emit(value, pretty);
17072
+ }
16978
17073
  var activeTelemetry = null;
16979
17074
  program.command("daemon", { hidden: true }).action(() => runDaemon());
16980
17075
  program.command("doctor").description("Validate Rill prerequisites before recording.").option("--encoding <mode>", "remote or local", "remote").action(async (commandOptions) => {
16981
17076
  const options = program.opts();
16982
17077
  const result = await runDoctor(options.apiUrl, { encoding: commandOptions.encoding });
16983
- emit(result, options.pretty);
17078
+ if (humanOutput()) process.stdout.write(formatDoctor(result));
17079
+ else emit(result, options.pretty);
16984
17080
  if (result.status === "error") process.exitCode = 1;
16985
17081
  });
16986
17082
  async function authorize(quiet = false) {
@@ -17001,15 +17097,19 @@ async function authorize(quiet = false) {
17001
17097
  if (result.status === "pending") continue;
17002
17098
  if (result.status !== "approved" || !result.token) throw new Error(`Device authorization ${result.status}.`);
17003
17099
  await saveCredential(options.apiUrl, result.token);
17004
- if (!quiet) emit({ schemaVersion: 1, status: "authenticated", expiresAt: result.expiresAt, apiUrl: options.apiUrl }, options.pretty);
17100
+ if (!quiet) {
17101
+ if (humanOutput()) process.stdout.write("\u2713 Signed in to Rill\nCheck your account: rill whoami\n");
17102
+ else emit({ schemaVersion: 1, status: "authenticated", expiresAt: result.expiresAt, apiUrl: options.apiUrl }, options.pretty);
17103
+ }
17005
17104
  return result.token;
17006
17105
  }
17007
17106
  throw new Error("Device authorization expired.");
17008
17107
  }
17009
- program.command("whoami").description("Show the current verified agent identity without starting sign-in.").action(async () => {
17108
+ 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
17109
  const options = program.opts();
17011
17110
  const identity = await whoami(options.apiUrl);
17012
- emit(identity, options.pretty);
17111
+ if (humanOutput(true)) process.stdout.write(formatIdentity(identity));
17112
+ else emit(identity, options.pretty);
17013
17113
  if (!identity.authenticated) process.exitCode = 1;
17014
17114
  });
17015
17115
  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 +17201,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
17101
17201
  }).catch(() => void 0);
17102
17202
  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
17203
  });
17104
- emit(result, options.pretty);
17204
+ emitRecording(result, options.pretty);
17105
17205
  });
17106
17206
  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
17207
  const options = program.opts();
@@ -17123,7 +17223,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
17123
17223
  await client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics);
17124
17224
  const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
17125
17225
  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);
17226
+ emitRecording({ schemaVersion: 1, ...result2, recordingId, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
17127
17227
  if (result2.status === "ready") await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
17128
17228
  else watchEncoding(options.apiUrl, recordingId);
17129
17229
  return;
@@ -17145,7 +17245,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
17145
17245
  ...recordingNextStep(options.apiUrl, recordingId, remote ? "ready" : "processing"),
17146
17246
  ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
17147
17247
  };
17148
- emit(result, options.pretty);
17248
+ emitRecording(result, options.pretty);
17149
17249
  if (remote) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
17150
17250
  });
17151
17251
  record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
@@ -17166,7 +17266,7 @@ record2.command("retry").argument("<recording-id>").option("--no-wait").action(a
17166
17266
  if (!accepted) throw new Error("Encoding job is unavailable.");
17167
17267
  if (["expired", "cancelled"].includes(accepted.status)) throw new Error(`Recording ${accepted.status}.`);
17168
17268
  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);
17269
+ emitRecording({ schemaVersion: 1, ...result, recordingId, ...recordingNextStep(options.apiUrl, recordingId, result.status, { resumableFrames: true }) }, options.pretty);
17170
17270
  if (result.status === "ready") await removeLocalCapture(recordingId);
17171
17271
  else watchEncoding(options.apiUrl, recordingId);
17172
17272
  });
@@ -17208,7 +17308,7 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
17208
17308
  try {
17209
17309
  const encoding = await client.encodingStatus(recordingId);
17210
17310
  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);
17311
+ emitRecording({ schemaVersion: 1, ...encoding, ...remote, recordingId, source: "remote", ...recordingNextStep(options.apiUrl, recordingId, encoding.status, { resumableFrames: true }) }, options.pretty);
17212
17312
  if (encoding.status === "ready") await removeLocalCapture(recordingId);
17213
17313
  return;
17214
17314
  } catch (error51) {
@@ -17221,7 +17321,7 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
17221
17321
  },
17222
17322
  remoteStatus: (id) => client.recordingStatus(id)
17223
17323
  }, options.apiUrl);
17224
- emit({ schemaVersion: 1, ...status }, options.pretty);
17324
+ emitRecording({ schemaVersion: 1, ...status, recordingId }, options.pretty);
17225
17325
  });
17226
17326
  record2.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
17227
17327
  const options = program.opts();
@@ -17301,8 +17401,20 @@ program.hook("postAction", async () => {
17301
17401
  await finishCliTelemetry(process.exitCode ? new Error("command_reported_failure: Command returned a failure status.") : null);
17302
17402
  });
17303
17403
  program.parseAsync().catch(async (error51) => {
17404
+ if (error51 instanceof CommanderError && error51.exitCode === 0) return;
17405
+ if (error51 instanceof CommanderError) {
17406
+ const action = program.args[0] === "record" ? program.args[1] : void 0;
17407
+ const missingRecording = error51.code === "commander.missingArgument" && ["stop", "status", "retry", "cancel"].includes(action ?? "");
17408
+ error51 = Object.assign(new Error(`validation_failed: ${missingRecording ? "A recording ID is required." : error51.message.replace(/^error: /, "")}`), {
17409
+ code: "validation_failed",
17410
+ recovery: missingRecording ? `Run rill record ${action} <recording-id>. Use rill record list to find the ID.` : "Run rill --help for available commands and options."
17411
+ });
17412
+ }
17304
17413
  await finishCliTelemetry(error51);
17305
- fail(error51);
17414
+ if (humanOutput(program.args[0] === "whoami")) {
17415
+ process.stderr.write(formatFailure(error51));
17416
+ process.exitCode = 1;
17417
+ } else fail(error51);
17306
17418
  });
17307
17419
  function commandPath(actionCommand) {
17308
17420
  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.17",
4
4
  "description": "Record, inspect, and share agent-controlled browser runs with Rill.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {