@runalabs/rill-cli 0.1.10 → 0.1.12

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 +30 -7
  2. package/dist/cli.js +508 -318
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5,9 +5,389 @@ var __export = (target, all) => {
5
5
  __defProp(target, name, { get: all[name], enumerable: true });
6
6
  };
7
7
 
8
+ // src/agent-session.ts
9
+ import { hostname, homedir } from "os";
10
+ import { mkdir, rm, stat } from "fs/promises";
11
+ import { join } from "path";
12
+ import { createHash, randomBytes } from "crypto";
13
+ var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
14
+ var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
15
+ var AgentAuthError = class extends Error {
16
+ constructor(code, message) {
17
+ super(`${code}: ${message}`);
18
+ this.code = code;
19
+ }
20
+ };
21
+ function parseAuthorization(value) {
22
+ if (!value?.startsWith("{")) return null;
23
+ const saved = JSON.parse(value);
24
+ if (saved.version !== 1 || typeof saved.anonymous !== "boolean") throw new Error("Saved Rill authorization is invalid. Run rill connect --email <your-email>.");
25
+ return saved;
26
+ }
27
+ async function request(apiUrl, path, body, form = false) {
28
+ const url2 = new URL(apiUrl);
29
+ if (url2.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)) throw new Error("Agent authentication requires HTTPS.");
30
+ const response = await fetch(`${apiUrl}${path}`, {
31
+ method: "POST",
32
+ redirect: "error",
33
+ signal: AbortSignal.timeout(15e3),
34
+ headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
35
+ body: form ? new URLSearchParams(Object.entries(body).map(([key, value2]) => [key, String(value2)])) : JSON.stringify(body)
36
+ });
37
+ const value = await response.json();
38
+ if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
39
+ return value;
40
+ }
41
+ function withTokens(saved, tokens) {
42
+ return {
43
+ ...saved,
44
+ accessToken: tokens.access_token,
45
+ accessExpiresAt: Date.now() + tokens.expires_in * 1e3,
46
+ ...tokens.identity_assertion ? { identityAssertion: tokens.identity_assertion, assertionExpires: tokens.assertion_expires } : {}
47
+ };
48
+ }
49
+ var save = (apiUrl, value) => saveCredential(apiUrl, JSON.stringify(value));
50
+ var pending = /* @__PURE__ */ new Map();
51
+ async function loadAgentCredential(apiUrl) {
52
+ if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
53
+ const inFlight = pending.get(apiUrl);
54
+ if (inFlight) return inFlight;
55
+ const promise2 = (async () => {
56
+ const raw = await loadStoredCredential(apiUrl);
57
+ const saved = parseAuthorization(raw);
58
+ if (!saved) return raw;
59
+ if (saved.accessToken && (saved.accessExpiresAt ?? 0) > Date.now() + 6e4) return saved.accessToken;
60
+ if (!saved.identityAssertion && saved.anonymous && saved.registrationToken) return null;
61
+ if (!saved.identityAssertion) throw new Error("Connection is awaiting approval. Run rill connect --email <your-email>.");
62
+ const tokens = await request(apiUrl, "/oauth2/token", { grant_type: JWT_GRANT, assertion: saved.identityAssertion }, true);
63
+ await save(apiUrl, withTokens(saved, tokens));
64
+ return tokens.access_token;
65
+ })();
66
+ pending.set(apiUrl, promise2);
67
+ try {
68
+ return await promise2;
69
+ } finally {
70
+ pending.delete(apiUrl);
71
+ }
72
+ }
73
+ async function ensureAnonymousCredential(apiUrl) {
74
+ const root = join(homedir(), ".rill", "auth-locks");
75
+ await mkdir(root, { recursive: true, mode: 448 });
76
+ const lock = join(root, createHash("sha256").update(apiUrl).digest("hex"));
77
+ const deadline = Date.now() + 3e4;
78
+ while (true) {
79
+ try {
80
+ await mkdir(lock, { mode: 448 });
81
+ break;
82
+ } catch (error51) {
83
+ if (error51.code !== "EEXIST") throw error51;
84
+ const info = await stat(lock).catch(() => null);
85
+ if (info && Date.now() - info.mtimeMs > 12e4) {
86
+ await rm(lock, { recursive: true, force: true });
87
+ continue;
88
+ }
89
+ if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
90
+ await new Promise((resolve) => setTimeout(resolve, 200));
91
+ }
92
+ }
93
+ try {
94
+ return await createAnonymousCredential(apiUrl);
95
+ } finally {
96
+ await rm(lock, { recursive: true, force: true });
97
+ }
98
+ }
99
+ async function createAnonymousCredential(apiUrl) {
100
+ const existing = await loadAgentCredential(apiUrl);
101
+ if (existing) return existing;
102
+ const stored = parseAuthorization(await loadStoredCredential(apiUrl));
103
+ const initial = stored?.registrationToken ? stored : { version: 1, anonymous: true, registrationToken: randomBytes(32).toString("hex") };
104
+ await save(apiUrl, initial);
105
+ const registered = await request(apiUrl, "/agent/identity", { type: "anonymous", analytics_enabled: process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1", registration_token: initial.registrationToken, name: `${hostname()} \xB7 ${process.platform}` });
106
+ const saved = { ...initial, registrationToken: void 0, identityAssertion: registered.identity_assertion, assertionExpires: registered.assertion_expires, claimToken: registered.claim_token };
107
+ await save(apiUrl, saved);
108
+ 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");
109
+ return await loadAgentCredential(apiUrl);
110
+ }
111
+ async function connectAgent(apiUrl, email3) {
112
+ if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
113
+ let saved = parseAuthorization(await loadStoredCredential(apiUrl));
114
+ let claim;
115
+ if (saved?.claimToken && saved.anonymous) {
116
+ try {
117
+ const result = await request(apiUrl, "/agent/identity/claim", { claim_token: saved.claimToken, email: email3 });
118
+ claim = result.claim_attempt;
119
+ } catch (error51) {
120
+ if (!(error51 instanceof AgentAuthError) || error51.code !== "claim_expired") throw error51;
121
+ }
122
+ }
123
+ if (!claim) {
124
+ await save(apiUrl, saved ?? { version: 1, anonymous: true });
125
+ const result = await request(apiUrl, "/agent/identity", { type: "service_auth", login_hint: email3, name: `${hostname()} \xB7 ${process.platform}` });
126
+ saved = { version: 1, anonymous: true, claimToken: result.claim_token };
127
+ await save(apiUrl, saved);
128
+ claim = result.claim;
129
+ }
130
+ process.stderr.write(`Open ${claim.verification_uri}
131
+ Sign in as ${email3} and enter ${claim.user_code}. Approve only the request you started.
132
+ `);
133
+ const deadline = Date.now() + claim.expires_in * 1e3;
134
+ let interval = claim.interval;
135
+ while (Date.now() < deadline) {
136
+ await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
137
+ try {
138
+ const tokens = await request(apiUrl, "/oauth2/token", { grant_type: CLAIM_GRANT, claim_token: saved.claimToken }, true);
139
+ const connected = withTokens({ version: 1, anonymous: false }, tokens);
140
+ await save(apiUrl, connected);
141
+ return { status: "authenticated", expiresAt: connected.assertionExpires, apiUrl };
142
+ } catch (error51) {
143
+ 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.");
144
+ if (error51 instanceof AgentAuthError && error51.code === "authorization_pending") continue;
145
+ if (error51 instanceof AgentAuthError && error51.code === "slow_down") {
146
+ interval += 5;
147
+ continue;
148
+ }
149
+ throw error51;
150
+ }
151
+ }
152
+ throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again to restart the approval attempt using the same saved claim token.");
153
+ }
154
+
155
+ // src/keychain.ts
156
+ import { spawn } from "child_process";
157
+ var service = "rill";
158
+ async function saveCredential(apiUrl, token) {
159
+ if (process.platform === "darwin") {
160
+ await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
161
+ return;
162
+ }
163
+ if (process.platform === "linux") {
164
+ await run("secret-tool", ["store", "--label=Rill agent credential", "service", service, "api-url", apiUrl], token);
165
+ return;
166
+ }
167
+ throw new Error("OS keychain storage is not supported on this platform yet. Use RILL_TOKEN.");
168
+ }
169
+ function loadCredential(apiUrl) {
170
+ return loadAgentCredential(apiUrl);
171
+ }
172
+ async function loadStoredCredential(apiUrl) {
173
+ try {
174
+ if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
175
+ if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
176
+ } catch (error51) {
177
+ const code = error51.exitCode;
178
+ if (process.platform === "darwin" && code === 44) return null;
179
+ if (process.platform === "linux" && code === 1 && !error51.message.trim()) return null;
180
+ throw new Error("Cannot read the OS credential store. Unlock your keychain or enable Secret Service; your identity has not been replaced.");
181
+ }
182
+ throw new Error("Local authorization requires macOS Keychain or Linux Secret Service.");
183
+ }
184
+ function run(command, args, input) {
185
+ return new Promise((resolve, reject) => {
186
+ const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
187
+ const stdout = [];
188
+ const stderr = [];
189
+ child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
190
+ child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
191
+ child.once("error", reject);
192
+ 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 })));
193
+ if (input) child.stdin.end(input);
194
+ else child.stdin.end();
195
+ });
196
+ }
197
+
198
+ // src/control-plane.ts
199
+ import { createReadStream } from "fs";
200
+ import { readFile, stat as stat2 } from "fs/promises";
201
+ import { Upload } from "tus-js-client";
202
+ var ControlPlaneError = class extends Error {
203
+ constructor(code, message, recovery, details, httpStatus) {
204
+ super(`${code}: ${message}${recovery ? `: ${recovery}` : ""}`);
205
+ this.code = code;
206
+ this.recovery = recovery;
207
+ this.details = details;
208
+ this.httpStatus = httpStatus;
209
+ this.name = "ControlPlaneError";
210
+ }
211
+ };
212
+ var ControlPlaneHttpError = class extends Error {
213
+ constructor(httpStatus) {
214
+ super(`Request failed (${httpStatus}).`);
215
+ this.httpStatus = httpStatus;
216
+ this.name = "ControlPlaneHttpError";
217
+ }
218
+ };
219
+ var ControlPlaneClient = class {
220
+ constructor(apiUrl, token) {
221
+ this.apiUrl = apiUrl;
222
+ this.token = token;
223
+ }
224
+ encodingCapabilities() {
225
+ return this.request("/api/encoding/capabilities");
226
+ }
227
+ beginEncoding(id, manifest) {
228
+ return this.request(`/api/encoding/${encodeURIComponent(id)}`, { method: "POST", body: JSON.stringify(manifest) });
229
+ }
230
+ encodingStatus(id) {
231
+ return this.request(`/api/encoding/${encodeURIComponent(id)}`);
232
+ }
233
+ uploadEncodingPart(id, part, bytes) {
234
+ return this.request(`/api/encoding/${encodeURIComponent(id)}/parts/${part}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "Content-Length": String(bytes.length) }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(12e4) });
235
+ }
236
+ completeEncoding(id) {
237
+ return this.request(`/api/encoding/${encodeURIComponent(id)}/complete`, { method: "POST" });
238
+ }
239
+ retryEncoding(id) {
240
+ return this.request(`/api/encoding/${encodeURIComponent(id)}/retry`, { method: "POST" });
241
+ }
242
+ createCloudRun(input) {
243
+ return this.request("/api/cloud/runs", { method: "POST", body: JSON.stringify(input) });
244
+ }
245
+ cloudRun(id) {
246
+ return this.request(`/api/cloud/runs/${encodeURIComponent(id)}`);
247
+ }
248
+ cancelCloudRun(id) {
249
+ return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
250
+ }
251
+ health() {
252
+ return this.request("/api/health", {}, false);
253
+ }
254
+ agentSession() {
255
+ return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
256
+ }
257
+ reportCliCommand(input) {
258
+ return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
259
+ }
260
+ createRecording(input) {
261
+ return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
262
+ }
263
+ markRecordingStarted(recordingId, input) {
264
+ return this.request(`/api/recordings/${recordingId}/local-started`, {
265
+ method: "POST",
266
+ body: JSON.stringify(input)
267
+ });
268
+ }
269
+ markRecordingStopped(recordingId, input) {
270
+ return this.request(`/api/recordings/${recordingId}/local-stopped`, {
271
+ method: "POST",
272
+ body: JSON.stringify(input)
273
+ });
274
+ }
275
+ submitRecordingFeedback(recordingId, input) {
276
+ return this.request(recordingId ? `/api/recordings/${recordingId}/feedback` : "/api/feedback", {
277
+ method: "POST",
278
+ body: JSON.stringify(input)
279
+ });
280
+ }
281
+ provisionVideo(recordingId, input) {
282
+ return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
283
+ }
284
+ recordingStatus(recordingId) {
285
+ return this.request(`/api/recordings/${recordingId}`);
286
+ }
287
+ cancelRecording(recordingId, reason = "cancelled_by_agent") {
288
+ return this.request(`/api/recordings/${recordingId}/cancel`, {
289
+ method: "POST",
290
+ body: JSON.stringify({ reason })
291
+ });
292
+ }
293
+ async waitUntilPlayable(recordingId, timeoutMs = 10 * 6e4) {
294
+ const deadline = Date.now() + timeoutMs;
295
+ let intervalMs = 1e3;
296
+ while (Date.now() < deadline) {
297
+ const recording = await this.recordingStatus(recordingId);
298
+ if (recording.status === "ready" && recording.shareUrl) {
299
+ return { ...recording, shareUrl: new URL(recording.shareUrl, this.apiUrl).toString() };
300
+ }
301
+ if (recording.status === "failed") throw new Error(`processing_failed: ${recording.failure_message ?? "Cloudflare Stream could not process the recording."}`);
302
+ process.stderr.write(`\rprocessing ${recording.status.padEnd(12)}`);
303
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
304
+ intervalMs = Math.min(5e3, Math.round(intervalMs * 1.4));
305
+ }
306
+ throw new Error("processing_failed: Timed out waiting for the recording to become playable. Run `rill record status <recording-id>` to continue checking.");
307
+ }
308
+ async uploadVideo(uploadUrl, filePath) {
309
+ const file2 = await stat2(filePath);
310
+ if (uploadUrl.startsWith(this.apiUrl)) {
311
+ const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
312
+ if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
313
+ return;
314
+ }
315
+ await new Promise((resolve, reject) => {
316
+ const upload = new Upload(createReadStream(filePath), {
317
+ uploadUrl,
318
+ uploadSize: file2.size,
319
+ retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
320
+ removeFingerprintOnSuccess: true,
321
+ onError: reject,
322
+ onSuccess: () => resolve(),
323
+ onProgress: (sent, total) => process.stderr.write(`\ruploading ${Math.round(sent / total * 100)}%`)
324
+ });
325
+ upload.start();
326
+ });
327
+ process.stderr.write("\n");
328
+ }
329
+ async uploadDiagnostics(recordingId, filePath, summary) {
330
+ const body = await readFile(filePath);
331
+ 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 });
332
+ }
333
+ listRecordings() {
334
+ return this.request("/api/recordings");
335
+ }
336
+ createDeviceAuthorization(input) {
337
+ return this.request("/api/device/authorizations", { method: "POST", body: JSON.stringify(input) }, false);
338
+ }
339
+ pollDeviceAuthorization(deviceCode) {
340
+ return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
341
+ }
342
+ inspect(shareUrl) {
343
+ if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
344
+ const url2 = new URL(shareUrl);
345
+ const slug = url2.pathname.split("/").filter(Boolean).at(-1);
346
+ if (!slug) throw new Error("The share URL does not contain a slug.");
347
+ return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context`, { headers: { Accept: "application/json" } }).then(async (response) => {
348
+ if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
349
+ return response.json();
350
+ });
351
+ }
352
+ async request(path, init = {}, authenticated = true) {
353
+ const headers = new Headers(init.headers);
354
+ if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
355
+ if (authenticated) {
356
+ const token = typeof this.token === "function" ? await this.token() : this.token;
357
+ if (!token) throw new Error("authentication_required: Run `rill login` or set RILL_TOKEN.");
358
+ headers.set("Authorization", `Bearer ${token}`);
359
+ }
360
+ const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
361
+ const value = await response.json().catch(() => null);
362
+ if (!response.ok && value?.error?.code && value.error.message) {
363
+ throw new ControlPlaneError(value.error.code, value.error.message, value.error.recovery, value.error.details, response.status);
364
+ }
365
+ if (!response.ok) throw new ControlPlaneHttpError(response.status);
366
+ return value;
367
+ }
368
+ };
369
+
370
+ // src/whoami.ts
371
+ async function whoami(apiUrl) {
372
+ const token = await loadCredential(apiUrl);
373
+ if (!token) return { schemaVersion: 1, status: "not_connected", authenticated: false, apiUrl };
374
+ const session = await new ControlPlaneClient(apiUrl, token).agentSession();
375
+ return {
376
+ schemaVersion: 1,
377
+ status: session.anonymous ? "anonymous" : "connected",
378
+ authenticated: true,
379
+ apiUrl,
380
+ anonymous: Boolean(session.anonymous),
381
+ email: session.email ?? null,
382
+ workspaceId: session.workspaceId ?? null,
383
+ scopes: session.scopes,
384
+ expiresAt: session.expiresAt
385
+ };
386
+ }
387
+
8
388
  // src/remote-encoding.ts
9
389
  import { open, readFile as readFile2 } from "fs/promises";
10
- import { join } from "path";
390
+ import { join as join2 } from "path";
11
391
 
12
392
  // ../../node_modules/zod/v4/classic/external.js
13
393
  var external_exports = {};
@@ -142,7 +522,7 @@ __export(external_exports, {
142
522
  guid: () => guid2,
143
523
  hash: () => hash,
144
524
  hex: () => hex2,
145
- hostname: () => hostname2,
525
+ hostname: () => hostname3,
146
526
  httpUrl: () => httpUrl,
147
527
  includes: () => _includes,
148
528
  instanceof: () => _instanceof,
@@ -1553,7 +1933,7 @@ __export(regexes_exports, {
1553
1933
  extendedDuration: () => extendedDuration,
1554
1934
  guid: () => guid,
1555
1935
  hex: () => hex,
1556
- hostname: () => hostname,
1936
+ hostname: () => hostname2,
1557
1937
  html5Email: () => html5Email,
1558
1938
  httpProtocol: () => httpProtocol,
1559
1939
  idnEmail: () => idnEmail,
@@ -1631,7 +2011,7 @@ var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]
1631
2011
  var cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
1632
2012
  var base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
1633
2013
  var base64url = /^[A-Za-z0-9_-]*$/;
1634
- var hostname = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
2014
+ var hostname2 = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
1635
2015
  var domain = /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
1636
2016
  var httpProtocol = /^https?$/;
1637
2017
  var e164 = /^\+[1-9]\d{6,14}$/;
@@ -12503,7 +12883,7 @@ __export(schemas_exports2, {
12503
12883
  guid: () => guid2,
12504
12884
  hash: () => hash,
12505
12885
  hex: () => hex2,
12506
- hostname: () => hostname2,
12886
+ hostname: () => hostname3,
12507
12887
  httpUrl: () => httpUrl,
12508
12888
  instanceof: () => _instanceof,
12509
12889
  int: () => int,
@@ -13123,7 +13503,7 @@ var ZodCustomStringFormat = /* @__PURE__ */ $constructor("ZodCustomStringFormat"
13123
13503
  function stringFormat(format, fnOrRegex, _params = {}) {
13124
13504
  return _stringFormat(ZodCustomStringFormat, format, fnOrRegex, _params);
13125
13505
  }
13126
- function hostname2(_params) {
13506
+ function hostname3(_params) {
13127
13507
  return _stringFormat(ZodCustomStringFormat, "hostname", regexes_exports.hostname, _params);
13128
13508
  }
13129
13509
  function hex2(_params) {
@@ -14538,173 +14918,6 @@ var frameManifestSchema = external_exports.object({
14538
14918
  if (m.frames.at(-1).timestamp - m.frames[0].timestamp > m.durationSeconds + 1) ctx.addIssue({ code: "custom", message: "Frame timestamps exceed capture duration." });
14539
14919
  });
14540
14920
 
14541
- // src/control-plane.ts
14542
- import { createReadStream } from "fs";
14543
- import { readFile, stat } from "fs/promises";
14544
- import { Upload } from "tus-js-client";
14545
- var ControlPlaneError = class extends Error {
14546
- constructor(code, message, recovery, details, httpStatus) {
14547
- super(`${code}: ${message}${recovery ? `: ${recovery}` : ""}`);
14548
- this.code = code;
14549
- this.recovery = recovery;
14550
- this.details = details;
14551
- this.httpStatus = httpStatus;
14552
- this.name = "ControlPlaneError";
14553
- }
14554
- };
14555
- var ControlPlaneHttpError = class extends Error {
14556
- constructor(httpStatus) {
14557
- super(`Request failed (${httpStatus}).`);
14558
- this.httpStatus = httpStatus;
14559
- this.name = "ControlPlaneHttpError";
14560
- }
14561
- };
14562
- var ControlPlaneClient = class {
14563
- constructor(apiUrl, token) {
14564
- this.apiUrl = apiUrl;
14565
- this.token = token;
14566
- }
14567
- encodingCapabilities() {
14568
- return this.request("/api/encoding/capabilities");
14569
- }
14570
- beginEncoding(id, manifest) {
14571
- return this.request(`/api/encoding/${encodeURIComponent(id)}`, { method: "POST", body: JSON.stringify(manifest) });
14572
- }
14573
- encodingStatus(id) {
14574
- return this.request(`/api/encoding/${encodeURIComponent(id)}`);
14575
- }
14576
- uploadEncodingPart(id, part, bytes) {
14577
- return this.request(`/api/encoding/${encodeURIComponent(id)}/parts/${part}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "Content-Length": String(bytes.length) }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(12e4) });
14578
- }
14579
- completeEncoding(id) {
14580
- return this.request(`/api/encoding/${encodeURIComponent(id)}/complete`, { method: "POST" });
14581
- }
14582
- retryEncoding(id) {
14583
- return this.request(`/api/encoding/${encodeURIComponent(id)}/retry`, { method: "POST" });
14584
- }
14585
- createCloudRun(input) {
14586
- return this.request("/api/cloud/runs", { method: "POST", body: JSON.stringify(input) });
14587
- }
14588
- cloudRun(id) {
14589
- return this.request(`/api/cloud/runs/${encodeURIComponent(id)}`);
14590
- }
14591
- cancelCloudRun(id) {
14592
- return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
14593
- }
14594
- health() {
14595
- return this.request("/api/health", {}, false);
14596
- }
14597
- agentSession() {
14598
- return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
14599
- }
14600
- reportCliCommand(input) {
14601
- return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
14602
- }
14603
- createRecording(input) {
14604
- return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
14605
- }
14606
- markRecordingStarted(recordingId, input) {
14607
- return this.request(`/api/recordings/${recordingId}/local-started`, {
14608
- method: "POST",
14609
- body: JSON.stringify(input)
14610
- });
14611
- }
14612
- markRecordingStopped(recordingId, input) {
14613
- return this.request(`/api/recordings/${recordingId}/local-stopped`, {
14614
- method: "POST",
14615
- body: JSON.stringify(input)
14616
- });
14617
- }
14618
- submitRecordingFeedback(recordingId, input) {
14619
- return this.request(recordingId ? `/api/recordings/${recordingId}/feedback` : "/api/feedback", {
14620
- method: "POST",
14621
- body: JSON.stringify(input)
14622
- });
14623
- }
14624
- provisionVideo(recordingId, input) {
14625
- return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
14626
- }
14627
- recordingStatus(recordingId) {
14628
- return this.request(`/api/recordings/${recordingId}`);
14629
- }
14630
- cancelRecording(recordingId, reason = "cancelled_by_agent") {
14631
- return this.request(`/api/recordings/${recordingId}/cancel`, {
14632
- method: "POST",
14633
- body: JSON.stringify({ reason })
14634
- });
14635
- }
14636
- async waitUntilPlayable(recordingId, timeoutMs = 10 * 6e4) {
14637
- const deadline = Date.now() + timeoutMs;
14638
- let intervalMs = 1e3;
14639
- while (Date.now() < deadline) {
14640
- const recording = await this.recordingStatus(recordingId);
14641
- if (recording.status === "ready" && recording.shareUrl) {
14642
- return { ...recording, shareUrl: new URL(recording.shareUrl, this.apiUrl).toString() };
14643
- }
14644
- if (recording.status === "failed") throw new Error(`processing_failed: ${recording.failure_message ?? "Cloudflare Stream could not process the recording."}`);
14645
- process.stderr.write(`\rprocessing ${recording.status.padEnd(12)}`);
14646
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
14647
- intervalMs = Math.min(5e3, Math.round(intervalMs * 1.4));
14648
- }
14649
- throw new Error("processing_failed: Timed out waiting for the recording to become playable. Run `rill record status <recording-id>` to continue checking.");
14650
- }
14651
- async uploadVideo(uploadUrl, filePath) {
14652
- const file2 = await stat(filePath);
14653
- if (uploadUrl.startsWith(this.apiUrl)) {
14654
- const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
14655
- if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
14656
- return;
14657
- }
14658
- await new Promise((resolve, reject) => {
14659
- const upload = new Upload(createReadStream(filePath), {
14660
- uploadUrl,
14661
- uploadSize: file2.size,
14662
- retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
14663
- removeFingerprintOnSuccess: true,
14664
- onError: reject,
14665
- onSuccess: () => resolve(),
14666
- onProgress: (sent, total) => process.stderr.write(`\ruploading ${Math.round(sent / total * 100)}%`)
14667
- });
14668
- upload.start();
14669
- });
14670
- process.stderr.write("\n");
14671
- }
14672
- async uploadDiagnostics(recordingId, filePath, summary) {
14673
- const body = await readFile(filePath);
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
- }
14676
- createDeviceAuthorization(input) {
14677
- return this.request("/api/device/authorizations", { method: "POST", body: JSON.stringify(input) }, false);
14678
- }
14679
- pollDeviceAuthorization(deviceCode) {
14680
- return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
14681
- }
14682
- inspect(shareUrl) {
14683
- const url2 = new URL(shareUrl);
14684
- const slug = url2.pathname.split("/").filter(Boolean).at(-1);
14685
- if (!slug) throw new Error("The share URL does not contain a slug.");
14686
- return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context`, { headers: { Accept: "application/json" } }).then(async (response) => {
14687
- if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
14688
- return response.json();
14689
- });
14690
- }
14691
- async request(path, init = {}, authenticated = true) {
14692
- const headers = new Headers(init.headers);
14693
- if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
14694
- 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}`);
14697
- }
14698
- const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
14699
- const value = await response.json().catch(() => null);
14700
- if (!response.ok && value?.error?.code && value.error.message) {
14701
- throw new ControlPlaneError(value.error.code, value.error.message, value.error.recovery, value.error.details, response.status);
14702
- }
14703
- if (!response.ok) throw new ControlPlaneHttpError(response.status);
14704
- return value;
14705
- }
14706
- };
14707
-
14708
14921
  // src/remote-encoding.ts
14709
14922
  async function withFrameUploadRetry(upload, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
14710
14923
  for (let attempt = 0; ; attempt++) {
@@ -14723,14 +14936,14 @@ Frame upload interrupted; retrying in ${delay / 1e3}s.
14723
14936
  }
14724
14937
  }
14725
14938
  async function submitFrameCapture(client, id, directory) {
14726
- const manifest = frameManifestSchema.parse(JSON.parse(await readFile2(join(directory, "capture-manifest.json"), "utf8")));
14939
+ const manifest = frameManifestSchema.parse(JSON.parse(await readFile2(join2(directory, "capture-manifest.json"), "utf8")));
14727
14940
  let status = await client.beginEncoding(id, manifest);
14728
14941
  if (status.status === "failed") status = await client.retryEncoding(id);
14729
14942
  if (status.status !== "uploading") {
14730
14943
  if (["expired", "cancelled"].includes(status.status)) throw new Error(`Capture ${status.status}; it cannot be retried.`);
14731
14944
  return status;
14732
14945
  }
14733
- const file2 = await open(join(directory, "frames.bin"), "r");
14946
+ const file2 = await open(join2(directory, "frames.bin"), "r");
14734
14947
  try {
14735
14948
  const total = manifest.frames.reduce((n, f) => n + f.size, 0);
14736
14949
  if ((await file2.stat()).size !== total) throw new Error("Capture bytes do not match the manifest.");
@@ -14766,22 +14979,22 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
14766
14979
 
14767
14980
  // src/cli.ts
14768
14981
  import { spawn as spawn6 } from "child_process";
14982
+ import { hostname as hostname4 } from "os";
14769
14983
  import { Command } from "commander";
14770
- import { hostname as hostname3 } from "os";
14771
14984
  import { basename, dirname as dirname2 } from "path";
14772
- import { readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
14985
+ import { readdir as readdir2, rm as rm5, stat as stat4 } from "fs/promises";
14773
14986
 
14774
14987
  // ../recorder/src/client.ts
14775
- import { request } from "http";
14776
- import { spawn } from "child_process";
14988
+ import { request as request2 } from "http";
14989
+ import { spawn as spawn2 } from "child_process";
14777
14990
 
14778
14991
  // ../recorder/src/paths.ts
14779
- import { homedir, tmpdir } from "os";
14780
- import { join as join2 } from "path";
14781
- var rillDirectory = () => process.env.RILL_HOME ?? join2(homedir(), ".rill");
14782
- var daemonSocketPath = () => process.env.RILL_SOCKET ?? join2(rillDirectory(), "recorder-v2.sock");
14783
- var daemonStatePath = () => join2(rillDirectory(), "sessions-v2.json");
14784
- var recordingTempRoot = () => process.env.RILL_TEMP ?? join2(tmpdir(), "rill-recordings");
14992
+ import { homedir as homedir2, tmpdir } from "os";
14993
+ import { join as join3 } from "path";
14994
+ var rillDirectory = () => process.env.RILL_HOME ?? join3(homedir2(), ".rill");
14995
+ var daemonSocketPath = () => process.env.RILL_SOCKET ?? join3(rillDirectory(), "recorder-v2.sock");
14996
+ var daemonStatePath = () => join3(rillDirectory(), "sessions-v2.json");
14997
+ var recordingTempRoot = () => process.env.RILL_TEMP ?? join3(tmpdir(), "rill-recordings");
14785
14998
 
14786
14999
  // ../recorder/src/client.ts
14787
15000
  async function ensureDaemon(cliEntryPath) {
@@ -14790,7 +15003,7 @@ async function ensureDaemon(cliEntryPath) {
14790
15003
  return;
14791
15004
  } catch {
14792
15005
  }
14793
- const child = spawn(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env });
15006
+ const child = spawn2(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env });
14794
15007
  child.unref();
14795
15008
  const deadline = Date.now() + 1e4;
14796
15009
  while (Date.now() < deadline) {
@@ -14815,7 +15028,7 @@ function recordingStatus(recordingId) {
14815
15028
  function callDaemon(method, path, body) {
14816
15029
  return new Promise((resolve, reject) => {
14817
15030
  const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
14818
- const outgoing = request({ socketPath: daemonSocketPath(), method, path, headers: payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : void 0 }, (incoming) => {
15031
+ const outgoing = request2({ socketPath: daemonSocketPath(), method, path, headers: payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : void 0 }, (incoming) => {
14819
15032
  const chunks = [];
14820
15033
  incoming.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
14821
15034
  incoming.on("end", () => {
@@ -14835,17 +15048,17 @@ function callDaemon(method, path, body) {
14835
15048
  }
14836
15049
 
14837
15050
  // ../recorder/src/retention.ts
14838
- import { readdir, readFile as readFile3, rm } from "fs/promises";
14839
- import { join as join3 } from "path";
15051
+ import { readdir, readFile as readFile3, rm as rm2 } from "fs/promises";
15052
+ import { join as join4 } from "path";
14840
15053
  async function purgeExpiredRemoteCaptures(root, now = Date.now()) {
14841
15054
  const removed = [];
14842
15055
  for (const id of await readdir(root).catch(() => [])) {
14843
15056
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
14844
- const directory = join3(root, id);
15057
+ const directory = join4(root, id);
14845
15058
  try {
14846
- const marker = JSON.parse(await readFile3(join3(directory, "remote-capture.json"), "utf8"));
15059
+ const marker = JSON.parse(await readFile3(join4(directory, "remote-capture.json"), "utf8"));
14847
15060
  if (typeof marker.expiresAt !== "number" || marker.expiresAt > now) continue;
14848
- await rm(directory, { recursive: true, force: true });
15061
+ await rm2(directory, { recursive: true, force: true });
14849
15062
  removed.push(id);
14850
15063
  } catch {
14851
15064
  }
@@ -14856,7 +15069,7 @@ async function hasRetainedRemoteCaptures(root) {
14856
15069
  for (const id of await readdir(root).catch(() => [])) {
14857
15070
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
14858
15071
  try {
14859
- const marker = JSON.parse(await readFile3(join3(root, id, "remote-capture.json"), "utf8"));
15072
+ const marker = JSON.parse(await readFile3(join4(root, id, "remote-capture.json"), "utf8"));
14860
15073
  if (typeof marker.expiresAt === "number" && Number.isFinite(marker.expiresAt)) return true;
14861
15074
  } catch {
14862
15075
  }
@@ -14866,8 +15079,8 @@ async function hasRetainedRemoteCaptures(root) {
14866
15079
 
14867
15080
  // ../recorder/src/daemon.ts
14868
15081
  import { createServer as createServer2 } from "http";
14869
- import { mkdir as mkdir4, readFile as readFile5, unlink, writeFile as writeFile4, rename, stat as stat2 } from "fs/promises";
14870
- import { dirname, join as join7 } from "path";
15082
+ import { mkdir as mkdir5, readFile as readFile5, unlink, writeFile as writeFile4, rename, stat as stat3 } from "fs/promises";
15083
+ import { dirname, join as join8 } from "path";
14871
15084
 
14872
15085
  // ../contracts/src/recording.ts
14873
15086
  var cursorAppearanceSchema = external_exports.object({
@@ -15028,10 +15241,10 @@ async function attachCursorOverlay(page, cdp, input) {
15028
15241
 
15029
15242
  // ../recorder/src/session.ts
15030
15243
  import { chromium } from "playwright-core";
15031
- import { spawn as spawn3 } from "child_process";
15244
+ import { spawn as spawn4 } from "child_process";
15032
15245
  import { createServer } from "net";
15033
- import { mkdir as mkdir3, mkdtemp, rm as rm2, writeFile as writeFile3 } from "fs/promises";
15034
- import { join as join6 } from "path";
15246
+ import { mkdir as mkdir4, mkdtemp, rm as rm3, writeFile as writeFile3 } from "fs/promises";
15247
+ import { join as join7 } from "path";
15035
15248
  import { tmpdir as tmpdir2 } from "os";
15036
15249
 
15037
15250
  // ../recorder/src/bundle.ts
@@ -15111,9 +15324,9 @@ function redactJson(value, depth = 0) {
15111
15324
  }
15112
15325
 
15113
15326
  // ../recorder/src/encoding.ts
15114
- import { open as open2, writeFile, mkdir } from "fs/promises";
15115
- import { spawn as spawn2 } from "child_process";
15116
- import { join as join4 } from "path";
15327
+ import { open as open2, writeFile, mkdir as mkdir2 } from "fs/promises";
15328
+ import { spawn as spawn3 } from "child_process";
15329
+ import { join as join5 } from "path";
15117
15330
  var active = 0;
15118
15331
  var waiting = [];
15119
15332
  function encodingConcurrency() {
@@ -15143,11 +15356,11 @@ async function encodeFrames(directory, manifest, output, signal) {
15143
15356
  lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
15144
15357
  }
15145
15358
  lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
15146
- await writeFile(join4(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
15359
+ await writeFile(join5(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
15147
15360
  const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
15148
15361
  if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
15149
15362
  await new Promise((resolve, reject) => {
15150
- const child = spawn2(process.env.RILL_FFMPEG ?? "ffmpeg", ["-hide_banner", "-loglevel", "error", "-protocol_whitelist", "file", "-f", "concat", "-safe", "1", "-i", "frames.ffconcat", "-vsync", "vfr", "-pix_fmt", "yuv420p", "-c:v", "libvpx-vp9", "-deadline", "good", "-cpu-used", "4", "-threads", String(threads), "-b:v", "0", "-crf", "32", "-y", output], { cwd: directory, stdio: ["ignore", "ignore", "pipe"] });
15363
+ const child = spawn3(process.env.RILL_FFMPEG ?? "ffmpeg", ["-hide_banner", "-loglevel", "error", "-protocol_whitelist", "file", "-f", "concat", "-safe", "1", "-i", "frames.ffconcat", "-vsync", "vfr", "-pix_fmt", "yuv420p", "-c:v", "libvpx-vp9", "-deadline", "good", "-cpu-used", "4", "-threads", String(threads), "-b:v", "0", "-crf", "32", "-y", output], { cwd: directory, stdio: ["ignore", "ignore", "pipe"] });
15151
15364
  let stderr = "";
15152
15365
  child.stderr.on("data", (chunk) => {
15153
15366
  stderr = (stderr + String(chunk)).slice(-2e3);
@@ -15172,8 +15385,8 @@ async function encodeFrames(directory, manifest, output, signal) {
15172
15385
  }
15173
15386
 
15174
15387
  // ../recorder/src/screencast.ts
15175
- import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile4, open as open3 } from "fs/promises";
15176
- import { join as join5 } from "path";
15388
+ import { mkdir as mkdir3, writeFile as writeFile2, readFile as readFile4, open as open3 } from "fs/promises";
15389
+ import { join as join6 } from "path";
15177
15390
  var ScreencastRecorder = class {
15178
15391
  constructor(session, directory, width, height) {
15179
15392
  this.session = session;
@@ -15188,7 +15401,7 @@ var ScreencastRecorder = class {
15188
15401
  frameBytes = 0;
15189
15402
  captureError;
15190
15403
  async start() {
15191
- await mkdir2(this.directory, { recursive: true, mode: 448 });
15404
+ await mkdir3(this.directory, { recursive: true, mode: 448 });
15192
15405
  this.session.on("Page.screencastFrame", (payload) => {
15193
15406
  const bytes = Buffer.from(payload.data, "base64");
15194
15407
  if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
@@ -15197,7 +15410,7 @@ var ScreencastRecorder = class {
15197
15410
  return;
15198
15411
  }
15199
15412
  this.frameBytes += bytes.length;
15200
- const path = join5(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
15413
+ const path = join6(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
15201
15414
  this.frameCounter += 1;
15202
15415
  const write = writeFile2(path, bytes, { mode: 384 }).then(() => {
15203
15416
  this.frames.push({ path, timestamp: payload.metadata.timestamp ?? Date.now() / 1e3 });
@@ -15221,7 +15434,7 @@ var ScreencastRecorder = class {
15221
15434
  if (!this.frames.length) throw new Error("No video frames were captured.");
15222
15435
  const ordered = this.frames.toSorted((a, b) => a.timestamp - b.timestamp);
15223
15436
  const frames = [];
15224
- const bundle = await open3(join5(this.directory, "frames.bin"), "w", 384);
15437
+ const bundle = await open3(join6(this.directory, "frames.bin"), "w", 384);
15225
15438
  try {
15226
15439
  for (const frame of ordered) {
15227
15440
  const bytes = await readFile4(frame.path);
@@ -15232,7 +15445,7 @@ var ScreencastRecorder = class {
15232
15445
  await bundle.close();
15233
15446
  }
15234
15447
  const manifest = frameManifestSchema.parse({ version: 1, durationSeconds: Math.min(600, expectedDurationSeconds), width: this.width, height: this.height, frames });
15235
- await writeFile2(join5(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
15448
+ await writeFile2(join6(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
15236
15449
  if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath);
15237
15450
  return { frameCount: this.frames.length, outputPath };
15238
15451
  }
@@ -15281,11 +15494,11 @@ var RecordingSession = class _RecordingSession {
15281
15494
  static async start(options) {
15282
15495
  const width = options.width ?? 1280;
15283
15496
  const height = options.height ?? 720;
15284
- await mkdir3(options.outputDirectory, { recursive: true, mode: 448 });
15285
- if (options.encoding === "remote") await writeFile3(join6(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
15497
+ await mkdir4(options.outputDirectory, { recursive: true, mode: 448 });
15498
+ if (options.encoding === "remote") await writeFile3(join7(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
15286
15499
  const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer });
15287
15500
  const cdpSession = await runtime.context.newCDPSession(runtime.page);
15288
- const screencast = new ScreencastRecorder(cdpSession, join6(options.outputDirectory, "frames"), width, height);
15501
+ const screencast = new ScreencastRecorder(cdpSession, join7(options.outputDirectory, "frames"), width, height);
15289
15502
  const session = new _RecordingSession(options, { ...runtime, screencast });
15290
15503
  try {
15291
15504
  await session.attachDiagnostics(options.captureNetworkBodies ?? false);
@@ -15317,8 +15530,8 @@ var RecordingSession = class _RecordingSession {
15317
15530
  if (this.maxTimer) clearTimeout(this.maxTimer);
15318
15531
  const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
15319
15532
  const outputDirectory = this.outputDirectory;
15320
- const videoPath = join6(outputDirectory, "recording.webm");
15321
- const diagnosticsPath = join6(outputDirectory, "diagnostics-v1.ndjson.gz");
15533
+ const videoPath = join7(outputDirectory, "recording.webm");
15534
+ const diagnosticsPath = join7(outputDirectory, "diagnostics-v1.ndjson.gz");
15322
15535
  const bundle = {
15323
15536
  schemaVersion: 1,
15324
15537
  capturePolicyVersion: "safe-default-v1",
@@ -15341,13 +15554,13 @@ var RecordingSession = class _RecordingSession {
15341
15554
  if (this.browserProcess) {
15342
15555
  await this.browser.close().catch(() => void 0);
15343
15556
  await terminateBrowser(this.browserProcess);
15344
- if (this.profileDirectory) await rm2(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
15557
+ if (this.profileDirectory) await rm3(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
15345
15558
  }
15346
15559
  }
15347
15560
  const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
15348
15561
  const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
15349
15562
  const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
15350
- return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory: join6(outputDirectory, "frames") } : {}, diagnosticsPath, durationSeconds, stoppedReason: reason, diagnostics: { consoleErrors, failedRequests, httpErrors, truncated: this.truncated, bodyCaptureEnabled: this.captureNetworkBodies } };
15563
+ return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory: join7(outputDirectory, "frames") } : {}, diagnosticsPath, durationSeconds, stoppedReason: reason, diagnostics: { consoleErrors, failedRequests, httpErrors, truncated: this.truncated, bodyCaptureEnabled: this.captureNetworkBodies } };
15351
15564
  }
15352
15565
  captureNetworkBodies = false;
15353
15566
  encoding = "local";
@@ -15399,34 +15612,34 @@ var RecordingSession = class _RecordingSession {
15399
15612
  this.consoleEvents.push(event);
15400
15613
  pushTimeline({ offsetMs: event.offsetMs, type: "exception", label: event.text.slice(0, 160), severity: "error" });
15401
15614
  });
15402
- this.page.on("request", (request2) => {
15615
+ this.page.on("request", (request3) => {
15403
15616
  if (this.diagnosticsPaused) return;
15404
15617
  if (this.networkEvents.length >= 5e3) {
15405
15618
  this.truncated = true;
15406
15619
  return;
15407
15620
  }
15408
15621
  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"]);
15622
+ this.requestStart.set(request3, started);
15623
+ const event = { id: `net_${this.networkEvents.length + 1}`, offsetMs: started, method: request3.method(), url: sanitizeUrl(request3.url()), resourceType: request3.resourceType() };
15624
+ if (captureNetworkBodies && request3.postData()) {
15625
+ const redacted = redactBody(request3.postData(), request3.headers()["content-type"]);
15413
15626
  event.requestBody = redacted.body;
15414
15627
  event.bodyOmittedReason = redacted.omittedReason;
15415
15628
  if (redacted.body) this.bodyBytes += Buffer.byteLength(redacted.body);
15416
15629
  }
15417
15630
  this.networkEvents.push(event);
15418
- this.requestEvents.set(request2, event);
15631
+ this.requestEvents.set(request3, event);
15419
15632
  });
15420
15633
  this.page.on("response", (response) => this.updateResponse(response));
15421
- this.page.on("requestfailed", (request2) => {
15422
- const event = this.requestEvents.get(request2);
15634
+ this.page.on("requestfailed", (request3) => {
15635
+ const event = this.requestEvents.get(request3);
15423
15636
  if (!event) return;
15424
- event.failureReason = request2.failure()?.errorText ?? "request_failed";
15425
- event.durationMs = Math.max(0, offset() - (this.requestStart.get(request2) ?? offset()));
15637
+ event.failureReason = request3.failure()?.errorText ?? "request_failed";
15638
+ event.durationMs = Math.max(0, offset() - (this.requestStart.get(request3) ?? offset()));
15426
15639
  pushTimeline({ offsetMs: event.offsetMs, type: "network", label: `${event.method} ${event.url} \u2192 ${event.failureReason}`, severity: "error" });
15427
15640
  });
15428
- this.page.on("requestfinished", async (request2) => {
15429
- const response = await request2.response();
15641
+ this.page.on("requestfinished", async (request3) => {
15642
+ const response = await request3.response();
15430
15643
  if (response) await this.captureResponseBody(response);
15431
15644
  });
15432
15645
  }
@@ -15459,7 +15672,7 @@ var RecordingSession = class _RecordingSession {
15459
15672
  };
15460
15673
  async function launchManaged(options) {
15461
15674
  const port = await getAvailablePort();
15462
- const profileDirectory = await mkdtemp(join6(tmpdir2(), "rill-chrome-"));
15675
+ const profileDirectory = await mkdtemp(join7(tmpdir2(), "rill-chrome-"));
15463
15676
  const executable = findChromeExecutable();
15464
15677
  const args = [
15465
15678
  `--remote-debugging-port=${port}`,
@@ -15477,7 +15690,7 @@ async function launchManaged(options) {
15477
15690
  "about:blank"
15478
15691
  ];
15479
15692
  const env = options.proxyServer ? Object.fromEntries(Object.entries(process.env).filter(([key]) => ["PATH", "HOME", "TMPDIR", "DISPLAY", "LANG", "LD_LIBRARY_PATH", "XDG_RUNTIME_DIR"].includes(key))) : process.env;
15480
- const browserProcess = spawn3(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, env });
15693
+ const browserProcess = spawn4(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, env });
15481
15694
  const cdpUrl = `http://127.0.0.1:${port}`;
15482
15695
  try {
15483
15696
  await waitForCdp(cdpUrl, browserProcess);
@@ -15488,7 +15701,7 @@ async function launchManaged(options) {
15488
15701
  return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
15489
15702
  } catch (error51) {
15490
15703
  await terminateBrowser(browserProcess);
15491
- await rm2(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
15704
+ await rm3(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
15492
15705
  throw error51;
15493
15706
  }
15494
15707
  }
@@ -15539,14 +15752,14 @@ var sessions = /* @__PURE__ */ new Map();
15539
15752
  var lastActivity = Date.now();
15540
15753
  async function runDaemon() {
15541
15754
  const socketPath = daemonSocketPath();
15542
- await mkdir4(dirname(socketPath), { recursive: true, mode: 448 });
15543
- await mkdir4(recordingTempRoot(), { recursive: true, mode: 448 });
15755
+ await mkdir5(dirname(socketPath), { recursive: true, mode: 448 });
15756
+ await mkdir5(recordingTempRoot(), { recursive: true, mode: 448 });
15544
15757
  await restoreRecoverableSessions();
15545
15758
  await purgeExpiredCaptures();
15546
15759
  await unlink(socketPath).catch((error51) => {
15547
15760
  if (error51.code !== "ENOENT") throw error51;
15548
15761
  });
15549
- const server = createServer2((request2, response) => void route(request2, response));
15762
+ const server = createServer2((request3, response) => void route(request3, response));
15550
15763
  server.listen(socketPath, () => process.platform !== "win32" && import("fs/promises").then(({ chmod }) => chmod(socketPath, 384)));
15551
15764
  const idleTimer = setInterval(() => {
15552
15765
  void (async () => {
@@ -15565,16 +15778,16 @@ async function runDaemon() {
15565
15778
  process.once("SIGTERM", close);
15566
15779
  process.once("SIGINT", close);
15567
15780
  }
15568
- async function route(request2, response) {
15781
+ async function route(request3, response) {
15569
15782
  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") {
15783
+ const url2 = new URL(request3.url ?? "/", "http://daemon.local");
15784
+ if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid });
15785
+ if (request3.method === "POST" && url2.pathname === "/sessions") {
15573
15786
  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
15787
  try {
15575
- const input = await readJson(request2);
15788
+ const input = await readJson(request3);
15576
15789
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
15577
- const outputDirectory = join7(recordingTempRoot(), input.recordingId);
15790
+ const outputDirectory = join8(recordingTempRoot(), input.recordingId);
15578
15791
  const session = await RecordingSession.start({ ...input, outputDirectory });
15579
15792
  const status = session.status();
15580
15793
  const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, startedAt: status.startedAt }, state: "recording" };
@@ -15591,8 +15804,8 @@ async function route(request2, response) {
15591
15804
  if (match) {
15592
15805
  const entry = sessions.get(decodeURIComponent(match[1]));
15593
15806
  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") {
15807
+ if (request3.method === "GET" && !match[2]) return json2(response, 200, serializeEntry(entry));
15808
+ if (request3.method === "POST" && match[2] === "stop") {
15596
15809
  if (entry.state === "ready") return json2(response, 200, serializeEntry(entry));
15597
15810
  await finalizeEntry(entry, "agent_request");
15598
15811
  return json2(response, entry.result ? 200 : 500, serializeEntry(entry));
@@ -15633,10 +15846,10 @@ async function persistState() {
15633
15846
  `, { mode: 384 });
15634
15847
  await rename(temporary, daemonStatePath());
15635
15848
  }
15636
- async function readJson(request2) {
15849
+ async function readJson(request3) {
15637
15850
  const chunks = [];
15638
15851
  let size = 0;
15639
- for await (const chunk of request2) {
15852
+ for await (const chunk of request3) {
15640
15853
  const buffer = Buffer.from(chunk);
15641
15854
  size += buffer.length;
15642
15855
  if (size > 1e6) throw new Error("Request body exceeds 1 MB.");
@@ -15652,7 +15865,7 @@ async function purgeExpiredCaptures() {
15652
15865
  const removed = await purgeExpiredRemoteCaptures(recordingTempRoot());
15653
15866
  for (const id of removed) sessions.delete(id);
15654
15867
  let changed = removed.length > 0;
15655
- for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat2(entry.result.frameDirectory).catch(() => null)) {
15868
+ for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat3(entry.result.frameDirectory).catch(() => null)) {
15656
15869
  sessions.delete(id);
15657
15870
  changed = true;
15658
15871
  }
@@ -15689,44 +15902,6 @@ function cliFailureCode(error51) {
15689
15902
  return message.match(/^([a-z][a-z0-9_]{0,79}):/)?.[1] ?? "unexpected_error";
15690
15903
  }
15691
15904
 
15692
- // src/keychain.ts
15693
- import { spawn as spawn4 } from "child_process";
15694
- var service = "rill";
15695
- async function saveCredential(apiUrl, token) {
15696
- if (process.platform === "darwin") {
15697
- await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
15698
- return;
15699
- }
15700
- if (process.platform === "linux") {
15701
- await run("secret-tool", ["store", "--label=Rill agent credential", "service", service, "api-url", apiUrl], token);
15702
- return;
15703
- }
15704
- throw new Error("OS keychain storage is not supported on this platform yet. Use RILL_TOKEN.");
15705
- }
15706
- async function loadCredential(apiUrl) {
15707
- if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
15708
- 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;
15713
- }
15714
- return null;
15715
- }
15716
- function run(command, args, input) {
15717
- return new Promise((resolve, reject) => {
15718
- const child = spawn4(command, args, { stdio: ["pipe", "pipe", "pipe"] });
15719
- const stdout = [];
15720
- const stderr = [];
15721
- child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
15722
- child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
15723
- 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}.`)));
15725
- if (input) child.stdin.end(input);
15726
- else child.stdin.end();
15727
- });
15728
- }
15729
-
15730
15905
  // src/output.ts
15731
15906
  function emit(value, pretty = false) {
15732
15907
  process.stdout.write(`${JSON.stringify(value, null, pretty ? 2 : void 0)}
@@ -15855,7 +16030,7 @@ async function runDoctor(apiUrl, overrides = {}) {
15855
16030
  } else {
15856
16031
  const token = await dependencies.loadCredential(apiUrl);
15857
16032
  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.` });
16033
+ 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
16034
  } else {
15860
16035
  try {
15861
16036
  const session = await dependencies.agentSession(apiUrl, token);
@@ -15873,10 +16048,10 @@ async function runDoctor(apiUrl, overrides = {}) {
15873
16048
  checks.push({ name: "ffmpeg", status: "skip", message: "Remote encoding does not require local FFmpeg." });
15874
16049
  try {
15875
16050
  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." });
16051
+ if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
16052
+ checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
15878
16053
  } catch (error51) {
15879
- checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "Use --encoding local until your workspace joins the private trial." });
16054
+ 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
16055
  }
15881
16056
  }
15882
16057
  return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
@@ -15990,10 +16165,10 @@ function controlPlaneUrl(environmentUrl) {
15990
16165
  async function removeLocalCapture(id) {
15991
16166
  await ensureDaemon(process.argv[1]);
15992
16167
  const local = await recordingStatus(id).catch(() => null);
15993
- if (local?.result?.frameDirectory) await rm4(dirname2(local.result.videoPath), { recursive: true, force: true });
16168
+ if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
15994
16169
  }
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 } });
16170
+ function watchEncoding(apiUrl, id) {
16171
+ const child = spawn6(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env } });
15997
16172
  child.on("error", () => void 0);
15998
16173
  child.unref();
15999
16174
  }
@@ -16009,8 +16184,9 @@ program.command("doctor").description("Validate Rill prerequisites before record
16009
16184
  });
16010
16185
  async function authorize(quiet = false) {
16011
16186
  const options = program.opts();
16187
+ 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
16188
  const client = new ControlPlaneClient(options.apiUrl, null);
16013
- const authorization = await client.createDeviceAuthorization({ deviceName: `${hostname3()} \xB7 ${process.platform}` });
16189
+ const authorization = await client.createDeviceAuthorization({ deviceName: `${hostname4()} \xB7 ${process.platform}` });
16014
16190
  process.stderr.write(`Open ${authorization.verificationUri} and enter ${authorization.userCode}
16015
16191
  `);
16016
16192
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
@@ -16029,7 +16205,17 @@ async function authorize(quiet = false) {
16029
16205
  }
16030
16206
  throw new Error("Device authorization expired.");
16031
16207
  }
16032
- program.command("login").description("Authorize Rill once.").action(async () => {
16208
+ program.command("whoami").description("Show the current verified agent identity without starting sign-in.").action(async () => {
16209
+ const options = program.opts();
16210
+ const identity = await whoami(options.apiUrl);
16211
+ emit(identity, options.pretty);
16212
+ if (!identity.authenticated) process.exitCode = 1;
16213
+ });
16214
+ 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) => {
16215
+ const options = program.opts();
16216
+ emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email) }, options.pretty);
16217
+ });
16218
+ program.command("login").description("Authorize Rill using the existing device login flow.").action(async () => {
16033
16219
  await authorize();
16034
16220
  });
16035
16221
  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 +16245,15 @@ var record2 = program.command("record").description("Run a cloud browser from a
16059
16245
  var cloudRun = program.command("run").description("Retrieve or cancel a cloud run.");
16060
16246
  for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<run-id>").action(async (id) => {
16061
16247
  const options = program.opts();
16062
- const client = new ControlPlaneClient(options.apiUrl, await loadCredential(options.apiUrl));
16248
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16063
16249
  const run2 = await (action === "status" ? client.cloudRun(id) : client.cancelCloudRun(id));
16064
16250
  if (options.json || options.pretty) emit(customerRun(run2), options.pretty);
16065
16251
  else process.stdout.write(formatCloudRun(run2));
16066
16252
  });
16067
16253
  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
16254
  const options = program.opts();
16069
- const token = await loadCredential(options.apiUrl);
16070
- const client = new ControlPlaneClient(options.apiUrl, token);
16255
+ await ensureAnonymousCredential(options.apiUrl);
16256
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16071
16257
  const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
16072
16258
  if (!["local", "remote"].includes(commandOptions.encoding)) throw new Error("Encoding must be remote or local.");
16073
16259
  if (commandOptions.encoding === "remote" && !(await client.encodingCapabilities()).enabled) throw new Error("Remote encoding is unavailable for this workspace. Use --encoding local.");
@@ -16079,7 +16265,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
16079
16265
  const local = await startRecordingWithCompensation(remote, {
16080
16266
  startLocal: async () => {
16081
16267
  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 });
16268
+ 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
16269
  },
16084
16270
  cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
16085
16271
  });
@@ -16090,7 +16276,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
16090
16276
  headed: Boolean(commandOptions.headed),
16091
16277
  attached: Boolean(commandOptions.cdpUrl)
16092
16278
  }).catch(() => void 0);
16093
- return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, maxDurationSeconds: Number(commandOptions.maxDuration) };
16279
+ 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
16280
  });
16095
16281
  emit(result, options.pretty);
16096
16282
  });
@@ -16099,8 +16285,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
16099
16285
  await ensureDaemon(process.argv[1]);
16100
16286
  const local = await stopRecording(recordingId);
16101
16287
  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);
16288
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16104
16289
  const stopped = await client.markRecordingStopped(recordingId, {
16105
16290
  durationSeconds: local.result.durationSeconds,
16106
16291
  stoppedReason: local.result.stoppedReason,
@@ -16112,11 +16297,11 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
16112
16297
  const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
16113
16298
  const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
16114
16299
  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);
16300
+ if (result2.status === "ready") await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16301
+ else watchEncoding(options.apiUrl, recordingId);
16117
16302
  return;
16118
16303
  }
16119
- const video = await stat3(local.result.videoPath);
16304
+ const video = await stat4(local.result.videoPath);
16120
16305
  const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
16121
16306
  await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
16122
16307
  const remote = commandOptions.wait === false || record2.opts().wait === false ? null : await client.waitUntilPlayable(recordingId);
@@ -16126,17 +16311,18 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
16126
16311
  recordingId,
16127
16312
  status: remote ? "ready" : "processing",
16128
16313
  shareUrl: remote?.shareUrl ?? null,
16314
+ expiresAt: remote?.expires_at ?? null,
16129
16315
  durationSeconds: local.result.durationSeconds,
16130
16316
  stoppedReason: local.result.stoppedReason,
16131
16317
  diagnostics: local.result.diagnostics,
16132
16318
  ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
16133
16319
  };
16134
16320
  emit(result, options.pretty);
16135
- if (remote) await rm4(dirname2(local.result.videoPath), { recursive: true, force: true });
16321
+ if (remote) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16136
16322
  });
16137
16323
  record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
16138
16324
  const options = program.opts();
16139
- const client = new ControlPlaneClient(options.apiUrl, await loadCredential(options.apiUrl));
16325
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16140
16326
  const current = await client.encodingStatus(recordingId).catch((error51) => {
16141
16327
  if (error51 instanceof ControlPlaneError && error51.code === "recording_not_found") return null;
16142
16328
  throw error51;
@@ -16154,17 +16340,17 @@ record2.command("retry").argument("<recording-id>").option("--no-wait").action(a
16154
16340
  const result = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
16155
16341
  emit({ schemaVersion: 1, ...result }, options.pretty);
16156
16342
  if (result.status === "ready") await removeLocalCapture(recordingId);
16157
- else watchEncoding(options.apiUrl, recordingId, await loadCredential(options.apiUrl));
16343
+ else watchEncoding(options.apiUrl, recordingId);
16158
16344
  });
16159
16345
  program.command("encoding-watch", { hidden: true }).argument("<recording-id>").action(async (id) => {
16160
16346
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) throw new Error("Invalid recording ID");
16161
16347
  const options = program.opts();
16162
- const client = new ControlPlaneClient(options.apiUrl, await loadCredential(options.apiUrl));
16348
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16163
16349
  const result = await waitForEncoding(client, id, 864e5);
16164
16350
  if (result.status === "ready") {
16165
16351
  await ensureDaemon(process.argv[1]);
16166
16352
  const local = await recordingStatus(id).catch(() => null);
16167
- if (local?.result?.frameDirectory) await rm4(dirname2(local.result.videoPath), { recursive: true, force: true });
16353
+ if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16168
16354
  }
16169
16355
  });
16170
16356
  var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
@@ -16174,7 +16360,7 @@ feedback.command("submit").argument("[recording-id]", "Omit only when setup fail
16174
16360
  if (![commandOptions.helped, commandOptions.friction, commandOptions.improvement].some((value) => value.trim())) throw new Error("validation_failed: Include at least one short feedback answer.");
16175
16361
  const options = program.opts();
16176
16362
  const token = await loadCredential(options.apiUrl);
16177
- const client = new ControlPlaneClient(options.apiUrl, token);
16363
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16178
16364
  await client.submitRecordingFeedback(recordingId ?? null, {
16179
16365
  outcome: commandOptions.outcome,
16180
16366
  whatHelped: commandOptions.helped,
@@ -16183,10 +16369,14 @@ feedback.command("submit").argument("[recording-id]", "Omit only when setup fail
16183
16369
  });
16184
16370
  emit(feedbackSubmissionReceipt(recordingId ?? null), options.pretty);
16185
16371
  });
16372
+ record2.command("list").description("List recordings visible to this connection.").action(async () => {
16373
+ const options = program.opts();
16374
+ emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).listRecordings(), options.pretty);
16375
+ });
16186
16376
  record2.command("status").argument("<recording-id>").action(async (recordingId) => {
16187
16377
  const options = program.opts();
16188
16378
  const token = await loadCredential(options.apiUrl);
16189
- const client = new ControlPlaneClient(options.apiUrl, token);
16379
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16190
16380
  try {
16191
16381
  const encoding = await client.encodingStatus(recordingId);
16192
16382
  const remote = encoding.status === "ready" ? await client.recordingStatus(recordingId) : {};
@@ -16208,24 +16398,24 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
16208
16398
  record2.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
16209
16399
  const options = program.opts();
16210
16400
  const token = await loadCredential(options.apiUrl);
16211
- const client = new ControlPlaneClient(options.apiUrl, token);
16401
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16212
16402
  emit(await client.cancelRecording(recordingId), options.pretty);
16213
16403
  });
16214
16404
  program.command("upload").argument("<video-file>").option("--title <title>").option("--no-wait").action(async (videoFile, commandOptions) => {
16215
16405
  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);
16406
+ await ensureAnonymousCredential(options.apiUrl);
16407
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16408
+ const file2 = await stat4(videoFile);
16219
16409
  const recording = await client.createRecording({ title: commandOptions.title ?? basename(videoFile), sourceUrl: null });
16220
16410
  const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file2.size, filename: basename(videoFile), uploadKind: "existing_video" });
16221
16411
  await client.uploadVideo(upload.uploadUrl, videoFile);
16222
16412
  const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recording.recordingId);
16223
16413
  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);
16414
+ 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
16415
  });
16226
16416
  program.command("inspect").argument("<share-url>").action(async (shareUrl) => {
16227
16417
  const options = program.opts();
16228
- emit(await new ControlPlaneClient(options.apiUrl, null).inspect(shareUrl), options.pretty);
16418
+ emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).inspect(shareUrl), options.pretty);
16229
16419
  });
16230
16420
  var drafts = program.command("drafts");
16231
16421
  drafts.command("list").action(async () => {
@@ -16241,9 +16431,9 @@ drafts.command("purge").action(async () => {
16241
16431
  let purged = 0;
16242
16432
  for (const name of names) {
16243
16433
  const path = `${root}/${name}`;
16244
- const metadata = await stat3(path);
16434
+ const metadata = await stat4(path);
16245
16435
  if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
16246
- await rm4(path, { recursive: true, force: true });
16436
+ await rm5(path, { recursive: true, force: true });
16247
16437
  purged += 1;
16248
16438
  }
16249
16439
  }
@@ -16255,7 +16445,7 @@ program.hook("preAction", async (_command, actionCommand) => {
16255
16445
  const options = program.opts();
16256
16446
  const token = await loadCredential(options.apiUrl).catch(() => null);
16257
16447
  if (!token) return;
16258
- const client = new ControlPlaneClient(options.apiUrl, token);
16448
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16259
16449
  await startCliTelemetry(client, command);
16260
16450
  });
16261
16451
  async function startCliTelemetry(client, command) {