@runalabs/rill-cli 0.1.10 → 0.1.11

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 +26 -7
  2. package/dist/cli.js +235 -69
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -148,7 +148,8 @@ Use `rill --help` or `<command> --help` for supported flags.
148
148
  | `rill doctor` | Check prerequisites, API compatibility, and authentication. |
149
149
  | `rill record start [options]` | Start a managed or attached browser recording. |
150
150
  | `rill record stop <recording-id>` | Finalize and upload; supports `--no-wait` and `--no-feedback`. |
151
- | `rill record status <recording-id>` | Read local state, falling back to remote state if the local session is unavailable. |
151
+ | `rill record status <recording-id>` | Read the remote encoding job first; otherwise check local capture state with a remote fallback. |
152
+ | `rill record retry <recording-id>` | Resume a retained remote capture or retry a failed encode; supports `--no-wait`. |
152
153
  | `rill record cancel <recording-id>` | Abandon a run created by this credential and release its remote quota reservation. |
153
154
  | `rill upload <video-file>` | Upload video-only evidence; supports `--title` and `--no-wait`. |
154
155
  | `rill inspect <share-url>` | Read bounded context without login. |
@@ -185,14 +186,15 @@ Check the exit status first, then structured fields. Known codes include `authen
185
186
 
186
187
  - **Doctor fails:** read the failed check's recovery field. Correct the credential, API connectivity, or executable path before recording.
187
188
  - **Quota exceeded:** inspect `error.details.quota.type` and `blockingRecordingId` when present. Check the blocking recording; cancel only an abandoned run. Review storage and daily allowance in Billing.
188
- - **Upload interrupted or processing timed out:** keep the recording ID and local artifacts. Check the dashboard for remote readiness. `record status` prefers local state and can report local `ready` without a share URL; this does not prove remote playback readiness.
189
+ - **Remote upload interrupted or encoding failed:** keep the recording ID and local capture files. Run `rill record status <recording-id>`, then `rill record retry <recording-id>` with the original credential before capture expiry. Retry resumes accepted chunks or requeues the encode without repeating browser actions. A `ready` result needs a share URL before you return it as evidence.
189
190
  - **Duration limit reached:** local capture finalizes automatically, but `record stop` is still needed to upload.
190
- - **Local session missing:** `record status` falls back to remote state. `drafts list` locates local directories, which may be incomplete. There is no `drafts resume` command.
191
+ - **Terminal closed:** after upload acceptance, the server finishes independently; retrieve the result with `record status`. Before acceptance, retry from the original machine while its captured frames remain available.
192
+ - **Local session missing:** `record status` checks for a remote encoding job before falling back to local or remote recording state. `drafts list` locates local directories, which may be incomplete. There is no `drafts resume` command.
191
193
  - **Share cannot be inspected:** open the complete URL in a browser; ask the owner for a current link if needed.
192
194
 
193
195
  A failed local startup automatically attempts to cancel the remote reservation it just created. `record cancel` changes remote state and does not stop the local browser; finish local capture before abandoning the remote reservation.
194
196
 
195
- When finalized artifacts survive, retain `recording.webm` and `diagnostics-v1.ndjson.gz` in the recording's draft directory. `rill upload <path-to-recording.webm>` is a video-only fallback: it creates a new recording and does not restore the original diagnostics. Successful stop with readiness removes that run's local artifact directory.
197
+ For `--encoding local`, when finalized artifacts survive, retain `recording.webm` and `diagnostics-v1.ndjson.gz` in the recording's draft directory. `rill upload <path-to-recording.webm>` is a video-only fallback: it creates a new recording and does not restore the original diagnostics. Successful stop with readiness removes that run's local artifact directory.
196
198
 
197
199
  `drafts purge` deletes local draft directories older than 24 hours by modification time, without a confirmation prompt. Recover needed evidence before purging.
198
200
 
@@ -203,7 +205,7 @@ When finalized artifacts survive, retain `recording.webm` and `diagnostics-v1.nd
203
205
  | `RILL_TOKEN` | Agent credential; overrides the keychain. |
204
206
  | `RILL_API_URL` | API origin; defaults to `https://userill.dev`. Global `--api-url` takes precedence. |
205
207
  | `RILL_CHROME_PATH` | Chrome or Chromium executable path. |
206
- | `RILL_FFMPEG` | ffmpeg executable; defaults to `ffmpeg` on `PATH`. |
208
+ | `RILL_FFMPEG` | FFmpeg executable for `--encoding local`; defaults to `ffmpeg` on `PATH`. |
207
209
  | `RILL_HOME` | Recorder state directory; defaults to `~/.rill`. |
208
210
  | `RILL_SOCKET` | Recorder socket; defaults to `recorder.sock` inside `RILL_HOME`. |
209
211
  | `RILL_TEMP` | Artifact root; defaults to `rill-recordings` inside the system temporary directory. |
@@ -264,8 +266,25 @@ Use `--json` for a machine-readable result (`--pretty` also enables JSON formatt
264
266
  Provider token usage is retained internally and omitted from customer output.
265
267
  `rill run status <run-id>` and `rill run cancel <run-id>` support the same output options.
266
268
 
267
- ### Remote encoding
269
+ ## Remote encoding
268
270
 
269
271
  Since 0.1.10, `record start` uses remote encoding by default. Chrome stays local, including access to localhost; FFmpeg is required only for `record start --encoding local`. For local encoding, install FFmpeg with `brew install ffmpeg` on macOS or `sudo apt install ffmpeg` on Debian/Ubuntu, then run `doctor --encoding local`. Remote encoding adds no charge and consumes no cloud-run credits; existing recording allowances still apply. Captures are limited to ten minutes and 1 GiB of raw frames, with three pending jobs per workspace. Busy recordings can reach the byte limit sooner.
270
272
 
271
- `record stop` uploads frames and waits for playback. `record stop ID --no-wait` returns after upload acceptance; processing continues if the terminal closes. Use `record status ID` to retrieve progress and the share link, or `record retry ID` to resume an interrupted upload or retry encoding without repeating browser actions. Failed raw captures expire after 24 hours. Remote encoding uses no cloud-run credits and adds no charge.
273
+ `record stop` uploads frames and waits for playback. `record stop ID --no-wait` returns after upload acceptance; processing continues if the terminal closes. Use `record status ID` to retrieve progress and the share link, or `record retry ID` to resume an interrupted upload or retry encoding without repeating browser actions. Raw captures are removed after successful publication. Recoverable captures expire after 24 hours: local retention starts at capture start, server retention at upload initialization. Keep the original credential and local files until upload acceptance; `expiresAt` reports the server deadline once a job exists.
274
+
275
+ ```sh
276
+ rill record stop <recording-id> --no-wait
277
+ rill record status <recording-id>
278
+ # If upload or encoding failed, retry the same recording:
279
+ rill record retry <recording-id>
280
+ ```
281
+
282
+ Progress runs through `uploading` → `queued` → `encoding` → `processing` → `ready`. Waiting for capacity is normal; do not repeat the browser journey merely because a job is queued. Long or busy recordings can take several minutes to upload and encode.
283
+
284
+ ### Anonymous local recordings and account connection
285
+
286
+ `rill record start` can begin without sign-in when the server supports anonymous access. The CLI stores the identity in the OS credential store and uses local encoding. Anonymous access includes three successful recordings, at most five minutes and 250 MB each; each recording and its unlisted share link expires seven days after creation. Failed uploads do not consume the successful-recording allowance. Source and service rate limits also apply.
287
+
288
+ 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 Rill's Credentials page to stop access and renewal. Deletion and changes to existing sharing settings are not included.
289
+
290
+ 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 or Linux Secret Service.
package/dist/cli.js CHANGED
@@ -14673,6 +14673,9 @@ var ControlPlaneClient = class {
14673
14673
  const body = await readFile(filePath);
14674
14674
  return this.request(`/api/recordings/${recordingId}/diagnostics`, { method: "PUT", headers: { "Content-Encoding": "gzip", "Content-Type": "application/x-ndjson", "Content-Length": String(body.length), "X-Rill-Summary": JSON.stringify(summary) }, body });
14675
14675
  }
14676
+ listRecordings() {
14677
+ return this.request("/api/recordings");
14678
+ }
14676
14679
  createDeviceAuthorization(input) {
14677
14680
  return this.request("/api/device/authorizations", { method: "POST", body: JSON.stringify(input) }, false);
14678
14681
  }
@@ -14680,6 +14683,7 @@ var ControlPlaneClient = class {
14680
14683
  return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
14681
14684
  }
14682
14685
  inspect(shareUrl) {
14686
+ if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
14683
14687
  const url2 = new URL(shareUrl);
14684
14688
  const slug = url2.pathname.split("/").filter(Boolean).at(-1);
14685
14689
  if (!slug) throw new Error("The share URL does not contain a slug.");
@@ -14692,8 +14696,9 @@ var ControlPlaneClient = class {
14692
14696
  const headers = new Headers(init.headers);
14693
14697
  if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
14694
14698
  if (authenticated) {
14695
- if (!this.token) throw new Error("authentication_required: Run `rill login` or set RILL_TOKEN.");
14696
- headers.set("Authorization", `Bearer ${this.token}`);
14699
+ const token = typeof this.token === "function" ? await this.token() : this.token;
14700
+ if (!token) throw new Error("authentication_required: Run `rill login` or set RILL_TOKEN.");
14701
+ headers.set("Authorization", `Bearer ${token}`);
14697
14702
  }
14698
14703
  const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
14699
14704
  const value = await response.json().catch(() => null);
@@ -14766,10 +14771,10 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
14766
14771
 
14767
14772
  // src/cli.ts
14768
14773
  import { spawn as spawn6 } from "child_process";
14774
+ import { hostname as hostname4 } from "os";
14769
14775
  import { Command } from "commander";
14770
- import { hostname as hostname3 } from "os";
14771
14776
  import { basename, dirname as dirname2 } from "path";
14772
- import { readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
14777
+ import { readdir as readdir2, rm as rm5, stat as stat4 } from "fs/promises";
14773
14778
 
14774
14779
  // ../recorder/src/client.ts
14775
14780
  import { request } from "http";
@@ -15399,34 +15404,34 @@ var RecordingSession = class _RecordingSession {
15399
15404
  this.consoleEvents.push(event);
15400
15405
  pushTimeline({ offsetMs: event.offsetMs, type: "exception", label: event.text.slice(0, 160), severity: "error" });
15401
15406
  });
15402
- this.page.on("request", (request2) => {
15407
+ this.page.on("request", (request3) => {
15403
15408
  if (this.diagnosticsPaused) return;
15404
15409
  if (this.networkEvents.length >= 5e3) {
15405
15410
  this.truncated = true;
15406
15411
  return;
15407
15412
  }
15408
15413
  const started = offset();
15409
- this.requestStart.set(request2, started);
15410
- const event = { id: `net_${this.networkEvents.length + 1}`, offsetMs: started, method: request2.method(), url: sanitizeUrl(request2.url()), resourceType: request2.resourceType() };
15411
- if (captureNetworkBodies && request2.postData()) {
15412
- const redacted = redactBody(request2.postData(), request2.headers()["content-type"]);
15414
+ this.requestStart.set(request3, started);
15415
+ const event = { id: `net_${this.networkEvents.length + 1}`, offsetMs: started, method: request3.method(), url: sanitizeUrl(request3.url()), resourceType: request3.resourceType() };
15416
+ if (captureNetworkBodies && request3.postData()) {
15417
+ const redacted = redactBody(request3.postData(), request3.headers()["content-type"]);
15413
15418
  event.requestBody = redacted.body;
15414
15419
  event.bodyOmittedReason = redacted.omittedReason;
15415
15420
  if (redacted.body) this.bodyBytes += Buffer.byteLength(redacted.body);
15416
15421
  }
15417
15422
  this.networkEvents.push(event);
15418
- this.requestEvents.set(request2, event);
15423
+ this.requestEvents.set(request3, event);
15419
15424
  });
15420
15425
  this.page.on("response", (response) => this.updateResponse(response));
15421
- this.page.on("requestfailed", (request2) => {
15422
- const event = this.requestEvents.get(request2);
15426
+ this.page.on("requestfailed", (request3) => {
15427
+ const event = this.requestEvents.get(request3);
15423
15428
  if (!event) return;
15424
- event.failureReason = request2.failure()?.errorText ?? "request_failed";
15425
- event.durationMs = Math.max(0, offset() - (this.requestStart.get(request2) ?? offset()));
15429
+ event.failureReason = request3.failure()?.errorText ?? "request_failed";
15430
+ event.durationMs = Math.max(0, offset() - (this.requestStart.get(request3) ?? offset()));
15426
15431
  pushTimeline({ offsetMs: event.offsetMs, type: "network", label: `${event.method} ${event.url} \u2192 ${event.failureReason}`, severity: "error" });
15427
15432
  });
15428
- this.page.on("requestfinished", async (request2) => {
15429
- const response = await request2.response();
15433
+ this.page.on("requestfinished", async (request3) => {
15434
+ const response = await request3.response();
15430
15435
  if (response) await this.captureResponseBody(response);
15431
15436
  });
15432
15437
  }
@@ -15546,7 +15551,7 @@ async function runDaemon() {
15546
15551
  await unlink(socketPath).catch((error51) => {
15547
15552
  if (error51.code !== "ENOENT") throw error51;
15548
15553
  });
15549
- const server = createServer2((request2, response) => void route(request2, response));
15554
+ const server = createServer2((request3, response) => void route(request3, response));
15550
15555
  server.listen(socketPath, () => process.platform !== "win32" && import("fs/promises").then(({ chmod }) => chmod(socketPath, 384)));
15551
15556
  const idleTimer = setInterval(() => {
15552
15557
  void (async () => {
@@ -15565,14 +15570,14 @@ async function runDaemon() {
15565
15570
  process.once("SIGTERM", close);
15566
15571
  process.once("SIGINT", close);
15567
15572
  }
15568
- async function route(request2, response) {
15573
+ async function route(request3, response) {
15569
15574
  lastActivity = Date.now();
15570
- const url2 = new URL(request2.url ?? "/", "http://daemon.local");
15571
- if (request2.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid });
15572
- if (request2.method === "POST" && url2.pathname === "/sessions") {
15575
+ const url2 = new URL(request3.url ?? "/", "http://daemon.local");
15576
+ if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid });
15577
+ if (request3.method === "POST" && url2.pathname === "/sessions") {
15573
15578
  if ([...sessions.values()].filter((entry) => entry.state === "recording").length >= 2) return json2(response, 429, { error: { code: "quota_exceeded", message: "This recorder already has two active sessions." } });
15574
15579
  try {
15575
- const input = await readJson(request2);
15580
+ const input = await readJson(request3);
15576
15581
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
15577
15582
  const outputDirectory = join7(recordingTempRoot(), input.recordingId);
15578
15583
  const session = await RecordingSession.start({ ...input, outputDirectory });
@@ -15591,8 +15596,8 @@ async function route(request2, response) {
15591
15596
  if (match) {
15592
15597
  const entry = sessions.get(decodeURIComponent(match[1]));
15593
15598
  if (!entry) return json2(response, 404, { error: { code: "recording_not_found", message: "The local recording session was not found." } });
15594
- if (request2.method === "GET" && !match[2]) return json2(response, 200, serializeEntry(entry));
15595
- if (request2.method === "POST" && match[2] === "stop") {
15599
+ if (request3.method === "GET" && !match[2]) return json2(response, 200, serializeEntry(entry));
15600
+ if (request3.method === "POST" && match[2] === "stop") {
15596
15601
  if (entry.state === "ready") return json2(response, 200, serializeEntry(entry));
15597
15602
  await finalizeEntry(entry, "agent_request");
15598
15603
  return json2(response, entry.result ? 200 : 500, serializeEntry(entry));
@@ -15633,10 +15638,10 @@ async function persistState() {
15633
15638
  `, { mode: 384 });
15634
15639
  await rename(temporary, daemonStatePath());
15635
15640
  }
15636
- async function readJson(request2) {
15641
+ async function readJson(request3) {
15637
15642
  const chunks = [];
15638
15643
  let size = 0;
15639
- for await (const chunk of request2) {
15644
+ for await (const chunk of request3) {
15640
15645
  const buffer = Buffer.from(chunk);
15641
15646
  size += buffer.length;
15642
15647
  if (size > 1e6) throw new Error("Request body exceeds 1 MB.");
@@ -15689,6 +15694,153 @@ function cliFailureCode(error51) {
15689
15694
  return message.match(/^([a-z][a-z0-9_]{0,79}):/)?.[1] ?? "unexpected_error";
15690
15695
  }
15691
15696
 
15697
+ // src/agent-session.ts
15698
+ import { hostname as hostname3, homedir as homedir2 } from "os";
15699
+ import { mkdir as mkdir5, rm as rm4, stat as stat3 } from "fs/promises";
15700
+ import { join as join8 } from "path";
15701
+ import { createHash, randomBytes } from "crypto";
15702
+ var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
15703
+ var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
15704
+ var AgentAuthError = class extends Error {
15705
+ constructor(code, message) {
15706
+ super(`${code}: ${message}`);
15707
+ this.code = code;
15708
+ }
15709
+ };
15710
+ function parseAuthorization(value) {
15711
+ if (!value?.startsWith("{")) return null;
15712
+ const saved = JSON.parse(value);
15713
+ if (saved.version !== 1 || typeof saved.anonymous !== "boolean") throw new Error("Saved Rill authorization is invalid. Run rill connect --email <your-email>.");
15714
+ return saved;
15715
+ }
15716
+ async function request2(apiUrl, path, body, form = false) {
15717
+ const url2 = new URL(apiUrl);
15718
+ if (url2.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)) throw new Error("Agent authentication requires HTTPS.");
15719
+ const response = await fetch(`${apiUrl}${path}`, {
15720
+ method: "POST",
15721
+ redirect: "error",
15722
+ signal: AbortSignal.timeout(15e3),
15723
+ headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
15724
+ body: form ? new URLSearchParams(Object.entries(body).map(([key, value2]) => [key, String(value2)])) : JSON.stringify(body)
15725
+ });
15726
+ const value = await response.json();
15727
+ if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
15728
+ return value;
15729
+ }
15730
+ function withTokens(saved, tokens) {
15731
+ return {
15732
+ ...saved,
15733
+ accessToken: tokens.access_token,
15734
+ accessExpiresAt: Date.now() + tokens.expires_in * 1e3,
15735
+ ...tokens.identity_assertion ? { identityAssertion: tokens.identity_assertion, assertionExpires: tokens.assertion_expires } : {}
15736
+ };
15737
+ }
15738
+ var save = (apiUrl, value) => saveCredential(apiUrl, JSON.stringify(value));
15739
+ var pending = /* @__PURE__ */ new Map();
15740
+ async function loadAgentCredential(apiUrl) {
15741
+ if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
15742
+ const inFlight = pending.get(apiUrl);
15743
+ if (inFlight) return inFlight;
15744
+ const promise2 = (async () => {
15745
+ const raw = await loadStoredCredential(apiUrl);
15746
+ const saved = parseAuthorization(raw);
15747
+ if (!saved) return raw;
15748
+ if (saved.accessToken && (saved.accessExpiresAt ?? 0) > Date.now() + 6e4) return saved.accessToken;
15749
+ if (!saved.identityAssertion && saved.anonymous && saved.registrationToken) return null;
15750
+ if (!saved.identityAssertion) throw new Error("Connection is awaiting approval. Run rill connect --email <your-email>.");
15751
+ const tokens = await request2(apiUrl, "/oauth2/token", { grant_type: JWT_GRANT, assertion: saved.identityAssertion }, true);
15752
+ await save(apiUrl, withTokens(saved, tokens));
15753
+ return tokens.access_token;
15754
+ })();
15755
+ pending.set(apiUrl, promise2);
15756
+ try {
15757
+ return await promise2;
15758
+ } finally {
15759
+ pending.delete(apiUrl);
15760
+ }
15761
+ }
15762
+ async function ensureAnonymousCredential(apiUrl) {
15763
+ const root = join8(homedir2(), ".rill", "auth-locks");
15764
+ await mkdir5(root, { recursive: true, mode: 448 });
15765
+ const lock = join8(root, createHash("sha256").update(apiUrl).digest("hex"));
15766
+ const deadline = Date.now() + 3e4;
15767
+ while (true) {
15768
+ try {
15769
+ await mkdir5(lock, { mode: 448 });
15770
+ break;
15771
+ } catch (error51) {
15772
+ if (error51.code !== "EEXIST") throw error51;
15773
+ const info = await stat3(lock).catch(() => null);
15774
+ if (info && Date.now() - info.mtimeMs > 12e4) {
15775
+ await rm4(lock, { recursive: true, force: true });
15776
+ continue;
15777
+ }
15778
+ if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
15779
+ await new Promise((resolve) => setTimeout(resolve, 200));
15780
+ }
15781
+ }
15782
+ try {
15783
+ return await createAnonymousCredential(apiUrl);
15784
+ } finally {
15785
+ await rm4(lock, { recursive: true, force: true });
15786
+ }
15787
+ }
15788
+ async function createAnonymousCredential(apiUrl) {
15789
+ const existing = await loadAgentCredential(apiUrl);
15790
+ if (existing) return existing;
15791
+ const stored = parseAuthorization(await loadStoredCredential(apiUrl));
15792
+ const initial = stored?.registrationToken ? stored : { version: 1, anonymous: true, registrationToken: randomBytes(32).toString("hex") };
15793
+ await save(apiUrl, initial);
15794
+ const registered = await request2(apiUrl, "/agent/identity", { type: "anonymous", analytics_enabled: process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1", registration_token: initial.registrationToken, name: `${hostname3()} \xB7 ${process.platform}` });
15795
+ const saved = { ...initial, registrationToken: void 0, identityAssertion: registered.identity_assertion, assertionExpires: registered.assertion_expires, claimToken: registered.claim_token };
15796
+ await save(apiUrl, saved);
15797
+ process.stderr.write("Recording anonymously: three recordings, up to five minutes each. Recordings and share links expire after seven days. Run rill connect --email <your-email> to keep them.\n");
15798
+ return await loadAgentCredential(apiUrl);
15799
+ }
15800
+ async function connectAgent(apiUrl, email3) {
15801
+ if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
15802
+ let saved = parseAuthorization(await loadStoredCredential(apiUrl));
15803
+ let claim;
15804
+ if (saved?.claimToken && saved.anonymous) {
15805
+ try {
15806
+ const result = await request2(apiUrl, "/agent/identity/claim", { claim_token: saved.claimToken, email: email3 });
15807
+ claim = result.claim_attempt;
15808
+ } catch (error51) {
15809
+ if (!(error51 instanceof AgentAuthError) || error51.code !== "claim_expired") throw error51;
15810
+ }
15811
+ }
15812
+ if (!claim) {
15813
+ await save(apiUrl, saved ?? { version: 1, anonymous: true });
15814
+ const result = await request2(apiUrl, "/agent/identity", { type: "service_auth", login_hint: email3, name: `${hostname3()} \xB7 ${process.platform}` });
15815
+ saved = { version: 1, anonymous: true, claimToken: result.claim_token };
15816
+ await save(apiUrl, saved);
15817
+ claim = result.claim;
15818
+ }
15819
+ process.stderr.write(`Open ${claim.verification_uri}
15820
+ Sign in as ${email3} and enter ${claim.user_code}. Approve only the request you started.
15821
+ `);
15822
+ const deadline = Date.now() + claim.expires_in * 1e3;
15823
+ let interval = claim.interval;
15824
+ while (Date.now() < deadline) {
15825
+ await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
15826
+ try {
15827
+ const tokens = await request2(apiUrl, "/oauth2/token", { grant_type: CLAIM_GRANT, claim_token: saved.claimToken }, true);
15828
+ const connected = withTokens({ version: 1, anonymous: false }, tokens);
15829
+ await save(apiUrl, connected);
15830
+ return { status: "authenticated", expiresAt: connected.assertionExpires, apiUrl };
15831
+ } catch (error51) {
15832
+ if (error51 instanceof AgentAuthError && error51.code === "expired_token") throw new AgentAuthError("expired_token", "The approval attempt expired. Run rill connect again to request a fresh code using the same saved claim token.");
15833
+ if (error51 instanceof AgentAuthError && error51.code === "authorization_pending") continue;
15834
+ if (error51 instanceof AgentAuthError && error51.code === "slow_down") {
15835
+ interval += 5;
15836
+ continue;
15837
+ }
15838
+ throw error51;
15839
+ }
15840
+ }
15841
+ throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again to restart the approval attempt using the same saved claim token.");
15842
+ }
15843
+
15692
15844
  // src/keychain.ts
15693
15845
  import { spawn as spawn4 } from "child_process";
15694
15846
  var service = "rill";
@@ -15703,15 +15855,20 @@ async function saveCredential(apiUrl, token) {
15703
15855
  }
15704
15856
  throw new Error("OS keychain storage is not supported on this platform yet. Use RILL_TOKEN.");
15705
15857
  }
15706
- async function loadCredential(apiUrl) {
15707
- if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
15858
+ function loadCredential(apiUrl) {
15859
+ return loadAgentCredential(apiUrl);
15860
+ }
15861
+ async function loadStoredCredential(apiUrl) {
15708
15862
  try {
15709
- if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim();
15710
- if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim();
15711
- } catch {
15712
- return null;
15863
+ if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
15864
+ if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
15865
+ } catch (error51) {
15866
+ const code = error51.exitCode;
15867
+ if (process.platform === "darwin" && code === 44) return null;
15868
+ if (process.platform === "linux" && code === 1 && !error51.message.trim()) return null;
15869
+ throw new Error("Cannot read the OS credential store. Unlock your keychain or enable Secret Service; your identity has not been replaced.");
15713
15870
  }
15714
- return null;
15871
+ throw new Error("Local authorization requires macOS Keychain or Linux Secret Service.");
15715
15872
  }
15716
15873
  function run(command, args, input) {
15717
15874
  return new Promise((resolve, reject) => {
@@ -15721,7 +15878,7 @@ function run(command, args, input) {
15721
15878
  child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
15722
15879
  child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
15723
15880
  child.once("error", reject);
15724
- child.once("exit", (code) => code === 0 ? resolve(Buffer.concat(stdout).toString("utf8")) : reject(new Error(Buffer.concat(stderr).toString("utf8").trim() || `${command} exited with ${code}.`)));
15881
+ child.once("exit", (code) => code === 0 ? resolve(Buffer.concat(stdout).toString("utf8")) : reject(Object.assign(new Error(Buffer.concat(stderr).toString("utf8").trim()), { exitCode: code })));
15725
15882
  if (input) child.stdin.end(input);
15726
15883
  else child.stdin.end();
15727
15884
  });
@@ -15855,7 +16012,7 @@ async function runDoctor(apiUrl, overrides = {}) {
15855
16012
  } else {
15856
16013
  const token = await dependencies.loadCredential(apiUrl);
15857
16014
  if (!token) {
15858
- checks.push({ name: "authentication", status: "fail", message: "No Rill agent credential is available.", recovery: `Run \`rill --api-url ${apiUrl} login\` or set RILL_TOKEN.` });
16015
+ checks.push({ name: "authentication", status: health.anonymousAccess ? "warn" : "fail", message: health.anonymousAccess ? "The first recording will create an anonymous identity in your OS credential store. Anonymous recordings expire after seven days." : "No Rill agent credential is available.", recovery: `Run \`rill --api-url ${apiUrl} login\` or set RILL_TOKEN.` });
15859
16016
  } else {
15860
16017
  try {
15861
16018
  const session = await dependencies.agentSession(apiUrl, token);
@@ -15873,10 +16030,10 @@ async function runDoctor(apiUrl, overrides = {}) {
15873
16030
  checks.push({ name: "ffmpeg", status: "skip", message: "Remote encoding does not require local FFmpeg." });
15874
16031
  try {
15875
16032
  const token = await dependencies.loadCredential(apiUrl);
15876
- if (!token || !(await dependencies.encodingCapabilities(apiUrl, token)).enabled) throw new Error("Remote encoding is not enabled for this workspace.");
15877
- checks.push({ name: "encoding", status: "pass", message: "Remote encoding is enabled." });
16033
+ if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
16034
+ checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
15878
16035
  } catch (error51) {
15879
- checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "Use --encoding local until your workspace joins the private trial." });
16036
+ checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "Remote encoding must be enabled on the server before using the default recording workflow." });
15880
16037
  }
15881
16038
  }
15882
16039
  return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
@@ -15990,10 +16147,10 @@ function controlPlaneUrl(environmentUrl) {
15990
16147
  async function removeLocalCapture(id) {
15991
16148
  await ensureDaemon(process.argv[1]);
15992
16149
  const local = await recordingStatus(id).catch(() => null);
15993
- if (local?.result?.frameDirectory) await rm4(dirname2(local.result.videoPath), { recursive: true, force: true });
16150
+ if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
15994
16151
  }
15995
- function watchEncoding(apiUrl, id, token) {
15996
- const child = spawn6(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env, RILL_TOKEN: token } });
16152
+ function watchEncoding(apiUrl, id) {
16153
+ const child = spawn6(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env } });
15997
16154
  child.on("error", () => void 0);
15998
16155
  child.unref();
15999
16156
  }
@@ -16009,8 +16166,9 @@ program.command("doctor").description("Validate Rill prerequisites before record
16009
16166
  });
16010
16167
  async function authorize(quiet = false) {
16011
16168
  const options = program.opts();
16169
+ if (parseAuthorization(await loadStoredCredential(options.apiUrl))?.anonymous) throw new Error("Use rill connect --email <your-email> to keep and claim this anonymous identity before using account access.");
16012
16170
  const client = new ControlPlaneClient(options.apiUrl, null);
16013
- const authorization = await client.createDeviceAuthorization({ deviceName: `${hostname3()} \xB7 ${process.platform}` });
16171
+ const authorization = await client.createDeviceAuthorization({ deviceName: `${hostname4()} \xB7 ${process.platform}` });
16014
16172
  process.stderr.write(`Open ${authorization.verificationUri} and enter ${authorization.userCode}
16015
16173
  `);
16016
16174
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
@@ -16029,7 +16187,11 @@ async function authorize(quiet = false) {
16029
16187
  }
16030
16188
  throw new Error("Device authorization expired.");
16031
16189
  }
16032
- program.command("login").description("Authorize Rill once.").action(async () => {
16190
+ program.command("connect").description("Connect this agent once, with renewable account access.").requiredOption("--email <email>", "Email of the Rill account approving access").action(async (input) => {
16191
+ const options = program.opts();
16192
+ emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email) }, options.pretty);
16193
+ });
16194
+ program.command("login").description("Authorize Rill using the existing device login flow.").action(async () => {
16033
16195
  await authorize();
16034
16196
  });
16035
16197
  var record2 = program.command("record").description("Run a cloud browser from a URL and prompt, or manage a local recording.").argument("[url]").option("-p, --prompt <prompt>").option("--expectation <text>").option("--account <name>", "Saved website test account").option("--id <uuid>", "Retry a submission with the same run ID").option("--no-wait").action(async (url2, commandOptions) => {
@@ -16059,15 +16221,15 @@ var record2 = program.command("record").description("Run a cloud browser from a
16059
16221
  var cloudRun = program.command("run").description("Retrieve or cancel a cloud run.");
16060
16222
  for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<run-id>").action(async (id) => {
16061
16223
  const options = program.opts();
16062
- const client = new ControlPlaneClient(options.apiUrl, await loadCredential(options.apiUrl));
16224
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16063
16225
  const run2 = await (action === "status" ? client.cloudRun(id) : client.cancelCloudRun(id));
16064
16226
  if (options.json || options.pretty) emit(customerRun(run2), options.pretty);
16065
16227
  else process.stdout.write(formatCloudRun(run2));
16066
16228
  });
16067
16229
  record2.command("start").option("--encoding <mode>", "remote or local", "remote").option("--url <url>").option("--title <title>").option("--description <description>", "").option("--headed").option("--cdp-url <url>").option("--width <pixels>", "", "1280").option("--height <pixels>", "", "720").option("--max-duration <seconds>", "", "600").action(async (commandOptions) => {
16068
16230
  const options = program.opts();
16069
- const token = await loadCredential(options.apiUrl);
16070
- const client = new ControlPlaneClient(options.apiUrl, token);
16231
+ await ensureAnonymousCredential(options.apiUrl);
16232
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16071
16233
  const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
16072
16234
  if (!["local", "remote"].includes(commandOptions.encoding)) throw new Error("Encoding must be remote or local.");
16073
16235
  if (commandOptions.encoding === "remote" && !(await client.encodingCapabilities()).enabled) throw new Error("Remote encoding is unavailable for this workspace. Use --encoding local.");
@@ -16079,7 +16241,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
16079
16241
  const local = await startRecordingWithCompensation(remote, {
16080
16242
  startLocal: async () => {
16081
16243
  await ensureDaemon(process.argv[1]);
16082
- return startRecording({ encoding: commandOptions.encoding, recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Number(commandOptions.maxDuration), captureNetworkBodies: remote.bodyCaptureEnabled, cursorAppearance: remote.cursorAppearance });
16244
+ return startRecording({ encoding: commandOptions.encoding, recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600), captureNetworkBodies: remote.bodyCaptureEnabled, cursorAppearance: remote.cursorAppearance });
16083
16245
  },
16084
16246
  cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
16085
16247
  });
@@ -16090,7 +16252,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
16090
16252
  headed: Boolean(commandOptions.headed),
16091
16253
  attached: Boolean(commandOptions.cdpUrl)
16092
16254
  }).catch(() => void 0);
16093
- return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, maxDurationSeconds: Number(commandOptions.maxDuration) };
16255
+ return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
16094
16256
  });
16095
16257
  emit(result, options.pretty);
16096
16258
  });
@@ -16099,8 +16261,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
16099
16261
  await ensureDaemon(process.argv[1]);
16100
16262
  const local = await stopRecording(recordingId);
16101
16263
  if (!local.result) throw new Error(local.error ?? "The recorder did not produce an artifact.");
16102
- const token = await loadCredential(options.apiUrl);
16103
- const client = new ControlPlaneClient(options.apiUrl, token);
16264
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16104
16265
  const stopped = await client.markRecordingStopped(recordingId, {
16105
16266
  durationSeconds: local.result.durationSeconds,
16106
16267
  stoppedReason: local.result.stoppedReason,
@@ -16112,11 +16273,11 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
16112
16273
  const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
16113
16274
  const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
16114
16275
  emit({ schemaVersion: 1, ...result2, ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
16115
- if (result2.status === "ready") await rm4(dirname2(local.result.videoPath), { recursive: true, force: true });
16116
- else watchEncoding(options.apiUrl, recordingId, token);
16276
+ if (result2.status === "ready") await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16277
+ else watchEncoding(options.apiUrl, recordingId);
16117
16278
  return;
16118
16279
  }
16119
- const video = await stat3(local.result.videoPath);
16280
+ const video = await stat4(local.result.videoPath);
16120
16281
  const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
16121
16282
  await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
16122
16283
  const remote = commandOptions.wait === false || record2.opts().wait === false ? null : await client.waitUntilPlayable(recordingId);
@@ -16126,17 +16287,18 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
16126
16287
  recordingId,
16127
16288
  status: remote ? "ready" : "processing",
16128
16289
  shareUrl: remote?.shareUrl ?? null,
16290
+ expiresAt: remote?.expires_at ?? null,
16129
16291
  durationSeconds: local.result.durationSeconds,
16130
16292
  stoppedReason: local.result.stoppedReason,
16131
16293
  diagnostics: local.result.diagnostics,
16132
16294
  ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
16133
16295
  };
16134
16296
  emit(result, options.pretty);
16135
- if (remote) await rm4(dirname2(local.result.videoPath), { recursive: true, force: true });
16297
+ if (remote) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16136
16298
  });
16137
16299
  record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
16138
16300
  const options = program.opts();
16139
- const client = new ControlPlaneClient(options.apiUrl, await loadCredential(options.apiUrl));
16301
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16140
16302
  const current = await client.encodingStatus(recordingId).catch((error51) => {
16141
16303
  if (error51 instanceof ControlPlaneError && error51.code === "recording_not_found") return null;
16142
16304
  throw error51;
@@ -16154,17 +16316,17 @@ record2.command("retry").argument("<recording-id>").option("--no-wait").action(a
16154
16316
  const result = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
16155
16317
  emit({ schemaVersion: 1, ...result }, options.pretty);
16156
16318
  if (result.status === "ready") await removeLocalCapture(recordingId);
16157
- else watchEncoding(options.apiUrl, recordingId, await loadCredential(options.apiUrl));
16319
+ else watchEncoding(options.apiUrl, recordingId);
16158
16320
  });
16159
16321
  program.command("encoding-watch", { hidden: true }).argument("<recording-id>").action(async (id) => {
16160
16322
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) throw new Error("Invalid recording ID");
16161
16323
  const options = program.opts();
16162
- const client = new ControlPlaneClient(options.apiUrl, await loadCredential(options.apiUrl));
16324
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16163
16325
  const result = await waitForEncoding(client, id, 864e5);
16164
16326
  if (result.status === "ready") {
16165
16327
  await ensureDaemon(process.argv[1]);
16166
16328
  const local = await recordingStatus(id).catch(() => null);
16167
- if (local?.result?.frameDirectory) await rm4(dirname2(local.result.videoPath), { recursive: true, force: true });
16329
+ if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16168
16330
  }
16169
16331
  });
16170
16332
  var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
@@ -16174,7 +16336,7 @@ feedback.command("submit").argument("[recording-id]", "Omit only when setup fail
16174
16336
  if (![commandOptions.helped, commandOptions.friction, commandOptions.improvement].some((value) => value.trim())) throw new Error("validation_failed: Include at least one short feedback answer.");
16175
16337
  const options = program.opts();
16176
16338
  const token = await loadCredential(options.apiUrl);
16177
- const client = new ControlPlaneClient(options.apiUrl, token);
16339
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16178
16340
  await client.submitRecordingFeedback(recordingId ?? null, {
16179
16341
  outcome: commandOptions.outcome,
16180
16342
  whatHelped: commandOptions.helped,
@@ -16183,10 +16345,14 @@ feedback.command("submit").argument("[recording-id]", "Omit only when setup fail
16183
16345
  });
16184
16346
  emit(feedbackSubmissionReceipt(recordingId ?? null), options.pretty);
16185
16347
  });
16348
+ record2.command("list").description("List recordings visible to this connection.").action(async () => {
16349
+ const options = program.opts();
16350
+ emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).listRecordings(), options.pretty);
16351
+ });
16186
16352
  record2.command("status").argument("<recording-id>").action(async (recordingId) => {
16187
16353
  const options = program.opts();
16188
16354
  const token = await loadCredential(options.apiUrl);
16189
- const client = new ControlPlaneClient(options.apiUrl, token);
16355
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16190
16356
  try {
16191
16357
  const encoding = await client.encodingStatus(recordingId);
16192
16358
  const remote = encoding.status === "ready" ? await client.recordingStatus(recordingId) : {};
@@ -16208,24 +16374,24 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
16208
16374
  record2.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
16209
16375
  const options = program.opts();
16210
16376
  const token = await loadCredential(options.apiUrl);
16211
- const client = new ControlPlaneClient(options.apiUrl, token);
16377
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16212
16378
  emit(await client.cancelRecording(recordingId), options.pretty);
16213
16379
  });
16214
16380
  program.command("upload").argument("<video-file>").option("--title <title>").option("--no-wait").action(async (videoFile, commandOptions) => {
16215
16381
  const options = program.opts();
16216
- const token = await loadCredential(options.apiUrl);
16217
- const client = new ControlPlaneClient(options.apiUrl, token);
16218
- const file2 = await stat3(videoFile);
16382
+ await ensureAnonymousCredential(options.apiUrl);
16383
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16384
+ const file2 = await stat4(videoFile);
16219
16385
  const recording = await client.createRecording({ title: commandOptions.title ?? basename(videoFile), sourceUrl: null });
16220
16386
  const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file2.size, filename: basename(videoFile), uploadKind: "existing_video" });
16221
16387
  await client.uploadVideo(upload.uploadUrl, videoFile);
16222
16388
  const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recording.recordingId);
16223
16389
  if (remote) process.stderr.write("\n");
16224
- emit({ schemaVersion: 1, recordingId: recording.recordingId, status: remote ? "ready" : "processing", shareUrl: remote?.shareUrl ?? null, diagnosticsAvailable: false }, options.pretty);
16390
+ emit({ schemaVersion: 1, recordingId: recording.recordingId, status: remote ? "ready" : "processing", shareUrl: remote?.shareUrl ?? null, expiresAt: remote?.expires_at ?? recording.expiresAt ?? null, diagnosticsAvailable: false }, options.pretty);
16225
16391
  });
16226
16392
  program.command("inspect").argument("<share-url>").action(async (shareUrl) => {
16227
16393
  const options = program.opts();
16228
- emit(await new ControlPlaneClient(options.apiUrl, null).inspect(shareUrl), options.pretty);
16394
+ emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).inspect(shareUrl), options.pretty);
16229
16395
  });
16230
16396
  var drafts = program.command("drafts");
16231
16397
  drafts.command("list").action(async () => {
@@ -16241,9 +16407,9 @@ drafts.command("purge").action(async () => {
16241
16407
  let purged = 0;
16242
16408
  for (const name of names) {
16243
16409
  const path = `${root}/${name}`;
16244
- const metadata = await stat3(path);
16410
+ const metadata = await stat4(path);
16245
16411
  if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
16246
- await rm4(path, { recursive: true, force: true });
16412
+ await rm5(path, { recursive: true, force: true });
16247
16413
  purged += 1;
16248
16414
  }
16249
16415
  }
@@ -16255,7 +16421,7 @@ program.hook("preAction", async (_command, actionCommand) => {
16255
16421
  const options = program.opts();
16256
16422
  const token = await loadCredential(options.apiUrl).catch(() => null);
16257
16423
  if (!token) return;
16258
- const client = new ControlPlaneClient(options.apiUrl, token);
16424
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16259
16425
  await startCliTelemetry(client, command);
16260
16426
  });
16261
16427
  async function startCliTelemetry(client, command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runalabs/rill-cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Record, inspect, and share agent-controlled browser runs with Rill.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {