@runalabs/rill-cli 0.1.12 → 0.1.14

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 +61 -10
  2. package/dist/cli.js +994 -346
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5,11 +5,128 @@ var __export = (target, all) => {
5
5
  __defProp(target, name, { get: all[name], enumerable: true });
6
6
  };
7
7
 
8
+ // src/windows-credentials.ts
9
+ import { spawn } from "child_process";
10
+ import { createHash, randomUUID } from "crypto";
11
+ import { homedir } from "os";
12
+ import { join, win32 } from "path";
13
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
14
+ var script = String.raw`$ErrorActionPreference='Stop'; try {
15
+ Add-Type -AssemblyName System.Security
16
+ $inputData=[Console]::In.ReadToEnd() | ConvertFrom-Json
17
+ $bytes=[Convert]::FromBase64String($inputData.data)
18
+ $entropy=[Convert]::FromBase64String($inputData.entropy)
19
+ if ($inputData.operation -eq 'protect') {
20
+ $result=[System.Security.Cryptography.ProtectedData]::Protect($bytes,$entropy,[System.Security.Cryptography.DataProtectionScope]::CurrentUser)
21
+ } elseif ($inputData.operation -eq 'unprotect') {
22
+ $result=[System.Security.Cryptography.ProtectedData]::Unprotect($bytes,$entropy,[System.Security.Cryptography.DataProtectionScope]::CurrentUser)
23
+ } else { throw 'Invalid operation' }
24
+ [Console]::Out.Write([Convert]::ToBase64String($result))
25
+ } catch { [Console]::Error.Write('Windows credential protection failed.'); exit 1 }`;
26
+ var transformWindowsSecret = async (operation, data, entropy) => {
27
+ if (process.platform !== "win32") throw new Error("Windows credential protection requires Windows.");
28
+ return new Promise((resolve, reject) => {
29
+ const executable = win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
30
+ const child = spawn(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
31
+ let output = "";
32
+ let failed = false;
33
+ const fail2 = () => {
34
+ if (failed) return;
35
+ failed = true;
36
+ child.kill();
37
+ reject(new Error("Cannot access Windows protected credentials. Your saved identity has not been replaced."));
38
+ };
39
+ const timer = setTimeout(fail2, 15e3);
40
+ child.stdout.on("data", (chunk) => {
41
+ output += chunk.toString("ascii");
42
+ if (output.length > 256 * 1024) fail2();
43
+ });
44
+ child.stderr.resume();
45
+ child.once("error", fail2);
46
+ child.stdin.once("error", fail2);
47
+ child.once("close", (code) => {
48
+ clearTimeout(timer);
49
+ if (failed) return;
50
+ if (code !== 0 || !output || !/^[A-Za-z0-9+/]+={0,2}$/.test(output)) {
51
+ fail2();
52
+ return;
53
+ }
54
+ resolve(Buffer.from(output, "base64"));
55
+ });
56
+ child.stdin.end(JSON.stringify({ operation, data: data.toString("base64"), entropy: entropy.toString("base64") }));
57
+ });
58
+ };
59
+ var credentialRoot = () => join(process.env.RILL_HOME ?? join(homedir(), ".rill"), "windows-credentials");
60
+ var key = (apiUrl) => createHash("sha256").update(apiUrl).digest();
61
+ async function saveWindowsCredential(apiUrl, value, root = credentialRoot(), transform2 = transformWindowsSecret) {
62
+ if (!value || Buffer.byteLength(value) > 64 * 1024) throw new Error("The Windows credential is empty or too large.");
63
+ const entropy = key(apiUrl), protectedBytes = await transform2("protect", Buffer.from(value, "utf8"), entropy);
64
+ if (!protectedBytes.length || protectedBytes.length > 192 * 1024) throw new Error("Windows credential protection returned invalid data.");
65
+ await mkdir(root, { recursive: true, mode: 448 });
66
+ const path = join(root, entropy.toString("hex") + ".dpapi"), temporary = path + "." + randomUUID() + ".tmp";
67
+ try {
68
+ await writeFile(temporary, JSON.stringify({ version: 1, ciphertext: protectedBytes.toString("base64") }), { mode: 384, flag: "wx" });
69
+ await rename(temporary, path);
70
+ } finally {
71
+ await rm(temporary, { force: true });
72
+ }
73
+ }
74
+ async function loadWindowsCredential(apiUrl, root = credentialRoot(), transform2 = transformWindowsSecret) {
75
+ const entropy = key(apiUrl), path = join(root, entropy.toString("hex") + ".dpapi");
76
+ try {
77
+ const info = await stat(path);
78
+ if (!info.isFile() || info.size > 256 * 1024) throw new Error("Invalid credential file.");
79
+ const stored = JSON.parse(await readFile(path, "utf8"));
80
+ if (stored.version !== 1 || typeof stored.ciphertext !== "string" || !stored.ciphertext || !/^[A-Za-z0-9+/]+={0,2}$/.test(stored.ciphertext)) throw new Error("Invalid protected credential.");
81
+ const plain = await transform2("unprotect", Buffer.from(stored.ciphertext, "base64"), entropy);
82
+ if (!plain.length || plain.length > 64 * 1024) throw new Error("Invalid credential size.");
83
+ return plain.toString("utf8");
84
+ } catch (error51) {
85
+ if (error51.code === "ENOENT") return null;
86
+ throw new Error("Cannot read Windows protected credentials. Your saved identity has not been replaced.");
87
+ }
88
+ }
89
+
90
+ // src/agent-session.ts
91
+ import { hostname, homedir as homedir2 } from "os";
92
+ import { mkdir as mkdir2, rm as rm2, stat as stat2 } from "fs/promises";
93
+ import { join as join2 } from "path";
94
+ import { createHash as createHash2, randomBytes } from "crypto";
95
+
96
+ // src/recording-guidance.ts
97
+ function cliCommandForPlatform(platform, apiUrl, ...args) {
98
+ const quote = (value) => /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : platform === "win32" ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", "'\\''")}'`;
99
+ return ["rill", "--api-url", apiUrl, ...args].map(quote).join(" ");
100
+ }
101
+ function cliCommand(apiUrl, ...args) {
102
+ return cliCommandForPlatform(process.platform, apiUrl, ...args);
103
+ }
104
+ var transportRecovery = "Unset RILL_SOCKET on Windows so Rill can use its authenticated named pipe. Use RILL_HOME to choose a separate recorder identity directory.";
105
+ function recorderTransportCheck(platform = process.platform, socketOverride = process.env.RILL_SOCKET) {
106
+ if (platform === "win32" && socketOverride) return { name: "recorder_transport", status: "fail", message: "Windows requires the authenticated recorder pipe.", recovery: transportRecovery };
107
+ return { name: "recorder_transport", status: "pass", message: platform === "win32" ? "The local recorder uses an authenticated Windows named pipe." : "The local recorder uses a Unix-domain socket." };
108
+ }
109
+ function assertRecorderTransport(platform = process.platform, socketOverride = process.env.RILL_SOCKET) {
110
+ const check2 = recorderTransportCheck(platform, socketOverride);
111
+ if (check2.status === "fail") throw Object.assign(new Error(`unsupported_platform: ${check2.message}`), { code: "unsupported_platform", recovery: check2.recovery });
112
+ }
113
+ function recordingNextStep(apiUrl, recordingId, status, options = {}) {
114
+ if (status === "uploading" && options.resumableFrames) return { nextCommand: cliCommand(apiUrl, "record", "retry", recordingId, "--no-wait"), message: "Frame upload is incomplete. If the uploader is still running, let it finish. If it exited or was interrupted, run nextCommand to resume missing parts and finalize the upload. Do not start a second uploader while the first is active." };
115
+ if (status === "captured") return { nextCommand: cliCommand(apiUrl, "record", "stop", recordingId, "--no-wait"), message: "Captured locally; upload required. This is not a playable share yet." };
116
+ if (["uploading", "queued", "encoding", "processing", "finalizing"].includes(status)) return { nextCommand: cliCommand(apiUrl, "record", "status", recordingId), message: "Finalizing recordings hold a concurrency slot. Wait for ready; cancel only if abandoning the recording." };
117
+ return {};
118
+ }
119
+ function browserAttachment(cdpUrl) {
120
+ return {
121
+ protocol: "cdp",
122
+ cdpUrl,
123
+ playwrightMethod: "chromium.connectOverCDP",
124
+ instructions: "Attach to this endpoint and use the existing browser context. A browser tool must explicitly support CDP attachment; do not drive a separate browser. Disconnect the automation client without closing the recorded browser, then run record stop.",
125
+ shellFallback: "See the shell-only Playwright example in the Rill CLI README; install playwright-core in a temporary directory outside the project."
126
+ };
127
+ }
128
+
8
129
  // 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
130
  var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
14
131
  var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
15
132
  var AgentAuthError = class extends Error {
@@ -17,6 +134,7 @@ var AgentAuthError = class extends Error {
17
134
  super(`${code}: ${message}`);
18
135
  this.code = code;
19
136
  }
137
+ code;
20
138
  };
21
139
  function parseAuthorization(value) {
22
140
  if (!value?.startsWith("{")) return null;
@@ -32,7 +150,7 @@ async function request(apiUrl, path, body, form = false) {
32
150
  redirect: "error",
33
151
  signal: AbortSignal.timeout(15e3),
34
152
  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)
153
+ body: form ? new URLSearchParams(Object.entries(body).map(([key2, value2]) => [key2, String(value2)])) : JSON.stringify(body)
36
154
  });
37
155
  const value = await response.json();
38
156
  if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
@@ -52,17 +170,7 @@ async function loadAgentCredential(apiUrl) {
52
170
  if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
53
171
  const inFlight = pending.get(apiUrl);
54
172
  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
- })();
173
+ const promise2 = withAuthorizationLock(apiUrl, () => readAgentCredential(apiUrl));
66
174
  pending.set(apiUrl, promise2);
67
175
  try {
68
176
  return await promise2;
@@ -70,20 +178,35 @@ async function loadAgentCredential(apiUrl) {
70
178
  pending.delete(apiUrl);
71
179
  }
72
180
  }
181
+ async function readAgentCredential(apiUrl) {
182
+ if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
183
+ const raw = await loadStoredCredential(apiUrl);
184
+ const saved = parseAuthorization(raw);
185
+ if (!saved) return raw;
186
+ if (saved.accessToken && (saved.accessExpiresAt ?? 0) > Date.now() + 6e4) return saved.accessToken;
187
+ if (!saved.identityAssertion && saved.anonymous && saved.registrationToken) return null;
188
+ if (!saved.identityAssertion) throw new Error(saved.connection ? `Connection is ${saved.connection.status}. Run ${cliCommand(apiUrl, "connect", "status", saved.connection.id)}.` : "Connection is awaiting approval. Run rill connect --email <your-email>.");
189
+ const tokens = await request(apiUrl, "/oauth2/token", { grant_type: JWT_GRANT, assertion: saved.identityAssertion }, true);
190
+ await save(apiUrl, withTokens(saved, tokens));
191
+ return tokens.access_token;
192
+ }
73
193
  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"));
194
+ return withAuthorizationLock(apiUrl, () => createAnonymousCredential(apiUrl));
195
+ }
196
+ async function withAuthorizationLock(apiUrl, action) {
197
+ const root = join2(homedir2(), ".rill", "auth-locks");
198
+ await mkdir2(root, { recursive: true, mode: 448 });
199
+ const lock = join2(root, createHash2("sha256").update(apiUrl).digest("hex"));
77
200
  const deadline = Date.now() + 3e4;
78
201
  while (true) {
79
202
  try {
80
- await mkdir(lock, { mode: 448 });
203
+ await mkdir2(lock, { mode: 448 });
81
204
  break;
82
205
  } catch (error51) {
83
206
  if (error51.code !== "EEXIST") throw error51;
84
- const info = await stat(lock).catch(() => null);
207
+ const info = await stat2(lock).catch(() => null);
85
208
  if (info && Date.now() - info.mtimeMs > 12e4) {
86
- await rm(lock, { recursive: true, force: true });
209
+ await rm2(lock, { recursive: true, force: true });
87
210
  continue;
88
211
  }
89
212
  if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
@@ -91,13 +214,13 @@ async function ensureAnonymousCredential(apiUrl) {
91
214
  }
92
215
  }
93
216
  try {
94
- return await createAnonymousCredential(apiUrl);
217
+ return await action();
95
218
  } finally {
96
- await rm(lock, { recursive: true, force: true });
219
+ await rm2(lock, { recursive: true, force: true });
97
220
  }
98
221
  }
99
222
  async function createAnonymousCredential(apiUrl) {
100
- const existing = await loadAgentCredential(apiUrl);
223
+ const existing = await readAgentCredential(apiUrl);
101
224
  if (existing) return existing;
102
225
  const stored = parseAuthorization(await loadStoredCredential(apiUrl));
103
226
  const initial = stored?.registrationToken ? stored : { version: 1, anonymous: true, registrationToken: randomBytes(32).toString("hex") };
@@ -106,11 +229,29 @@ async function createAnonymousCredential(apiUrl) {
106
229
  const saved = { ...initial, registrationToken: void 0, identityAssertion: registered.identity_assertion, assertionExpires: registered.assertion_expires, claimToken: registered.claim_token };
107
230
  await save(apiUrl, saved);
108
231
  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);
232
+ return await readAgentCredential(apiUrl);
110
233
  }
111
- async function connectAgent(apiUrl, email3) {
234
+ async function connectAgent(apiUrl, email3, options = {}) {
235
+ let result = await withAuthorizationLock(apiUrl, () => beginConnection(apiUrl, email3));
236
+ if (options.wait === false) return result;
237
+ while (result.status === "pending") {
238
+ await new Promise((resolve) => setTimeout(resolve, result.retryAfterSeconds * 1e3));
239
+ result = await pollConnection(apiUrl, result.authorizationId);
240
+ }
241
+ if (result.status === "expired") throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again using the same saved identity.");
242
+ if (result.status === "denied") throw new AgentAuthError("access_denied", "Connection request was denied or revoked. Run rill connect again only when ready to request approval.");
243
+ return result;
244
+ }
245
+ async function beginConnection(apiUrl, email3) {
112
246
  if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
113
247
  let saved = parseAuthorization(await loadStoredCredential(apiUrl));
248
+ if (saved?.connection?.status === "pending" && saved.connection.expiresAt > Date.now()) {
249
+ if (saved.connection.email !== email3) throw new Error("A connection for another email is pending. Let it expire before starting a different connection.");
250
+ process.stderr.write(`Open ${saved.connection.verificationUri}
251
+ Sign in as ${email3} and enter ${saved.connection.userCode}. Approve only the request you started.
252
+ `);
253
+ return connectionView(apiUrl, saved);
254
+ }
114
255
  let claim;
115
256
  if (saved?.claimToken && saved.anonymous) {
116
257
  try {
@@ -130,32 +271,72 @@ async function connectAgent(apiUrl, email3) {
130
271
  process.stderr.write(`Open ${claim.verification_uri}
131
272
  Sign in as ${email3} and enter ${claim.user_code}. Approve only the request you started.
132
273
  `);
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
- }
274
+ saved = { ...saved, connection: {
275
+ id: randomBytes(16).toString("hex"),
276
+ email: email3,
277
+ verificationUri: claim.verification_uri,
278
+ userCode: claim.user_code,
279
+ expiresAt: Date.now() + claim.expires_in * 1e3,
280
+ interval: claim.interval,
281
+ nextPollAt: Date.now() + claim.interval * 1e3,
282
+ status: "pending"
283
+ } };
284
+ await save(apiUrl, saved);
285
+ return connectionView(apiUrl, saved);
286
+ }
287
+ function connectionView(apiUrl, saved) {
288
+ const connection = saved.connection;
289
+ return {
290
+ status: connection.status,
291
+ authorizationId: connection.id,
292
+ apiUrl,
293
+ expiresAt: connection.status === "authenticated" ? saved.assertionExpires : new Date(connection.expiresAt).toISOString(),
294
+ retryAfterSeconds: connection.status === "pending" ? Math.max(0, Math.ceil((connection.nextPollAt - Date.now()) / 1e3)) : 0,
295
+ ...connection.status === "pending" ? {
296
+ verificationUri: connection.verificationUri,
297
+ userCode: connection.userCode,
298
+ nextCommand: cliCommand(apiUrl, "connect", "status", connection.id)
299
+ } : connection.status === "authenticated" ? {} : { nextCommand: cliCommand(apiUrl, "connect", "--email", connection.email) }
300
+ };
301
+ }
302
+ async function pollConnection(apiUrl, authorizationId) {
303
+ return withAuthorizationLock(apiUrl, () => pollSavedConnection(apiUrl, authorizationId));
304
+ }
305
+ async function pollSavedConnection(apiUrl, authorizationId) {
306
+ if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before polling a local connection.");
307
+ let saved = parseAuthorization(await loadStoredCredential(apiUrl));
308
+ if (!saved?.connection || saved.connection.id !== authorizationId) throw new AgentAuthError("authorization_not_found", "No matching saved connection for this API URL. Use the latest connect response.");
309
+ const connection = saved.connection;
310
+ if (connection.status !== "pending") return connectionView(apiUrl, saved);
311
+ if (Date.now() >= connection.expiresAt) {
312
+ connection.status = "expired";
313
+ await save(apiUrl, saved);
314
+ return connectionView(apiUrl, saved);
315
+ }
316
+ if (Date.now() < connection.nextPollAt) return connectionView(apiUrl, saved);
317
+ connection.nextPollAt = Date.now() + connection.interval * 1e3;
318
+ await save(apiUrl, saved);
319
+ try {
320
+ const tokens = await request(apiUrl, "/oauth2/token", { grant_type: CLAIM_GRANT, claim_token: saved.claimToken }, true);
321
+ saved = withTokens({ version: 1, anonymous: false, connection: { ...connection, status: "authenticated", userCode: "" } }, tokens);
322
+ } catch (error51) {
323
+ if (!(error51 instanceof AgentAuthError)) throw error51;
324
+ if (error51.code === "expired_token" || error51.code === "claim_expired") connection.status = "expired";
325
+ else if (error51.code === "access_denied" || error51.code === "claim_revoked") connection.status = "denied";
326
+ else if (error51.code === "slow_down") {
327
+ connection.interval += 5;
328
+ connection.nextPollAt = Date.now() + connection.interval * 1e3;
329
+ } else if (error51.code !== "authorization_pending") throw error51;
151
330
  }
152
- throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again to restart the approval attempt using the same saved claim token.");
331
+ await save(apiUrl, saved);
332
+ return connectionView(apiUrl, saved);
153
333
  }
154
334
 
155
335
  // src/keychain.ts
156
- import { spawn } from "child_process";
336
+ import { spawn as spawn2 } from "child_process";
157
337
  var service = "rill";
158
338
  async function saveCredential(apiUrl, token) {
339
+ if (process.platform === "win32") return saveWindowsCredential(apiUrl, token);
159
340
  if (process.platform === "darwin") {
160
341
  await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
161
342
  return;
@@ -170,6 +351,7 @@ function loadCredential(apiUrl) {
170
351
  return loadAgentCredential(apiUrl);
171
352
  }
172
353
  async function loadStoredCredential(apiUrl) {
354
+ if (process.platform === "win32") return loadWindowsCredential(apiUrl);
173
355
  try {
174
356
  if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
175
357
  if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
@@ -183,7 +365,7 @@ async function loadStoredCredential(apiUrl) {
183
365
  }
184
366
  function run(command, args, input) {
185
367
  return new Promise((resolve, reject) => {
186
- const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
368
+ const child = spawn2(command, args, { stdio: ["pipe", "pipe", "pipe"] });
187
369
  const stdout = [];
188
370
  const stderr = [];
189
371
  child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
@@ -197,7 +379,7 @@ function run(command, args, input) {
197
379
 
198
380
  // src/control-plane.ts
199
381
  import { createReadStream } from "fs";
200
- import { readFile, stat as stat2 } from "fs/promises";
382
+ import { readFile as readFile2, stat as stat3 } from "fs/promises";
201
383
  import { Upload } from "tus-js-client";
202
384
  var ControlPlaneError = class extends Error {
203
385
  constructor(code, message, recovery, details, httpStatus) {
@@ -208,6 +390,10 @@ var ControlPlaneError = class extends Error {
208
390
  this.httpStatus = httpStatus;
209
391
  this.name = "ControlPlaneError";
210
392
  }
393
+ code;
394
+ recovery;
395
+ details;
396
+ httpStatus;
211
397
  };
212
398
  var ControlPlaneHttpError = class extends Error {
213
399
  constructor(httpStatus) {
@@ -215,12 +401,18 @@ var ControlPlaneHttpError = class extends Error {
215
401
  this.httpStatus = httpStatus;
216
402
  this.name = "ControlPlaneHttpError";
217
403
  }
404
+ httpStatus;
218
405
  };
219
406
  var ControlPlaneClient = class {
220
407
  constructor(apiUrl, token) {
221
408
  this.apiUrl = apiUrl;
222
409
  this.token = token;
223
410
  }
411
+ apiUrl;
412
+ token;
413
+ recordingStatusCommand(id) {
414
+ return cliCommand(this.apiUrl, "record", "status", id);
415
+ }
224
416
  encodingCapabilities() {
225
417
  return this.request("/api/encoding/capabilities");
226
418
  }
@@ -299,17 +491,18 @@ var ControlPlaneClient = class {
299
491
  return { ...recording, shareUrl: new URL(recording.shareUrl, this.apiUrl).toString() };
300
492
  }
301
493
  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)}`);
494
+ process.stderr.write("\rUpload complete; waiting for playable video. ");
303
495
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
304
496
  intervalMs = Math.min(5e3, Math.round(intervalMs * 1.4));
305
497
  }
306
- throw new Error("processing_failed: Timed out waiting for the recording to become playable. Run `rill record status <recording-id>` to continue checking.");
498
+ throw new ControlPlaneError("processing_pending", "Upload complete; the recording is not playable yet.", `Run ${cliCommand(this.apiUrl, "record", "status", recordingId)} to continue checking.`, { recordingId, nextCommand: cliCommand(this.apiUrl, "record", "status", recordingId) });
307
499
  }
308
500
  async uploadVideo(uploadUrl, filePath) {
309
- const file2 = await stat2(filePath);
501
+ const file2 = await stat3(filePath);
310
502
  if (uploadUrl.startsWith(this.apiUrl)) {
311
503
  const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
312
504
  if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
505
+ process.stderr.write("Upload complete; playback processing may continue.\n");
313
506
  return;
314
507
  }
315
508
  await new Promise((resolve, reject) => {
@@ -324,10 +517,10 @@ var ControlPlaneClient = class {
324
517
  });
325
518
  upload.start();
326
519
  });
327
- process.stderr.write("\n");
520
+ process.stderr.write("\nUpload complete; playback processing may continue.\n");
328
521
  }
329
522
  async uploadDiagnostics(recordingId, filePath, summary) {
330
- const body = await readFile(filePath);
523
+ const body = await readFile2(filePath);
331
524
  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
525
  }
333
526
  listRecordings() {
@@ -339,18 +532,49 @@ var ControlPlaneClient = class {
339
532
  pollDeviceAuthorization(deviceCode) {
340
533
  return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
341
534
  }
342
- inspect(shareUrl) {
535
+ async inspect(shareUrl, cursor) {
343
536
  if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
344
537
  const url2 = new URL(shareUrl);
345
538
  const slug = url2.pathname.split("/").filter(Boolean).at(-1);
346
539
  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) => {
540
+ return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`, { headers: await this.reviewHeaders(url2.origin) }).then(async (response) => {
348
541
  if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
349
542
  return response.json();
350
543
  });
351
544
  }
545
+ async reviewHeaders(origin) {
546
+ const headers = new Headers({ Accept: "application/json" });
547
+ if (origin !== new URL(this.apiUrl).origin) return headers;
548
+ const token = typeof this.token === "function" ? await this.token() : this.token;
549
+ if (token) headers.set("Authorization", `Bearer ${token}`);
550
+ this.identifyAgent(headers);
551
+ const id = process.env.RILL_AGENT_ID;
552
+ if (id && /^[a-zA-Z0-9_-]{8,80}$/.test(id)) headers.set("x-rill-agent-id", id);
553
+ if (id && process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1") headers.set("x-rill-analytics", "1");
554
+ return headers;
555
+ }
556
+ identifyAgent(headers) {
557
+ if (process.env.DO_NOT_TRACK === "1" || process.env.RILL_ANONYMOUS_ANALYTICS === "0") headers.set("x-rill-analytics", "0");
558
+ const client = process.env.RILL_AGENT_CLIENT || (process.env.CODEX_THREAD_ID ? "Codex" : process.env.CLAUDECODE ? "Claude" : process.env.CURSOR_TRACE_ID ? "Cursor" : void 0);
559
+ if (client) headers.set("x-rill-agent-client", client.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 80));
560
+ if (process.env.RILL_AGENT_NAME) headers.set("x-rill-agent-name", process.env.RILL_AGENT_NAME.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 48));
561
+ }
562
+ reviewPath(shareUrl) {
563
+ const url2 = new URL(shareUrl);
564
+ if (url2.origin !== new URL(this.apiUrl).origin) throw new Error("The share URL must match --api-url before sending agent credentials.");
565
+ const match = /^\/s\/([^/]+)\/?$/.exec(url2.pathname);
566
+ if (!match) throw new Error("Expected a Rill /s/<slug> review link.");
567
+ return `/api/review/shares/${encodeURIComponent(decodeURIComponent(match[1]))}`;
568
+ }
569
+ reply(shareUrl, discussionId, body, requestId) {
570
+ return this.request(`${this.reviewPath(shareUrl)}/discussions/${encodeURIComponent(discussionId)}/replies`, { method: "POST", body: JSON.stringify({ body, requestId }) });
571
+ }
572
+ editReply(shareUrl, commentId, body) {
573
+ return this.request(`${this.reviewPath(shareUrl)}/comments/${encodeURIComponent(commentId)}`, { method: "PATCH", body: JSON.stringify({ body }) });
574
+ }
352
575
  async request(path, init = {}, authenticated = true) {
353
576
  const headers = new Headers(init.headers);
577
+ if (authenticated) this.identifyAgent(headers);
354
578
  if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
355
579
  if (authenticated) {
356
580
  const token = typeof this.token === "function" ? await this.token() : this.token;
@@ -360,7 +584,11 @@ var ControlPlaneClient = class {
360
584
  const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
361
585
  const value = await response.json().catch(() => null);
362
586
  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);
587
+ const quota = value.error.details?.quota;
588
+ const blockingId = quota?.blockingRecordingId;
589
+ const concurrency = quota?.type === "credential_concurrency" || quota?.type === "workspace_concurrency";
590
+ const recovery = value.error.code === "quota_exceeded" && concurrency && blockingId ? `Run ${cliCommand(this.apiUrl, "record", "status", blockingId)}. Uploading and processing recordings hold a concurrency slot: wait for ready. If captured locally, run record stop to upload. Cancel only an abandoned recording.` : value.error.recovery;
591
+ throw new ControlPlaneError(value.error.code, value.error.message, recovery, value.error.details, response.status);
364
592
  }
365
593
  if (!response.ok) throw new ControlPlaneHttpError(response.status);
366
594
  return value;
@@ -386,8 +614,8 @@ async function whoami(apiUrl) {
386
614
  }
387
615
 
388
616
  // src/remote-encoding.ts
389
- import { open, readFile as readFile2 } from "fs/promises";
390
- import { join as join2 } from "path";
617
+ import { open, readFile as readFile3 } from "fs/promises";
618
+ import { join as join3 } from "path";
391
619
 
392
620
  // ../../node_modules/zod/v4/classic/external.js
393
621
  var external_exports = {};
@@ -1111,9 +1339,9 @@ function floatSafeRemainder(val, step) {
1111
1339
  return ratio - roundedRatio;
1112
1340
  }
1113
1341
  var EVALUATING = /* @__PURE__ */ Symbol("evaluating");
1114
- function defineLazy(object2, key, getter) {
1342
+ function defineLazy(object2, key2, getter) {
1115
1343
  let value = void 0;
1116
- Object.defineProperty(object2, key, {
1344
+ Object.defineProperty(object2, key2, {
1117
1345
  get() {
1118
1346
  if (value === EVALUATING) {
1119
1347
  return void 0;
@@ -1125,7 +1353,7 @@ function defineLazy(object2, key, getter) {
1125
1353
  return value;
1126
1354
  },
1127
1355
  set(v) {
1128
- Object.defineProperty(object2, key, {
1356
+ Object.defineProperty(object2, key2, {
1129
1357
  value: v
1130
1358
  // configurable: true,
1131
1359
  });
@@ -1158,11 +1386,11 @@ function cloneDef(schema) {
1158
1386
  function getElementAtPath(obj, path) {
1159
1387
  if (!path)
1160
1388
  return obj;
1161
- return path.reduce((acc, key) => acc?.[key], obj);
1389
+ return path.reduce((acc, key2) => acc?.[key2], obj);
1162
1390
  }
1163
1391
  function promiseAllObject(promisesObj) {
1164
1392
  const keys = Object.keys(promisesObj);
1165
- const promises = keys.map((key) => promisesObj[key]);
1393
+ const promises = keys.map((key2) => promisesObj[key2]);
1166
1394
  return Promise.all(promises).then((results) => {
1167
1395
  const resolvedObj = {};
1168
1396
  for (let i = 0; i < keys.length; i++) {
@@ -1234,8 +1462,8 @@ function shallowClone(o) {
1234
1462
  }
1235
1463
  function numKeys(data) {
1236
1464
  let keyCount = 0;
1237
- for (const key in data) {
1238
- if (Object.prototype.hasOwnProperty.call(data, key)) {
1465
+ for (const key2 in data) {
1466
+ if (Object.prototype.hasOwnProperty.call(data, key2)) {
1239
1467
  keyCount++;
1240
1468
  }
1241
1469
  }
@@ -1385,13 +1613,13 @@ function pick(schema, mask) {
1385
1613
  const def = mergeDefs(schema._zod.def, {
1386
1614
  get shape() {
1387
1615
  const newShape = {};
1388
- for (const key in mask) {
1389
- if (!(key in currDef.shape)) {
1390
- throw new Error(`Unrecognized key: "${key}"`);
1616
+ for (const key2 in mask) {
1617
+ if (!(key2 in currDef.shape)) {
1618
+ throw new Error(`Unrecognized key: "${key2}"`);
1391
1619
  }
1392
- if (!mask[key])
1620
+ if (!mask[key2])
1393
1621
  continue;
1394
- newShape[key] = currDef.shape[key];
1622
+ newShape[key2] = currDef.shape[key2];
1395
1623
  }
1396
1624
  assignProp(this, "shape", newShape);
1397
1625
  return newShape;
@@ -1410,13 +1638,13 @@ function omit(schema, mask) {
1410
1638
  const def = mergeDefs(schema._zod.def, {
1411
1639
  get shape() {
1412
1640
  const newShape = { ...schema._zod.def.shape };
1413
- for (const key in mask) {
1414
- if (!(key in currDef.shape)) {
1415
- throw new Error(`Unrecognized key: "${key}"`);
1641
+ for (const key2 in mask) {
1642
+ if (!(key2 in currDef.shape)) {
1643
+ throw new Error(`Unrecognized key: "${key2}"`);
1416
1644
  }
1417
- if (!mask[key])
1645
+ if (!mask[key2])
1418
1646
  continue;
1419
- delete newShape[key];
1647
+ delete newShape[key2];
1420
1648
  }
1421
1649
  assignProp(this, "shape", newShape);
1422
1650
  return newShape;
@@ -1433,8 +1661,8 @@ function extend(schema, shape) {
1433
1661
  const hasChecks = checks && checks.length > 0;
1434
1662
  if (hasChecks) {
1435
1663
  const existingShape = schema._zod.def.shape;
1436
- for (const key in shape) {
1437
- if (Object.getOwnPropertyDescriptor(existingShape, key) !== void 0) {
1664
+ for (const key2 in shape) {
1665
+ if (Object.getOwnPropertyDescriptor(existingShape, key2) !== void 0) {
1438
1666
  throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
1439
1667
  }
1440
1668
  }
@@ -1490,23 +1718,23 @@ function partial(Class2, schema, mask) {
1490
1718
  const oldShape = schema._zod.def.shape;
1491
1719
  const shape = { ...oldShape };
1492
1720
  if (mask) {
1493
- for (const key in mask) {
1494
- if (!(key in oldShape)) {
1495
- throw new Error(`Unrecognized key: "${key}"`);
1721
+ for (const key2 in mask) {
1722
+ if (!(key2 in oldShape)) {
1723
+ throw new Error(`Unrecognized key: "${key2}"`);
1496
1724
  }
1497
- if (!mask[key])
1725
+ if (!mask[key2])
1498
1726
  continue;
1499
- shape[key] = Class2 ? new Class2({
1727
+ shape[key2] = Class2 ? new Class2({
1500
1728
  type: "optional",
1501
- innerType: oldShape[key]
1502
- }) : oldShape[key];
1729
+ innerType: oldShape[key2]
1730
+ }) : oldShape[key2];
1503
1731
  }
1504
1732
  } else {
1505
- for (const key in oldShape) {
1506
- shape[key] = Class2 ? new Class2({
1733
+ for (const key2 in oldShape) {
1734
+ shape[key2] = Class2 ? new Class2({
1507
1735
  type: "optional",
1508
- innerType: oldShape[key]
1509
- }) : oldShape[key];
1736
+ innerType: oldShape[key2]
1737
+ }) : oldShape[key2];
1510
1738
  }
1511
1739
  }
1512
1740
  assignProp(this, "shape", shape);
@@ -1522,22 +1750,22 @@ function required(Class2, schema, mask) {
1522
1750
  const oldShape = schema._zod.def.shape;
1523
1751
  const shape = { ...oldShape };
1524
1752
  if (mask) {
1525
- for (const key in mask) {
1526
- if (!(key in shape)) {
1527
- throw new Error(`Unrecognized key: "${key}"`);
1753
+ for (const key2 in mask) {
1754
+ if (!(key2 in shape)) {
1755
+ throw new Error(`Unrecognized key: "${key2}"`);
1528
1756
  }
1529
- if (!mask[key])
1757
+ if (!mask[key2])
1530
1758
  continue;
1531
- shape[key] = new Class2({
1759
+ shape[key2] = new Class2({
1532
1760
  type: "nonoptional",
1533
- innerType: oldShape[key]
1761
+ innerType: oldShape[key2]
1534
1762
  });
1535
1763
  }
1536
1764
  } else {
1537
- for (const key in oldShape) {
1538
- shape[key] = new Class2({
1765
+ for (const key2 in oldShape) {
1766
+ shape[key2] = new Class2({
1539
1767
  type: "nonoptional",
1540
- innerType: oldShape[key]
1768
+ innerType: oldShape[key2]
1541
1769
  });
1542
1770
  }
1543
1771
  }
@@ -3318,13 +3546,13 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
3318
3546
  return payload;
3319
3547
  };
3320
3548
  });
3321
- function handlePropertyResult(result, final, key, input, isOptionalIn, isOptionalOut) {
3322
- const isPresent = key in input;
3549
+ function handlePropertyResult(result, final, key2, input, isOptionalIn, isOptionalOut) {
3550
+ const isPresent = key2 in input;
3323
3551
  if (result.issues.length) {
3324
3552
  if (isOptionalIn && isOptionalOut && !isPresent) {
3325
3553
  return;
3326
3554
  }
3327
- final.issues.push(...prefixIssues(key, result.issues));
3555
+ final.issues.push(...prefixIssues(key2, result.issues));
3328
3556
  }
3329
3557
  if (!isPresent && !isOptionalIn) {
3330
3558
  if (!result.issues.length) {
@@ -3332,17 +3560,17 @@ function handlePropertyResult(result, final, key, input, isOptionalIn, isOptiona
3332
3560
  code: "invalid_type",
3333
3561
  expected: "nonoptional",
3334
3562
  input: void 0,
3335
- path: [key]
3563
+ path: [key2]
3336
3564
  });
3337
3565
  }
3338
3566
  return;
3339
3567
  }
3340
3568
  if (result.value === void 0) {
3341
3569
  if (isPresent) {
3342
- final.value[key] = void 0;
3570
+ final.value[key2] = void 0;
3343
3571
  }
3344
3572
  } else {
3345
- final.value[key] = result.value;
3573
+ final.value[key2] = result.value;
3346
3574
  }
3347
3575
  }
3348
3576
  function normalizeDef(def) {
@@ -3368,20 +3596,20 @@ function handleCatchall(proms, input, payload, ctx, def, inst) {
3368
3596
  const t = _catchall.def.type;
3369
3597
  const isOptionalIn = _catchall.optin === "optional";
3370
3598
  const isOptionalOut = _catchall.optout === "optional";
3371
- for (const key in input) {
3372
- if (key === "__proto__")
3599
+ for (const key2 in input) {
3600
+ if (key2 === "__proto__")
3373
3601
  continue;
3374
- if (keySet.has(key))
3602
+ if (keySet.has(key2))
3375
3603
  continue;
3376
3604
  if (t === "never") {
3377
- unrecognized.push(key);
3605
+ unrecognized.push(key2);
3378
3606
  continue;
3379
3607
  }
3380
- const r = _catchall.run({ value: input[key], issues: [] }, ctx);
3608
+ const r = _catchall.run({ value: input[key2], issues: [] }, ctx);
3381
3609
  if (r instanceof Promise) {
3382
- proms.push(r.then((r2) => handlePropertyResult(r2, payload, key, input, isOptionalIn, isOptionalOut)));
3610
+ proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalIn, isOptionalOut)));
3383
3611
  } else {
3384
- handlePropertyResult(r, payload, key, input, isOptionalIn, isOptionalOut);
3612
+ handlePropertyResult(r, payload, key2, input, isOptionalIn, isOptionalOut);
3385
3613
  }
3386
3614
  }
3387
3615
  if (unrecognized.length) {
@@ -3417,12 +3645,12 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
3417
3645
  defineLazy(inst._zod, "propValues", () => {
3418
3646
  const shape = def.shape;
3419
3647
  const propValues = {};
3420
- for (const key in shape) {
3421
- const field = shape[key]._zod;
3648
+ for (const key2 in shape) {
3649
+ const field = shape[key2]._zod;
3422
3650
  if (field.values) {
3423
- propValues[key] ?? (propValues[key] = /* @__PURE__ */ new Set());
3651
+ propValues[key2] ?? (propValues[key2] = /* @__PURE__ */ new Set());
3424
3652
  for (const v of field.values)
3425
- propValues[key].add(v);
3653
+ propValues[key2].add(v);
3426
3654
  }
3427
3655
  }
3428
3656
  return propValues;
@@ -3445,15 +3673,15 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
3445
3673
  payload.value = {};
3446
3674
  const proms = [];
3447
3675
  const shape = value.shape;
3448
- for (const key of value.keys) {
3449
- const el = shape[key];
3676
+ for (const key2 of value.keys) {
3677
+ const el = shape[key2];
3450
3678
  const isOptionalIn = el._zod.optin === "optional";
3451
3679
  const isOptionalOut = el._zod.optout === "optional";
3452
- const r = el._zod.run({ value: input[key], issues: [] }, ctx);
3680
+ const r = el._zod.run({ value: input[key2], issues: [] }, ctx);
3453
3681
  if (r instanceof Promise) {
3454
- proms.push(r.then((r2) => handlePropertyResult(r2, payload, key, input, isOptionalIn, isOptionalOut)));
3682
+ proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalIn, isOptionalOut)));
3455
3683
  } else {
3456
- handlePropertyResult(r, payload, key, input, isOptionalIn, isOptionalOut);
3684
+ handlePropertyResult(r, payload, key2, input, isOptionalIn, isOptionalOut);
3457
3685
  }
3458
3686
  }
3459
3687
  if (!catchall) {
@@ -3469,24 +3697,24 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
3469
3697
  const generateFastpass = (shape) => {
3470
3698
  const doc = new Doc(["shape", "payload", "ctx"]);
3471
3699
  const normalized = _normalized.value;
3472
- const parseStr = (key) => {
3473
- const k = esc(key);
3700
+ const parseStr = (key2) => {
3701
+ const k = esc(key2);
3474
3702
  return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
3475
3703
  };
3476
3704
  doc.write(`const input = payload.value;`);
3477
3705
  const ids = /* @__PURE__ */ Object.create(null);
3478
3706
  let counter = 0;
3479
- for (const key of normalized.keys) {
3480
- ids[key] = `key_${counter++}`;
3707
+ for (const key2 of normalized.keys) {
3708
+ ids[key2] = `key_${counter++}`;
3481
3709
  }
3482
3710
  doc.write(`const newResult = {};`);
3483
- for (const key of normalized.keys) {
3484
- const id = ids[key];
3485
- const k = esc(key);
3486
- const schema = shape[key];
3711
+ for (const key2 of normalized.keys) {
3712
+ const id = ids[key2];
3713
+ const k = esc(key2);
3714
+ const schema = shape[key2];
3487
3715
  const isOptionalIn = schema?._zod?.optin === "optional";
3488
3716
  const isOptionalOut = schema?._zod?.optout === "optional";
3489
- doc.write(`const ${id} = ${parseStr(key)};`);
3717
+ doc.write(`const ${id} = ${parseStr(key2)};`);
3490
3718
  if (isOptionalIn && isOptionalOut) {
3491
3719
  doc.write(`
3492
3720
  if (${id}.issues.length) {
@@ -3798,17 +4026,17 @@ function mergeValues(a, b) {
3798
4026
  }
3799
4027
  if (isPlainObject(a) && isPlainObject(b)) {
3800
4028
  const bKeys = Object.keys(b);
3801
- const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
4029
+ const sharedKeys = Object.keys(a).filter((key2) => bKeys.indexOf(key2) !== -1);
3802
4030
  const newObj = { ...a, ...b };
3803
- for (const key of sharedKeys) {
3804
- const sharedValue = mergeValues(a[key], b[key]);
4031
+ for (const key2 of sharedKeys) {
4032
+ const sharedValue = mergeValues(a[key2], b[key2]);
3805
4033
  if (!sharedValue.valid) {
3806
4034
  return {
3807
4035
  valid: false,
3808
- mergeErrorPath: [key, ...sharedValue.mergeErrorPath]
4036
+ mergeErrorPath: [key2, ...sharedValue.mergeErrorPath]
3809
4037
  };
3810
4038
  }
3811
- newObj[key] = sharedValue.data;
4039
+ newObj[key2] = sharedValue.data;
3812
4040
  }
3813
4041
  return { valid: true, data: newObj };
3814
4042
  }
@@ -3943,9 +4171,9 @@ var $ZodTuple = /* @__PURE__ */ $constructor("$ZodTuple", (inst, def) => {
3943
4171
  return handleTupleResults(itemResults, payload, items, input, optoutStart);
3944
4172
  };
3945
4173
  });
3946
- function getTupleOptStart(items, key) {
4174
+ function getTupleOptStart(items, key2) {
3947
4175
  for (let i = items.length - 1; i >= 0; i--) {
3948
- if (items[i]._zod[key] !== "optional")
4176
+ if (items[i]._zod[key2] !== "optional")
3949
4177
  return i + 1;
3950
4178
  }
3951
4179
  return 0;
@@ -3996,10 +4224,10 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
3996
4224
  if (values) {
3997
4225
  payload.value = {};
3998
4226
  const recordKeys = /* @__PURE__ */ new Set();
3999
- for (const key of values) {
4000
- if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
4001
- recordKeys.add(typeof key === "number" ? key.toString() : key);
4002
- const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
4227
+ for (const key2 of values) {
4228
+ if (typeof key2 === "string" || typeof key2 === "number" || typeof key2 === "symbol") {
4229
+ recordKeys.add(typeof key2 === "number" ? key2.toString() : key2);
4230
+ const keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
4003
4231
  if (keyResult instanceof Promise) {
4004
4232
  throw new Error("Async schemas not supported in object keys currently");
4005
4233
  }
@@ -4008,34 +4236,34 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
4008
4236
  code: "invalid_key",
4009
4237
  origin: "record",
4010
4238
  issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
4011
- input: key,
4012
- path: [key],
4239
+ input: key2,
4240
+ path: [key2],
4013
4241
  inst
4014
4242
  });
4015
4243
  continue;
4016
4244
  }
4017
4245
  const outKey = keyResult.value;
4018
- const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
4246
+ const result = def.valueType._zod.run({ value: input[key2], issues: [] }, ctx);
4019
4247
  if (result instanceof Promise) {
4020
4248
  proms.push(result.then((result2) => {
4021
4249
  if (result2.issues.length) {
4022
- payload.issues.push(...prefixIssues(key, result2.issues));
4250
+ payload.issues.push(...prefixIssues(key2, result2.issues));
4023
4251
  }
4024
4252
  payload.value[outKey] = result2.value;
4025
4253
  }));
4026
4254
  } else {
4027
4255
  if (result.issues.length) {
4028
- payload.issues.push(...prefixIssues(key, result.issues));
4256
+ payload.issues.push(...prefixIssues(key2, result.issues));
4029
4257
  }
4030
4258
  payload.value[outKey] = result.value;
4031
4259
  }
4032
4260
  }
4033
4261
  }
4034
4262
  let unrecognized;
4035
- for (const key in input) {
4036
- if (!recordKeys.has(key)) {
4263
+ for (const key2 in input) {
4264
+ if (!recordKeys.has(key2)) {
4037
4265
  unrecognized = unrecognized ?? [];
4038
- unrecognized.push(key);
4266
+ unrecognized.push(key2);
4039
4267
  }
4040
4268
  }
4041
4269
  if (unrecognized && unrecognized.length > 0) {
@@ -4048,18 +4276,18 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
4048
4276
  }
4049
4277
  } else {
4050
4278
  payload.value = {};
4051
- for (const key of Reflect.ownKeys(input)) {
4052
- if (key === "__proto__")
4279
+ for (const key2 of Reflect.ownKeys(input)) {
4280
+ if (key2 === "__proto__")
4053
4281
  continue;
4054
- if (!Object.prototype.propertyIsEnumerable.call(input, key))
4282
+ if (!Object.prototype.propertyIsEnumerable.call(input, key2))
4055
4283
  continue;
4056
- let keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
4284
+ let keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
4057
4285
  if (keyResult instanceof Promise) {
4058
4286
  throw new Error("Async schemas not supported in object keys currently");
4059
4287
  }
4060
- const checkNumericKey = typeof key === "string" && number.test(key) && keyResult.issues.length;
4288
+ const checkNumericKey = typeof key2 === "string" && number.test(key2) && keyResult.issues.length;
4061
4289
  if (checkNumericKey) {
4062
- const retryResult = def.keyType._zod.run({ value: Number(key), issues: [] }, ctx);
4290
+ const retryResult = def.keyType._zod.run({ value: Number(key2), issues: [] }, ctx);
4063
4291
  if (retryResult instanceof Promise) {
4064
4292
  throw new Error("Async schemas not supported in object keys currently");
4065
4293
  }
@@ -4069,30 +4297,30 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
4069
4297
  }
4070
4298
  if (keyResult.issues.length) {
4071
4299
  if (def.mode === "loose") {
4072
- payload.value[key] = input[key];
4300
+ payload.value[key2] = input[key2];
4073
4301
  } else {
4074
4302
  payload.issues.push({
4075
4303
  code: "invalid_key",
4076
4304
  origin: "record",
4077
4305
  issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
4078
- input: key,
4079
- path: [key],
4306
+ input: key2,
4307
+ path: [key2],
4080
4308
  inst
4081
4309
  });
4082
4310
  }
4083
4311
  continue;
4084
4312
  }
4085
- const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
4313
+ const result = def.valueType._zod.run({ value: input[key2], issues: [] }, ctx);
4086
4314
  if (result instanceof Promise) {
4087
4315
  proms.push(result.then((result2) => {
4088
4316
  if (result2.issues.length) {
4089
- payload.issues.push(...prefixIssues(key, result2.issues));
4317
+ payload.issues.push(...prefixIssues(key2, result2.issues));
4090
4318
  }
4091
4319
  payload.value[keyResult.value] = result2.value;
4092
4320
  }));
4093
4321
  } else {
4094
4322
  if (result.issues.length) {
4095
- payload.issues.push(...prefixIssues(key, result.issues));
4323
+ payload.issues.push(...prefixIssues(key2, result.issues));
4096
4324
  }
4097
4325
  payload.value[keyResult.value] = result.value;
4098
4326
  }
@@ -4119,15 +4347,15 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
4119
4347
  }
4120
4348
  const proms = [];
4121
4349
  payload.value = /* @__PURE__ */ new Map();
4122
- for (const [key, value] of input) {
4123
- const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
4350
+ for (const [key2, value] of input) {
4351
+ const keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
4124
4352
  const valueResult = def.valueType._zod.run({ value, issues: [] }, ctx);
4125
4353
  if (keyResult instanceof Promise || valueResult instanceof Promise) {
4126
4354
  proms.push(Promise.all([keyResult, valueResult]).then(([keyResult2, valueResult2]) => {
4127
- handleMapResult(keyResult2, valueResult2, payload, key, input, inst, ctx);
4355
+ handleMapResult(keyResult2, valueResult2, payload, key2, input, inst, ctx);
4128
4356
  }));
4129
4357
  } else {
4130
- handleMapResult(keyResult, valueResult, payload, key, input, inst, ctx);
4358
+ handleMapResult(keyResult, valueResult, payload, key2, input, inst, ctx);
4131
4359
  }
4132
4360
  }
4133
4361
  if (proms.length)
@@ -4135,10 +4363,10 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
4135
4363
  return payload;
4136
4364
  };
4137
4365
  });
4138
- function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
4366
+ function handleMapResult(keyResult, valueResult, final, key2, input, inst, ctx) {
4139
4367
  if (keyResult.issues.length) {
4140
- if (propertyKeyTypes.has(typeof key)) {
4141
- final.issues.push(...prefixIssues(key, keyResult.issues));
4368
+ if (propertyKeyTypes.has(typeof key2)) {
4369
+ final.issues.push(...prefixIssues(key2, keyResult.issues));
4142
4370
  } else {
4143
4371
  final.issues.push({
4144
4372
  code: "invalid_key",
@@ -4150,15 +4378,15 @@ function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
4150
4378
  }
4151
4379
  }
4152
4380
  if (valueResult.issues.length) {
4153
- if (propertyKeyTypes.has(typeof key)) {
4154
- final.issues.push(...prefixIssues(key, valueResult.issues));
4381
+ if (propertyKeyTypes.has(typeof key2)) {
4382
+ final.issues.push(...prefixIssues(key2, valueResult.issues));
4155
4383
  } else {
4156
4384
  final.issues.push({
4157
4385
  origin: "map",
4158
4386
  code: "invalid_element",
4159
4387
  input,
4160
4388
  inst,
4161
- key,
4389
+ key: key2,
4162
4390
  issues: valueResult.issues.map((iss) => finalizeIssue(iss, ctx, config()))
4163
4391
  });
4164
4392
  }
@@ -11919,8 +12147,8 @@ function extractDefs(ctx, schema) {
11919
12147
  if (defId)
11920
12148
  seen.defId = defId;
11921
12149
  const schema2 = seen.schema;
11922
- for (const key in schema2) {
11923
- delete schema2[key];
12150
+ for (const key2 in schema2) {
12151
+ delete schema2[key2];
11924
12152
  }
11925
12153
  schema2.$ref = ref;
11926
12154
  };
@@ -11989,20 +12217,20 @@ function finalize(ctx, schema) {
11989
12217
  Object.assign(schema2, _cached);
11990
12218
  const isParentRef = zodSchema._zod.parent === ref;
11991
12219
  if (isParentRef) {
11992
- for (const key in schema2) {
11993
- if (key === "$ref" || key === "allOf")
12220
+ for (const key2 in schema2) {
12221
+ if (key2 === "$ref" || key2 === "allOf")
11994
12222
  continue;
11995
- if (!(key in _cached)) {
11996
- delete schema2[key];
12223
+ if (!(key2 in _cached)) {
12224
+ delete schema2[key2];
11997
12225
  }
11998
12226
  }
11999
12227
  }
12000
12228
  if (refSchema.$ref && refSeen.def) {
12001
- for (const key in schema2) {
12002
- if (key === "$ref" || key === "allOf")
12229
+ for (const key2 in schema2) {
12230
+ if (key2 === "$ref" || key2 === "allOf")
12003
12231
  continue;
12004
- if (key in refSeen.def && JSON.stringify(schema2[key]) === JSON.stringify(refSeen.def[key])) {
12005
- delete schema2[key];
12232
+ if (key2 in refSeen.def && JSON.stringify(schema2[key2]) === JSON.stringify(refSeen.def[key2])) {
12233
+ delete schema2[key2];
12006
12234
  }
12007
12235
  }
12008
12236
  }
@@ -12014,11 +12242,11 @@ function finalize(ctx, schema) {
12014
12242
  if (parentSeen?.schema.$ref) {
12015
12243
  schema2.$ref = parentSeen.schema.$ref;
12016
12244
  if (parentSeen.def) {
12017
- for (const key in schema2) {
12018
- if (key === "$ref" || key === "allOf")
12245
+ for (const key2 in schema2) {
12246
+ if (key2 === "$ref" || key2 === "allOf")
12019
12247
  continue;
12020
- if (key in parentSeen.def && JSON.stringify(schema2[key]) === JSON.stringify(parentSeen.def[key])) {
12021
- delete schema2[key];
12248
+ if (key2 in parentSeen.def && JSON.stringify(schema2[key2]) === JSON.stringify(parentSeen.def[key2])) {
12249
+ delete schema2[key2];
12022
12250
  }
12023
12251
  }
12024
12252
  }
@@ -12119,8 +12347,8 @@ function isTransforming(_schema, _ctx) {
12119
12347
  return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);
12120
12348
  }
12121
12349
  if (def.type === "object") {
12122
- for (const key in def.shape) {
12123
- if (isTransforming(def.shape[key], ctx))
12350
+ for (const key2 in def.shape) {
12351
+ if (isTransforming(def.shape[key2], ctx))
12124
12352
  return true;
12125
12353
  }
12126
12354
  return false;
@@ -12409,15 +12637,15 @@ var objectProcessor = (schema, ctx, _json, params) => {
12409
12637
  json3.type = "object";
12410
12638
  json3.properties = {};
12411
12639
  const shape = def.shape;
12412
- for (const key in shape) {
12413
- json3.properties[key] = process2(shape[key], ctx, {
12640
+ for (const key2 in shape) {
12641
+ json3.properties[key2] = process2(shape[key2], ctx, {
12414
12642
  ...params,
12415
- path: [...params.path, "properties", key]
12643
+ path: [...params.path, "properties", key2]
12416
12644
  });
12417
12645
  }
12418
12646
  const allKeys = new Set(Object.keys(shape));
12419
- const requiredKeys = new Set([...allKeys].filter((key) => {
12420
- const v = def.shape[key]._zod;
12647
+ const requiredKeys = new Set([...allKeys].filter((key2) => {
12648
+ const v = def.shape[key2]._zod;
12421
12649
  if (ctx.io === "input") {
12422
12650
  return v.optin === void 0;
12423
12651
  } else {
@@ -12683,9 +12911,9 @@ function toJSONSchema(input, params) {
12683
12911
  };
12684
12912
  ctx2.external = external;
12685
12913
  for (const entry of registry2._idmap.entries()) {
12686
- const [key, schema] = entry;
12914
+ const [key2, schema] = entry;
12687
12915
  extractDefs(ctx2, schema);
12688
- schemas[key] = finalize(ctx2, schema);
12916
+ schemas[key2] = finalize(ctx2, schema);
12689
12917
  }
12690
12918
  if (Object.keys(defs).length > 0) {
12691
12919
  const defsSegment = ctx2.target === "draft-2020-12" ? "$defs" : "definitions";
@@ -13091,14 +13319,14 @@ function _installLazyMethods(inst, group, methods) {
13091
13319
  if (installed.has(group))
13092
13320
  return;
13093
13321
  installed.add(group);
13094
- for (const key in methods) {
13095
- const fn = methods[key];
13096
- Object.defineProperty(proto, key, {
13322
+ for (const key2 in methods) {
13323
+ const fn = methods[key2];
13324
+ Object.defineProperty(proto, key2, {
13097
13325
  configurable: true,
13098
13326
  enumerable: false,
13099
13327
  get() {
13100
13328
  const bound = fn.bind(this);
13101
- Object.defineProperty(this, key, {
13329
+ Object.defineProperty(this, key2, {
13102
13330
  configurable: true,
13103
13331
  writable: true,
13104
13332
  enumerable: true,
@@ -13107,7 +13335,7 @@ function _installLazyMethods(inst, group, methods) {
13107
13335
  return bound;
13108
13336
  },
13109
13337
  set(v) {
13110
- Object.defineProperty(this, key, {
13338
+ Object.defineProperty(this, key2, {
13111
13339
  configurable: true,
13112
13340
  writable: true,
13113
13341
  enumerable: true,
@@ -14495,11 +14723,11 @@ function resolveRef(ref, ctx) {
14495
14723
  }
14496
14724
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
14497
14725
  if (path[0] === defsKey) {
14498
- const key = path[1];
14499
- if (!key || !ctx.defs[key]) {
14726
+ const key2 = path[1];
14727
+ if (!key2 || !ctx.defs[key2]) {
14500
14728
  throw new Error(`Reference not found: ${ref}`);
14501
14729
  }
14502
- return ctx.defs[key];
14730
+ return ctx.defs[key2];
14503
14731
  }
14504
14732
  throw new Error(`Reference not found: ${ref}`);
14505
14733
  }
@@ -14685,9 +14913,9 @@ function convertBaseSchema(schema, ctx) {
14685
14913
  const shape = {};
14686
14914
  const properties = schema.properties || {};
14687
14915
  const requiredSet = new Set(schema.required || []);
14688
- for (const [key, propSchema] of Object.entries(properties)) {
14916
+ for (const [key2, propSchema] of Object.entries(properties)) {
14689
14917
  const propZodSchema = convertSchema(propSchema, ctx);
14690
- shape[key] = requiredSet.has(key) ? propZodSchema : propZodSchema.optional();
14918
+ shape[key2] = requiredSet.has(key2) ? propZodSchema : propZodSchema.optional();
14691
14919
  }
14692
14920
  if (schema.propertyNames) {
14693
14921
  const keySchema = convertSchema(schema.propertyNames, ctx);
@@ -14828,20 +15056,20 @@ function convertSchema(schema, ctx) {
14828
15056
  }
14829
15057
  const extraMeta = {};
14830
15058
  const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
14831
- for (const key of coreMetadataKeys) {
14832
- if (key in schema) {
14833
- extraMeta[key] = schema[key];
15059
+ for (const key2 of coreMetadataKeys) {
15060
+ if (key2 in schema) {
15061
+ extraMeta[key2] = schema[key2];
14834
15062
  }
14835
15063
  }
14836
15064
  const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
14837
- for (const key of contentMetadataKeys) {
14838
- if (key in schema) {
14839
- extraMeta[key] = schema[key];
15065
+ for (const key2 of contentMetadataKeys) {
15066
+ if (key2 in schema) {
15067
+ extraMeta[key2] = schema[key2];
14840
15068
  }
14841
15069
  }
14842
- for (const key of Object.keys(schema)) {
14843
- if (!RECOGNIZED_KEYS.has(key)) {
14844
- extraMeta[key] = schema[key];
15070
+ for (const key2 of Object.keys(schema)) {
15071
+ if (!RECOGNIZED_KEYS.has(key2)) {
15072
+ extraMeta[key2] = schema[key2];
14845
15073
  }
14846
15074
  }
14847
15075
  if (Object.keys(extraMeta).length > 0) {
@@ -14936,18 +15164,28 @@ Frame upload interrupted; retrying in ${delay / 1e3}s.
14936
15164
  }
14937
15165
  }
14938
15166
  async function submitFrameCapture(client, id, directory) {
14939
- const manifest = frameManifestSchema.parse(JSON.parse(await readFile2(join2(directory, "capture-manifest.json"), "utf8")));
15167
+ const manifest = frameManifestSchema.parse(JSON.parse(await readFile3(join3(directory, "capture-manifest.json"), "utf8")));
14940
15168
  let status = await client.beginEncoding(id, manifest);
14941
15169
  if (status.status === "failed") status = await client.retryEncoding(id);
14942
15170
  if (status.status !== "uploading") {
14943
15171
  if (["expired", "cancelled"].includes(status.status)) throw new Error(`Capture ${status.status}; it cannot be retried.`);
14944
15172
  return status;
14945
15173
  }
14946
- const file2 = await open(join2(directory, "frames.bin"), "r");
15174
+ const file2 = await open(join3(directory, "frames.bin"), "r");
14947
15175
  try {
14948
15176
  const total = manifest.frames.reduce((n, f) => n + f.size, 0);
14949
15177
  if ((await file2.stat()).size !== total) throw new Error("Capture bytes do not match the manifest.");
14950
15178
  const uploaded = new Set(status.uploadedParts);
15179
+ let acceptedBytes = [...uploaded].reduce((sum, part) => sum + (Number.isInteger(part) && part >= 0 && part < Math.ceil(total / ENCODING_PART_BYTES) ? Math.min(ENCODING_PART_BYTES, total - part * ENCODING_PART_BYTES) : 0), 0);
15180
+ let transferredBytes = 0;
15181
+ const startedAt = Date.now();
15182
+ const progress = () => {
15183
+ const seconds = (Date.now() - startedAt) / 1e3;
15184
+ const rate = seconds > 0 ? transferredBytes / seconds : 0;
15185
+ const remaining = rate > 0 ? ` \xB7 about ${Math.ceil((total - acceptedBytes) / rate)}s left` : "";
15186
+ process.stderr.write(`\ruploading frames ${Math.floor(acceptedBytes / total * 100)}% \xB7 ${(acceptedBytes / 1048576).toFixed(2)}/${(total / 1048576).toFixed(2)} MiB${rate > 0 ? ` \xB7 ${(rate / 1048576).toFixed(2)} MiB/s` : ""}${remaining}`);
15187
+ };
15188
+ progress();
14951
15189
  for (let part = 0; part < Math.ceil(total / ENCODING_PART_BYTES); part++) {
14952
15190
  if (uploaded.has(part)) continue;
14953
15191
  const bytes = Buffer.alloc(Math.min(ENCODING_PART_BYTES, total - part * ENCODING_PART_BYTES));
@@ -14958,11 +15196,14 @@ async function submitFrameCapture(client, id, directory) {
14958
15196
  offset += r.bytesRead;
14959
15197
  }
14960
15198
  await withFrameUploadRetry(() => client.uploadEncodingPart(id, part, bytes));
14961
- process.stderr.write(`\ruploading frames ${Math.round(Math.min(total, (part + 1) * ENCODING_PART_BYTES) / total * 100)}%`);
15199
+ acceptedBytes += bytes.length;
15200
+ transferredBytes += bytes.length;
15201
+ progress();
14962
15202
  }
14963
15203
  } finally {
14964
15204
  await file2.close();
14965
15205
  }
15206
+ process.stderr.write("\nFrame upload complete; submitting for encoding.\n");
14966
15207
  return client.completeEncoding(id);
14967
15208
  }
14968
15209
  async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
@@ -14974,39 +15215,133 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
14974
15215
  process.stderr.write(`\r${status.status.padEnd(20)}`);
14975
15216
  await new Promise((r) => setTimeout(r, 2e3));
14976
15217
  }
14977
- throw new Error(`Encoding continues remotely. Check rill record status ${id}.`);
15218
+ throw new ControlPlaneError("processing_pending", "Encoding continues remotely.", `Run ${client.recordingStatusCommand(id)}.`, { recordingId: id, nextCommand: client.recordingStatusCommand(id) });
14978
15219
  }
14979
15220
 
14980
15221
  // src/cli.ts
14981
- import { spawn as spawn6 } from "child_process";
14982
- import { hostname as hostname4 } from "os";
15222
+ import { spawn as spawn8 } from "child_process";
15223
+ import { hostname as hostname4, tmpdir as tmpdir3 } from "os";
14983
15224
  import { Command } from "commander";
14984
- import { basename, dirname as dirname2 } from "path";
14985
- import { readdir as readdir2, rm as rm5, stat as stat4 } from "fs/promises";
15225
+ import { basename, dirname as dirname2, join as join11 } from "path";
15226
+ import { readdir as readdir2, rm as rm7, stat as stat5 } from "fs/promises";
14986
15227
 
14987
- // ../recorder/src/client.ts
14988
- import { request as request2 } from "http";
14989
- import { spawn as spawn2 } from "child_process";
15228
+ // ../recorder/src/windows-ipc.ts
15229
+ import { spawn as spawn3 } from "child_process";
15230
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
15231
+ import { link, lstat, mkdir as mkdir3, readFile as readFile4, rm as rm3, writeFile as writeFile2 } from "fs/promises";
15232
+ import { join as join5, win32 as win322 } from "path";
14990
15233
 
14991
15234
  // ../recorder/src/paths.ts
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");
15235
+ import { homedir as homedir3, tmpdir } from "os";
15236
+ import { join as join4 } from "path";
15237
+ var rillDirectory = () => process.env.RILL_HOME ?? join4(homedir3(), ".rill");
15238
+ var daemonSocketPath = () => process.env.RILL_SOCKET ?? join4(rillDirectory(), "recorder-v2.sock");
15239
+ var daemonStatePath = () => join4(rillDirectory(), "sessions-v2.json");
15240
+ var recordingTempRoot = () => process.env.RILL_TEMP ?? join4(tmpdir(), "rill-recordings");
15241
+
15242
+ // ../recorder/src/windows-ipc.ts
15243
+ var aclScript = String.raw`$ErrorActionPreference='Stop';try {
15244
+ $path=([Console]::In.ReadToEnd() | ConvertFrom-Json).path
15245
+ $sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User
15246
+ $acl=[System.Security.AccessControl.DirectorySecurity]::new()
15247
+ $acl.SetOwner($sid)
15248
+ $acl.SetAccessRuleProtection($true,$false)
15249
+ $rule=[System.Security.AccessControl.FileSystemAccessRule]::new($sid,[System.Security.AccessControl.FileSystemRights]::FullControl,[System.Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit',[System.Security.AccessControl.PropagationFlags]::None,[System.Security.AccessControl.AccessControlType]::Allow)
15250
+ $acl.AddAccessRule($rule)
15251
+ ([System.IO.DirectoryInfo]::new($path)).SetAccessControl($acl)
15252
+ [Console]::Out.Write('secured')
15253
+ }catch{[Console]::Error.Write('Recorder directory protection failed.');exit 1}`;
15254
+ async function secureWindowsIpcDirectory(path) {
15255
+ if (process.platform !== "win32") throw new Error("Windows IPC directory protection requires Windows.");
15256
+ await new Promise((resolve, reject) => {
15257
+ const executable = win322.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15258
+ const child = spawn3(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", aclScript], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
15259
+ let output = "", failed = false;
15260
+ const fail2 = () => {
15261
+ if (failed) return;
15262
+ failed = true;
15263
+ child.kill();
15264
+ reject(new Error("Cannot secure the Windows recorder directory. Recording has not started."));
15265
+ };
15266
+ const timer = setTimeout(fail2, 15e3);
15267
+ child.once("error", fail2);
15268
+ child.stdin.once("error", fail2);
15269
+ child.stderr.resume();
15270
+ child.stdout.on("data", (chunk) => {
15271
+ output += chunk.toString("ascii");
15272
+ if (output.length > 1024) fail2();
15273
+ });
15274
+ child.once("close", (code) => {
15275
+ clearTimeout(timer);
15276
+ if (failed) return;
15277
+ if (code !== 0 || output !== "secured") {
15278
+ fail2();
15279
+ return;
15280
+ }
15281
+ resolve();
15282
+ });
15283
+ child.stdin.end(JSON.stringify({ path }));
15284
+ });
15285
+ }
15286
+ function validWindowsIpcIdentity(value) {
15287
+ const v = value;
15288
+ return v?.version === 1 && typeof v.pipe === "string" && /^\\\\\.\\pipe\\rill-v3-[a-f0-9]{64}$/.test(v.pipe) && typeof v.token === "string" && /^[a-f0-9]{64}$/.test(v.token);
15289
+ }
15290
+ function acceptsWindowsIpcToken(expected, header) {
15291
+ if (typeof header !== "string" || !/^Bearer [a-f0-9]{64}$/.test(header)) return false;
15292
+ const supplied = header.slice(7);
15293
+ return supplied.length === expected.length && timingSafeEqual(Buffer.from(supplied), Buffer.from(expected));
15294
+ }
15295
+ async function windowsIpcIdentity(directory = join5(rillDirectory(), "windows-ipc-v3"), secure = secureWindowsIpcDirectory) {
15296
+ await mkdir3(directory, { recursive: true, mode: 448 });
15297
+ if ((await lstat(directory)).isSymbolicLink()) throw new Error("Recorder directory cannot be a symbolic link.");
15298
+ await secure(directory);
15299
+ const path = join5(directory, "identity.json");
15300
+ const load = async () => {
15301
+ const info = await lstat(path);
15302
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 4096) throw new Error("Invalid recorder identity.");
15303
+ const value = JSON.parse(await readFile4(path, "utf8"));
15304
+ if (!validWindowsIpcIdentity(value)) throw new Error("Invalid recorder identity.");
15305
+ return value;
15306
+ };
15307
+ try {
15308
+ return await load();
15309
+ } catch (e) {
15310
+ if (e.code !== "ENOENT") throw new Error("Cannot read the protected recorder identity.");
15311
+ }
15312
+ const identity = { version: 1, pipe: "\\\\.\\pipe\\rill-v3-" + randomBytes2(32).toString("hex"), token: randomBytes2(32).toString("hex") };
15313
+ const temporary = join5(directory, randomBytes2(16).toString("hex") + ".tmp");
15314
+ try {
15315
+ await writeFile2(temporary, JSON.stringify(identity), { mode: 384, flag: "wx" });
15316
+ try {
15317
+ await link(temporary, path);
15318
+ } catch (e) {
15319
+ if (e.code !== "EEXIST") throw e;
15320
+ }
15321
+ return await load();
15322
+ } finally {
15323
+ await rm3(temporary, { force: true });
15324
+ }
15325
+ }
14998
15326
 
14999
15327
  // ../recorder/src/client.ts
15328
+ import { request as request2 } from "http";
15329
+ import { spawn as spawn4 } from "child_process";
15000
15330
  async function ensureDaemon(cliEntryPath) {
15001
15331
  try {
15002
15332
  await callDaemon("GET", "/health");
15003
15333
  return;
15004
15334
  } catch {
15005
15335
  }
15006
- const child = spawn2(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env });
15336
+ const child = spawn4(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env, windowsHide: true });
15337
+ let startupError;
15338
+ child.once("error", () => {
15339
+ startupError = new Error("The local recorder process could not be launched.");
15340
+ });
15007
15341
  child.unref();
15008
15342
  const deadline = Date.now() + 1e4;
15009
15343
  while (Date.now() < deadline) {
15344
+ if (startupError) throw startupError;
15010
15345
  try {
15011
15346
  await callDaemon("GET", "/health");
15012
15347
  return;
@@ -15025,10 +15360,16 @@ function stopRecording(recordingId) {
15025
15360
  function recordingStatus(recordingId) {
15026
15361
  return callDaemon("GET", `/sessions/${encodeURIComponent(recordingId)}`);
15027
15362
  }
15028
- function callDaemon(method, path, body) {
15363
+ var windowsIdentityPromise;
15364
+ async function callDaemon(method, path, body) {
15365
+ if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC. Unset it before recording.");
15366
+ const identity = process.platform === "win32" ? await (windowsIdentityPromise ??= windowsIpcIdentity().catch((error51) => {
15367
+ windowsIdentityPromise = void 0;
15368
+ throw error51;
15369
+ })) : void 0;
15029
15370
  return new Promise((resolve, reject) => {
15030
15371
  const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
15031
- const outgoing = request2({ socketPath: daemonSocketPath(), method, path, headers: payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : void 0 }, (incoming) => {
15372
+ const outgoing = request2({ socketPath: identity?.pipe ?? daemonSocketPath(), method, path, headers: { ...payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : {}, ...identity ? { Authorization: "Bearer " + identity.token } : {} } }, (incoming) => {
15032
15373
  const chunks = [];
15033
15374
  incoming.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
15034
15375
  incoming.on("end", () => {
@@ -15041,6 +15382,7 @@ function callDaemon(method, path, body) {
15041
15382
  }
15042
15383
  });
15043
15384
  });
15385
+ outgoing.setTimeout(path === "/health" ? 2e3 : 12e4, () => outgoing.destroy(new Error("Local recorder request timed out.")));
15044
15386
  outgoing.once("error", reject);
15045
15387
  if (payload) outgoing.write(payload);
15046
15388
  outgoing.end();
@@ -15048,17 +15390,17 @@ function callDaemon(method, path, body) {
15048
15390
  }
15049
15391
 
15050
15392
  // ../recorder/src/retention.ts
15051
- import { readdir, readFile as readFile3, rm as rm2 } from "fs/promises";
15052
- import { join as join4 } from "path";
15393
+ import { readdir, readFile as readFile5, rm as rm4 } from "fs/promises";
15394
+ import { join as join6 } from "path";
15053
15395
  async function purgeExpiredRemoteCaptures(root, now = Date.now()) {
15054
15396
  const removed = [];
15055
15397
  for (const id of await readdir(root).catch(() => [])) {
15056
15398
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
15057
- const directory = join4(root, id);
15399
+ const directory = join6(root, id);
15058
15400
  try {
15059
- const marker = JSON.parse(await readFile3(join4(directory, "remote-capture.json"), "utf8"));
15401
+ const marker = JSON.parse(await readFile5(join6(directory, "remote-capture.json"), "utf8"));
15060
15402
  if (typeof marker.expiresAt !== "number" || marker.expiresAt > now) continue;
15061
- await rm2(directory, { recursive: true, force: true });
15403
+ await rm4(directory, { recursive: true, force: true });
15062
15404
  removed.push(id);
15063
15405
  } catch {
15064
15406
  }
@@ -15069,7 +15411,7 @@ async function hasRetainedRemoteCaptures(root) {
15069
15411
  for (const id of await readdir(root).catch(() => [])) {
15070
15412
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
15071
15413
  try {
15072
- const marker = JSON.parse(await readFile3(join4(root, id, "remote-capture.json"), "utf8"));
15414
+ const marker = JSON.parse(await readFile5(join6(root, id, "remote-capture.json"), "utf8"));
15073
15415
  if (typeof marker.expiresAt === "number" && Number.isFinite(marker.expiresAt)) return true;
15074
15416
  } catch {
15075
15417
  }
@@ -15079,8 +15421,189 @@ async function hasRetainedRemoteCaptures(root) {
15079
15421
 
15080
15422
  // ../recorder/src/daemon.ts
15081
15423
  import { createServer as createServer2 } from "http";
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";
15424
+ import { mkdir as mkdir7, readFile as readFile7, unlink, writeFile as writeFile6, rename as rename2, stat as stat4, chmod } from "fs/promises";
15425
+ import { dirname, join as join10 } from "path";
15426
+
15427
+ // ../contracts/src/cloud-blocklist.ts
15428
+ var blockedCloudDomains = [
15429
+ // Torrent indexes and unofficial software-download destinations.
15430
+ // Explicit domains only: do not block legitimate torrent clients or all downloads.
15431
+ "yasdl.com",
15432
+ "soft98.ir",
15433
+ "downloadha.com",
15434
+ "patoghu.com",
15435
+ "thepiratebay.org",
15436
+ "1337x.to",
15437
+ "yts.mx",
15438
+ "eztvx.to",
15439
+ "fitgirl-repacks.site",
15440
+ "dodi-repacks.site",
15441
+ // Search and reference
15442
+ "google.com",
15443
+ "google.co.uk",
15444
+ "google.co.in",
15445
+ "google.com.ng",
15446
+ "google.ca",
15447
+ "google.com.au",
15448
+ "google.de",
15449
+ "google.fr",
15450
+ "google.co.jp",
15451
+ "google.com.br",
15452
+ "google.es",
15453
+ "bing.com",
15454
+ "yahoo.com",
15455
+ "duckduckgo.com",
15456
+ "baidu.com",
15457
+ "yandex.com",
15458
+ "yandex.ru",
15459
+ "wikipedia.org",
15460
+ "quora.com",
15461
+ // Social networks and media
15462
+ "reddit.com",
15463
+ "redd.it",
15464
+ "facebook.com",
15465
+ "fb.com",
15466
+ "instagram.com",
15467
+ "threads.net",
15468
+ "threads.com",
15469
+ "twitter.com",
15470
+ "x.com",
15471
+ "t.co",
15472
+ "linkedin.com",
15473
+ "tiktok.com",
15474
+ "pinterest.com",
15475
+ "snapchat.com",
15476
+ "youtube.com",
15477
+ "youtu.be",
15478
+ "twitch.tv",
15479
+ "discord.com",
15480
+ "discord.gg",
15481
+ "telegram.org",
15482
+ "t.me",
15483
+ "whatsapp.com",
15484
+ // Shopping and marketplaces
15485
+ "amazon.com",
15486
+ "amazon.co.uk",
15487
+ "amazon.ca",
15488
+ "amazon.de",
15489
+ "amazon.fr",
15490
+ "amazon.in",
15491
+ "amazon.co.jp",
15492
+ "amazon.com.au",
15493
+ "amazon.it",
15494
+ "amazon.es",
15495
+ "ebay.com",
15496
+ "ebay.co.uk",
15497
+ "etsy.com",
15498
+ "walmart.com",
15499
+ "target.com",
15500
+ "aliexpress.com",
15501
+ "alibaba.com",
15502
+ "temu.com",
15503
+ "shein.com",
15504
+ "craigslist.org",
15505
+ "facebookmarketplace.com",
15506
+ "bestbuy.com",
15507
+ "costco.com",
15508
+ "homedepot.com",
15509
+ "wayfair.com",
15510
+ "mercadolibre.com",
15511
+ "shopee.com",
15512
+ "lazada.com",
15513
+ "jiji.ng",
15514
+ "jumia.com.ng",
15515
+ "konga.com",
15516
+ "propertypro.ng",
15517
+ "property24.com",
15518
+ // Travel, property, jobs and business directories
15519
+ "airbnb.com",
15520
+ "airbnb.co.uk",
15521
+ "airbnb.ca",
15522
+ "airbnb.com.au",
15523
+ "airbnb.fr",
15524
+ "airbnb.de",
15525
+ "booking.com",
15526
+ "expedia.com",
15527
+ "tripadvisor.com",
15528
+ "agoda.com",
15529
+ "hotels.com",
15530
+ "kayak.com",
15531
+ "skyscanner.net",
15532
+ "zillow.com",
15533
+ "realtor.com",
15534
+ "rightmove.co.uk",
15535
+ "zoopla.co.uk",
15536
+ "indeed.com",
15537
+ "glassdoor.com",
15538
+ "yelp.com",
15539
+ "yellowpages.com",
15540
+ "trustpilot.com",
15541
+ "crunchbase.com",
15542
+ "g2.com",
15543
+ "capterra.com",
15544
+ "clutch.co",
15545
+ "zoominfo.com",
15546
+ "apollo.io",
15547
+ "monster.com",
15548
+ "ziprecruiter.com",
15549
+ "seek.com.au",
15550
+ "naukri.com",
15551
+ "redfin.com",
15552
+ "trulia.com",
15553
+ "apartments.com",
15554
+ "realtor.ca",
15555
+ "realestate.com.au",
15556
+ "vrbo.com",
15557
+ "trip.com",
15558
+ "trivago.com",
15559
+ "hostelworld.com"
15560
+ ];
15561
+ var blockedCloudTargetMessage = "This website is not supported for cloud runs. Use your own website or app instead.";
15562
+ function isBlockedCloudTarget(value) {
15563
+ try {
15564
+ const hostname5 = new URL(value).hostname.toLowerCase().replace(/\.+$/, "");
15565
+ return blockedCloudDomains.some((domain2) => hostname5 === domain2 || hostname5.endsWith(`.${domain2}`));
15566
+ } catch {
15567
+ return false;
15568
+ }
15569
+ }
15570
+
15571
+ // ../contracts/src/cloud-run.ts
15572
+ var publicTarget = external_exports.string().url().max(2048).refine((value) => {
15573
+ let u;
15574
+ try {
15575
+ u = new URL(value);
15576
+ } catch {
15577
+ return false;
15578
+ }
15579
+ return ["http:", "https:"].includes(u.protocol) && !u.username && !u.password && !u.port && !u.hash && u.hostname.includes(".") && !/^[0-9.]+$/.test(u.hostname) && !u.hostname.endsWith(".exe.xyz") && !u.hostname.endsWith(".local") && !u.hostname.endsWith(".internal") && !u.hostname.endsWith(".localhost");
15580
+ }, "Use a public HTTP(S) URL without credentials, fragments, or a custom port.");
15581
+ var cloudRunTarget = publicTarget.refine((value) => !isBlockedCloudTarget(value), blockedCloudTargetMessage);
15582
+ var interactionFields = {
15583
+ mode: external_exports.enum(["browse", "interact"]).optional(),
15584
+ interactionConfirmed: external_exports.boolean().optional(),
15585
+ // Legacy payload compatibility only; Interact automatically permits API destinations.
15586
+ allowedWriteOrigins: external_exports.array(cloudRunTarget.refine((v) => new URL(v).origin === v, "Use an exact origin.")).max(5).optional()
15587
+ };
15588
+ var sideEffectSchema = external_exports.object({
15589
+ action: external_exports.enum(["fill", "check", "select", "press", "click"]),
15590
+ page: external_exports.string().max(2048),
15591
+ target: external_exports.string().max(160),
15592
+ status: external_exports.enum(["performed", "unknown"])
15593
+ }).strict();
15594
+ var interactionReportFields = {
15595
+ mode: external_exports.enum(["browse", "interact"]).optional(),
15596
+ sideEffects: external_exports.array(sideEffectSchema).max(30).optional(),
15597
+ cleanup: external_exports.literal("not_attempted").optional()
15598
+ };
15599
+ var cloudSubmission = external_exports.object({ ...interactionFields, id: external_exports.string().uuid(), url: cloudRunTarget, prompt: external_exports.string().trim().min(1).max(2e3), expectation: external_exports.string().trim().min(1).max(1e3).optional(), account: external_exports.string().regex(/^[a-z0-9-]{1,60}$/).optional() }).strict();
15600
+ var siteAccountSchema = external_exports.object({
15601
+ name: external_exports.string().regex(/^[a-z0-9-]{1,60}$/),
15602
+ origin: publicTarget.refine((v) => new URL(v).protocol === "https:" && new URL(v).origin === v, "Use an exact HTTPS origin."),
15603
+ loginUrl: publicTarget.refine((v) => new URL(v).protocol === "https:").optional(),
15604
+ username: external_exports.string().min(1).max(320),
15605
+ password: external_exports.string().min(1).max(1e3)
15606
+ }).strict().refine((v) => !v.loginUrl || new URL(v.loginUrl).origin === v.origin, "Login must use the saved website origin.");
15084
15607
 
15085
15608
  // ../contracts/src/recording.ts
15086
15609
  var cursorAppearanceSchema = external_exports.object({
@@ -15121,6 +15644,7 @@ var recordingSchema = external_exports.object({
15121
15644
  })
15122
15645
  });
15123
15646
  var agentOutcomeSchema = external_exports.object({
15647
+ ...interactionReportFields,
15124
15648
  outcome: external_exports.enum(["walkthrough", "passed", "failed", "inconclusive"]),
15125
15649
  summary: external_exports.string().min(1).max(1e3),
15126
15650
  evidence: external_exports.string().max(1e3)
@@ -15208,15 +15732,15 @@ async function attachCursorOverlay(page, cdp, input) {
15208
15732
  const appearance = cursorAppearanceSchema.parse(input ?? {});
15209
15733
  if (!appearance.enabled) return async () => {
15210
15734
  };
15211
- const key = `__rillCursor_${crypto.randomUUID().replaceAll("-", "")}`;
15212
- const source = `(${installCursorOverlay.toString()})(${JSON.stringify({ ...appearance, key })})`;
15735
+ const key2 = `__rillCursor_${crypto.randomUUID().replaceAll("-", "")}`;
15736
+ const source = `(${installCursorOverlay.toString()})(${JSON.stringify({ ...appearance, key: key2 })})`;
15213
15737
  await cdp.send("Page.enable");
15214
15738
  const { identifier } = await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source });
15215
15739
  let stopped = false;
15216
- const removeFromFrame = (frame) => frame.evaluate((key2) => {
15217
- const cleanup2 = window[key2];
15740
+ const removeFromFrame = (frame) => frame.evaluate((key3) => {
15741
+ const cleanup2 = window[key3];
15218
15742
  if (typeof cleanup2 === "function") cleanup2();
15219
- }, key).catch(() => void 0);
15743
+ }, key2).catch(() => void 0);
15220
15744
  const onNavigation = (frame) => {
15221
15745
  if (stopped) return;
15222
15746
  void frame.evaluate(source).then(() => {
@@ -15241,10 +15765,10 @@ async function attachCursorOverlay(page, cdp, input) {
15241
15765
 
15242
15766
  // ../recorder/src/session.ts
15243
15767
  import { chromium } from "playwright-core";
15244
- import { spawn as spawn4 } from "child_process";
15768
+ import { spawn as spawn6 } from "child_process";
15245
15769
  import { createServer } from "net";
15246
- import { mkdir as mkdir4, mkdtemp, rm as rm3, writeFile as writeFile3 } from "fs/promises";
15247
- import { join as join7 } from "path";
15770
+ import { mkdir as mkdir6, mkdtemp, rm as rm5, writeFile as writeFile5 } from "fs/promises";
15771
+ import { join as join9 } from "path";
15248
15772
  import { tmpdir as tmpdir2 } from "os";
15249
15773
 
15250
15774
  // ../recorder/src/bundle.ts
@@ -15270,11 +15794,16 @@ async function writeBundle(bundle, outputPath) {
15270
15794
 
15271
15795
  // ../recorder/src/environment.ts
15272
15796
  import { existsSync } from "fs";
15273
- function findChromeExecutable() {
15274
- if (process.env.RILL_CHROME_PATH) return process.env.RILL_CHROME_PATH;
15275
- if (process.platform === "darwin") return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
15797
+ import { win32 as win323 } from "path";
15798
+ function findChromeExecutable(platform = process.platform, env = process.env, exists = existsSync) {
15799
+ if (env.RILL_CHROME_PATH) return env.RILL_CHROME_PATH;
15800
+ if (platform === "darwin") return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
15801
+ if (platform === "win32") {
15802
+ const candidates2 = [env.PROGRAMFILES ?? env.ProgramFiles, env["PROGRAMFILES(X86)"] ?? env["ProgramFiles(x86)"], env.LOCALAPPDATA].filter((root) => Boolean(root)).map((root) => win323.join(root, "Google", "Chrome", "Application", "chrome.exe"));
15803
+ return candidates2.find(exists) ?? win323.join(env.PROGRAMFILES ?? env.ProgramFiles ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe");
15804
+ }
15276
15805
  const candidates = ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser"];
15277
- return candidates.find((candidate) => existsSync(candidate)) ?? "google-chrome";
15806
+ return candidates.find(exists) ?? "google-chrome";
15278
15807
  }
15279
15808
 
15280
15809
  // ../recorder/src/redaction.ts
@@ -15286,7 +15815,7 @@ function sanitizeUrl(input) {
15286
15815
  url2.username = "";
15287
15816
  url2.password = "";
15288
15817
  url2.hash = "";
15289
- for (const key of [...url2.searchParams.keys()]) url2.searchParams.set(key, "[REDACTED]");
15818
+ for (const key2 of [...url2.searchParams.keys()]) url2.searchParams.set(key2, "[REDACTED]");
15290
15819
  return url2.toString();
15291
15820
  } catch {
15292
15821
  return "[invalid-url]";
@@ -15317,16 +15846,16 @@ function redactJson(value, depth = 0) {
15317
15846
  if (depth > 12) return "[TRUNCATED]";
15318
15847
  if (Array.isArray(value)) return value.slice(0, 100).map((item) => redactJson(item, depth + 1));
15319
15848
  if (value && typeof value === "object") {
15320
- return Object.fromEntries(Object.entries(value).slice(0, 200).map(([key, child]) => [key, secretKeyPattern.test(key) ? "[REDACTED]" : redactJson(child, depth + 1)]));
15849
+ return Object.fromEntries(Object.entries(value).slice(0, 200).map(([key2, child]) => [key2, secretKeyPattern.test(key2) ? "[REDACTED]" : redactJson(child, depth + 1)]));
15321
15850
  }
15322
15851
  if (typeof value === "string") return redactText(value, 8e3);
15323
15852
  return value;
15324
15853
  }
15325
15854
 
15326
15855
  // ../recorder/src/encoding.ts
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";
15856
+ import { open as open2, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
15857
+ import { spawn as spawn5 } from "child_process";
15858
+ import { join as join7 } from "path";
15330
15859
  var active = 0;
15331
15860
  var waiting = [];
15332
15861
  function encodingConcurrency() {
@@ -15334,8 +15863,22 @@ function encodingConcurrency() {
15334
15863
  if (!Number.isInteger(n) || n < 1 || n > 8) throw new Error("Invalid RILL_ENCODING_CONCURRENCY");
15335
15864
  return n;
15336
15865
  }
15337
- async function withEncodingSlot(work) {
15338
- if (active >= encodingConcurrency()) await new Promise((resolve) => waiting.push(resolve));
15866
+ async function withEncodingSlot(work, signal) {
15867
+ signal?.throwIfAborted();
15868
+ if (active >= encodingConcurrency()) await new Promise((resolve, reject) => {
15869
+ const granted = () => {
15870
+ signal?.removeEventListener("abort", abort);
15871
+ resolve();
15872
+ };
15873
+ const abort = () => {
15874
+ const i = waiting.indexOf(granted);
15875
+ if (i >= 0) waiting.splice(i, 1);
15876
+ reject(new Error("encoding_aborted"));
15877
+ };
15878
+ waiting.push(granted);
15879
+ signal?.addEventListener("abort", abort, { once: true });
15880
+ if (signal?.aborted) abort();
15881
+ });
15339
15882
  else active++;
15340
15883
  try {
15341
15884
  return await work();
@@ -15356,11 +15899,15 @@ async function encodeFrames(directory, manifest, output, signal) {
15356
15899
  lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
15357
15900
  }
15358
15901
  lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
15359
- await writeFile(join5(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
15902
+ await writeFile3(join7(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
15360
15903
  const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
15361
15904
  if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
15905
+ signal?.throwIfAborted();
15906
+ const dimensions = `scale=${manifest.width}:${manifest.height}:force_original_aspect_ratio=decrease,pad=${manifest.width}:${manifest.height}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
15907
+ const filters = output.endsWith(".mp4") ? `${dimensions},fps=30,tpad=stop_mode=clone:stop_duration=1` : dimensions;
15908
+ const codec2 = output.endsWith(".mp4") ? ["-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-movflags", "+faststart", "-fps_mode", "cfr", "-t", String(Math.min(60, manifest.durationSeconds))] : ["-c:v", "libvpx-vp9", "-deadline", "good", "-cpu-used", "4", "-b:v", "0", "-crf", "32"];
15362
15909
  await new Promise((resolve, reject) => {
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"] });
15910
+ const child = spawn5(process.env.RILL_FFMPEG ?? "ffmpeg", ["-hide_banner", "-loglevel", "error", "-protocol_whitelist", "file", "-f", "concat", "-safe", "1", "-i", "frames.ffconcat", "-vsync", "vfr", "-pix_fmt", "yuv420p", "-vf", filters, ...codec2, "-threads", String(threads), "-y", output], { cwd: directory, windowsHide: true, stdio: ["ignore", "ignore", "pipe"] });
15364
15911
  let stderr = "";
15365
15912
  child.stderr.on("data", (chunk) => {
15366
15913
  stderr = (stderr + String(chunk)).slice(-2e3);
@@ -15368,6 +15915,7 @@ async function encodeFrames(directory, manifest, output, signal) {
15368
15915
  const abort = () => child.kill("SIGKILL");
15369
15916
  const timer = setTimeout(abort, 20 * 6e4);
15370
15917
  signal?.addEventListener("abort", abort, { once: true });
15918
+ if (signal?.aborted) abort();
15371
15919
  const clean = () => {
15372
15920
  clearTimeout(timer);
15373
15921
  signal?.removeEventListener("abort", abort);
@@ -15381,12 +15929,12 @@ async function encodeFrames(directory, manifest, output, signal) {
15381
15929
  code === 0 ? resolve() : reject(new Error(`ffmpeg failed (${code}): ${stderr}`));
15382
15930
  });
15383
15931
  });
15384
- });
15932
+ }, signal);
15385
15933
  }
15386
15934
 
15387
15935
  // ../recorder/src/screencast.ts
15388
- import { mkdir as mkdir3, writeFile as writeFile2, readFile as readFile4, open as open3 } from "fs/promises";
15389
- import { join as join6 } from "path";
15936
+ import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile6, open as open3 } from "fs/promises";
15937
+ import { join as join8 } from "path";
15390
15938
  var ScreencastRecorder = class {
15391
15939
  constructor(session, directory, width, height) {
15392
15940
  this.session = session;
@@ -15394,6 +15942,10 @@ var ScreencastRecorder = class {
15394
15942
  this.width = width;
15395
15943
  this.height = height;
15396
15944
  }
15945
+ session;
15946
+ directory;
15947
+ width;
15948
+ height;
15397
15949
  frames = [];
15398
15950
  pendingWrites = /* @__PURE__ */ new Set();
15399
15951
  started = false;
@@ -15401,7 +15953,7 @@ var ScreencastRecorder = class {
15401
15953
  frameBytes = 0;
15402
15954
  captureError;
15403
15955
  async start() {
15404
- await mkdir3(this.directory, { recursive: true, mode: 448 });
15956
+ await mkdir5(this.directory, { recursive: true, mode: 448 });
15405
15957
  this.session.on("Page.screencastFrame", (payload) => {
15406
15958
  const bytes = Buffer.from(payload.data, "base64");
15407
15959
  if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
@@ -15410,9 +15962,9 @@ var ScreencastRecorder = class {
15410
15962
  return;
15411
15963
  }
15412
15964
  this.frameBytes += bytes.length;
15413
- const path = join6(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
15965
+ const path = join8(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
15414
15966
  this.frameCounter += 1;
15415
- const write = writeFile2(path, bytes, { mode: 384 }).then(() => {
15967
+ const write = writeFile4(path, bytes, { mode: 384 }).then(() => {
15416
15968
  this.frames.push({ path, timestamp: payload.metadata.timestamp ?? Date.now() / 1e3 });
15417
15969
  }).catch((error51) => {
15418
15970
  this.captureError = error51 instanceof Error ? error51 : new Error(String(error51));
@@ -15426,7 +15978,7 @@ var ScreencastRecorder = class {
15426
15978
  await this.session.send("Page.startScreencast", { format: "jpeg", quality: 82, maxWidth: this.width, maxHeight: this.height, everyNthFrame: 1 });
15427
15979
  this.started = true;
15428
15980
  }
15429
- async stop(outputPath, expectedDurationSeconds, captureOnly = false) {
15981
+ async stop(outputPath, expectedDurationSeconds, captureOnly = false, signal) {
15430
15982
  if (this.started) await this.session.send("Page.stopScreencast").catch(() => void 0);
15431
15983
  await new Promise((resolve) => setTimeout(resolve, 120));
15432
15984
  await Promise.all(this.pendingWrites);
@@ -15434,10 +15986,10 @@ var ScreencastRecorder = class {
15434
15986
  if (!this.frames.length) throw new Error("No video frames were captured.");
15435
15987
  const ordered = this.frames.toSorted((a, b) => a.timestamp - b.timestamp);
15436
15988
  const frames = [];
15437
- const bundle = await open3(join6(this.directory, "frames.bin"), "w", 384);
15989
+ const bundle = await open3(join8(this.directory, "frames.bin"), "w", 384);
15438
15990
  try {
15439
15991
  for (const frame of ordered) {
15440
- const bytes = await readFile4(frame.path);
15992
+ const bytes = await readFile6(frame.path);
15441
15993
  await bundle.writeFile(bytes);
15442
15994
  frames.push({ timestamp: frame.timestamp, size: bytes.length });
15443
15995
  }
@@ -15445,8 +15997,8 @@ var ScreencastRecorder = class {
15445
15997
  await bundle.close();
15446
15998
  }
15447
15999
  const manifest = frameManifestSchema.parse({ version: 1, durationSeconds: Math.min(600, expectedDurationSeconds), width: this.width, height: this.height, frames });
15448
- await writeFile2(join6(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
15449
- if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath);
16000
+ await writeFile4(join8(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
16001
+ if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath, signal);
15450
16002
  return { frameCount: this.frames.length, outputPath };
15451
16003
  }
15452
16004
  };
@@ -15470,6 +16022,7 @@ var RecordingSession = class _RecordingSession {
15470
16022
  requestEvents = /* @__PURE__ */ new Map();
15471
16023
  stopPromise;
15472
16024
  maxTimer;
16025
+ maxDurationSeconds;
15473
16026
  removeCursorOverlay;
15474
16027
  diagnosticsPaused = false;
15475
16028
  secretValues = [];
@@ -15480,7 +16033,10 @@ var RecordingSession = class _RecordingSession {
15480
16033
  bodyBytes = 0;
15481
16034
  truncated = false;
15482
16035
  constructor(options, runtime) {
16036
+ this.maxDurationSeconds = options.maxDurationSeconds ?? 600;
15483
16037
  this.encoding = options.encoding ?? "local";
16038
+ this.videoFormat = options.videoFormat ?? "webm";
16039
+ this.captureSignal = options.signal;
15484
16040
  this.recordingId = options.recordingId;
15485
16041
  this.cdpUrl = runtime.cdpUrl;
15486
16042
  this.browser = runtime.browser;
@@ -15494,15 +16050,17 @@ var RecordingSession = class _RecordingSession {
15494
16050
  static async start(options) {
15495
16051
  const width = options.width ?? 1280;
15496
16052
  const height = options.height ?? 720;
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 });
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 });
16053
+ await mkdir6(options.outputDirectory, { recursive: true, mode: 448 });
16054
+ if (options.encoding === "remote") await writeFile5(join9(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
16055
+ const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer, blockServiceWorkers: options.blockServiceWorkers });
15500
16056
  const cdpSession = await runtime.context.newCDPSession(runtime.page);
15501
- const screencast = new ScreencastRecorder(cdpSession, join7(options.outputDirectory, "frames"), width, height);
16057
+ const screencast = new ScreencastRecorder(cdpSession, join9(options.outputDirectory, "frames"), width, height);
15502
16058
  const session = new _RecordingSession(options, { ...runtime, screencast });
15503
16059
  try {
15504
16060
  await session.attachDiagnostics(options.captureNetworkBodies ?? false);
15505
- session.removeCursorOverlay = await attachCursorOverlay(runtime.page, cdpSession, options.cursorAppearance);
16061
+ if (!options.hideCursor) session.removeCursorOverlay = await attachCursorOverlay(runtime.page, cdpSession, options.cursorAppearance);
16062
+ await options.beforeCapture?.(runtime.page);
16063
+ session.startedAt = /* @__PURE__ */ new Date();
15506
16064
  await screencast.start();
15507
16065
  if (options.url) await session.page.goto(options.url, { waitUntil: "domcontentloaded" });
15508
16066
  } catch (error51) {
@@ -15511,7 +16069,7 @@ var RecordingSession = class _RecordingSession {
15511
16069
  }
15512
16070
  session.maxTimer = setTimeout(() => {
15513
16071
  void session.stop("max_duration").catch(() => void 0);
15514
- }, (options.maxDurationSeconds ?? 600) * 1e3);
16072
+ }, Math.max(0, session.startedAt.getTime() + session.maxDurationSeconds * 1e3 - Date.now()));
15515
16073
  return session;
15516
16074
  }
15517
16075
  // In-process agents share this connection so exposed diagnostic bindings are
@@ -15520,7 +16078,17 @@ var RecordingSession = class _RecordingSession {
15520
16078
  return this.page;
15521
16079
  }
15522
16080
  status() {
15523
- return { recordingId: this.recordingId, cdpUrl: this.cdpUrl, startedAt: this.startedAt.toISOString(), status: this.stopPromise ? "finalizing" : "recording" };
16081
+ const elapsedSeconds = Math.max(0, (Date.now() - this.startedAt.getTime()) / 1e3);
16082
+ return {
16083
+ recordingId: this.recordingId,
16084
+ cdpUrl: this.cdpUrl,
16085
+ startedAt: this.startedAt.toISOString(),
16086
+ maxDurationSeconds: this.maxDurationSeconds,
16087
+ elapsedSeconds,
16088
+ remainingSeconds: this.stopPromise ? 0 : Math.max(0, this.maxDurationSeconds - elapsedSeconds),
16089
+ stopsAt: new Date(this.startedAt.getTime() + this.maxDurationSeconds * 1e3).toISOString(),
16090
+ status: this.stopPromise ? "finalizing" : "recording"
16091
+ };
15524
16092
  }
15525
16093
  async stop(reason = "agent_request") {
15526
16094
  this.stopPromise ??= this.finalize(reason);
@@ -15530,8 +16098,8 @@ var RecordingSession = class _RecordingSession {
15530
16098
  if (this.maxTimer) clearTimeout(this.maxTimer);
15531
16099
  const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
15532
16100
  const outputDirectory = this.outputDirectory;
15533
- const videoPath = join7(outputDirectory, "recording.webm");
15534
- const diagnosticsPath = join7(outputDirectory, "diagnostics-v1.ndjson.gz");
16101
+ const videoPath = join9(outputDirectory, `recording.${this.videoFormat}`);
16102
+ const diagnosticsPath = join9(outputDirectory, "diagnostics-v1.ndjson.gz");
15535
16103
  const bundle = {
15536
16104
  schemaVersion: 1,
15537
16105
  capturePolicyVersion: "safe-default-v1",
@@ -15548,21 +16116,23 @@ var RecordingSession = class _RecordingSession {
15548
16116
  try {
15549
16117
  const safeBundle = JSON.parse(this.secretValues.reduce((text, secret) => text.split(JSON.stringify(secret).slice(1, -1)).join("[redacted]"), JSON.stringify(bundle)));
15550
16118
  await writeBundle(safeBundle, diagnosticsPath);
15551
- await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote");
16119
+ await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote", this.captureSignal);
15552
16120
  } finally {
15553
16121
  await this.removeCursorOverlay?.();
15554
16122
  if (this.browserProcess) {
15555
16123
  await this.browser.close().catch(() => void 0);
15556
16124
  await terminateBrowser(this.browserProcess);
15557
- if (this.profileDirectory) await rm3(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
16125
+ if (this.profileDirectory) await rm5(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
15558
16126
  }
15559
16127
  }
15560
16128
  const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
15561
16129
  const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
15562
16130
  const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
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 } };
16131
+ return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory: join9(outputDirectory, "frames") } : {}, diagnosticsPath, durationSeconds, stoppedReason: reason, diagnostics: { consoleErrors, failedRequests, httpErrors, truncated: this.truncated, bodyCaptureEnabled: this.captureNetworkBodies } };
15564
16132
  }
15565
16133
  captureNetworkBodies = false;
16134
+ captureSignal;
16135
+ videoFormat = "webm";
15566
16136
  encoding = "local";
15567
16137
  outputDirectory = "";
15568
16138
  async attachDiagnostics(captureNetworkBodies) {
@@ -15672,7 +16242,7 @@ var RecordingSession = class _RecordingSession {
15672
16242
  };
15673
16243
  async function launchManaged(options) {
15674
16244
  const port = await getAvailablePort();
15675
- const profileDirectory = await mkdtemp(join7(tmpdir2(), "rill-chrome-"));
16245
+ const profileDirectory = await mkdtemp(join9(tmpdir2(), "rill-chrome-"));
15676
16246
  const executable = findChromeExecutable();
15677
16247
  const args = [
15678
16248
  `--remote-debugging-port=${port}`,
@@ -15689,19 +16259,19 @@ async function launchManaged(options) {
15689
16259
  ...options.headed ? [] : ["--headless=new", "--hide-scrollbars"],
15690
16260
  "about:blank"
15691
16261
  ];
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;
15693
- const browserProcess = spawn4(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, env });
16262
+ const env = options.proxyServer ? Object.fromEntries(Object.entries(process.env).filter(([key2]) => ["PATH", "HOME", "TMPDIR", "DISPLAY", "LANG", "LD_LIBRARY_PATH", "XDG_RUNTIME_DIR"].includes(key2))) : process.env;
16263
+ const browserProcess = spawn6(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, windowsHide: true, env });
15694
16264
  const cdpUrl = `http://127.0.0.1:${port}`;
15695
16265
  try {
15696
16266
  await waitForCdp(cdpUrl, browserProcess);
15697
16267
  const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
15698
- const context = browser.contexts()[0];
16268
+ const context = options.blockServiceWorkers ? await browser.newContext({ serviceWorkers: "block" }) : browser.contexts()[0];
15699
16269
  const page = context.pages()[0] ?? await context.newPage();
15700
16270
  await page.setViewportSize({ width: options.width, height: options.height });
15701
16271
  return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
15702
16272
  } catch (error51) {
15703
16273
  await terminateBrowser(browserProcess);
15704
- await rm3(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
16274
+ await rm5(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
15705
16275
  throw error51;
15706
16276
  }
15707
16277
  }
@@ -15751,16 +16321,31 @@ async function waitForCdp(cdpUrl, browserProcess) {
15751
16321
  var sessions = /* @__PURE__ */ new Map();
15752
16322
  var lastActivity = Date.now();
15753
16323
  async function runDaemon() {
15754
- const socketPath = daemonSocketPath();
15755
- await mkdir5(dirname(socketPath), { recursive: true, mode: 448 });
15756
- await mkdir5(recordingTempRoot(), { recursive: true, mode: 448 });
16324
+ if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC.");
16325
+ const identity = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
16326
+ const socketPath = identity?.pipe ?? daemonSocketPath();
16327
+ await mkdir7(identity ? dirname(daemonStatePath()) : dirname(socketPath), { recursive: true, mode: 448 });
16328
+ await mkdir7(recordingTempRoot(), { recursive: true, mode: 448 });
15757
16329
  await restoreRecoverableSessions();
15758
16330
  await purgeExpiredCaptures();
15759
- await unlink(socketPath).catch((error51) => {
16331
+ if (!identity) await unlink(socketPath).catch((error51) => {
15760
16332
  if (error51.code !== "ENOENT") throw error51;
15761
16333
  });
15762
- const server = createServer2((request3, response) => void route(request3, response));
15763
- server.listen(socketPath, () => process.platform !== "win32" && import("fs/promises").then(({ chmod }) => chmod(socketPath, 384)));
16334
+ const server = createServer2((request3, response) => {
16335
+ if (identity && !acceptsWindowsIpcToken(identity.token, request3.headers.authorization)) {
16336
+ request3.resume();
16337
+ json2(response, 401, { error: { code: "authentication_required", message: "Recorder authentication required." } });
16338
+ return;
16339
+ }
16340
+ void route(request3, response);
16341
+ });
16342
+ await new Promise((resolve, reject) => {
16343
+ server.once("error", reject);
16344
+ server.listen(socketPath, () => {
16345
+ if (identity) resolve();
16346
+ else void chmod(socketPath, 384).then(resolve, reject);
16347
+ });
16348
+ });
15764
16349
  const idleTimer = setInterval(() => {
15765
16350
  void (async () => {
15766
16351
  await purgeExpiredCaptures();
@@ -15773,7 +16358,7 @@ async function runDaemon() {
15773
16358
  const close = () => {
15774
16359
  clearInterval(idleTimer);
15775
16360
  server.close();
15776
- void unlink(socketPath).catch(() => void 0);
16361
+ if (!identity) void unlink(socketPath).catch(() => void 0);
15777
16362
  };
15778
16363
  process.once("SIGTERM", close);
15779
16364
  process.once("SIGINT", close);
@@ -15787,12 +16372,12 @@ async function route(request3, response) {
15787
16372
  try {
15788
16373
  const input = await readJson(request3);
15789
16374
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
15790
- const outputDirectory = join8(recordingTempRoot(), input.recordingId);
16375
+ const outputDirectory = join10(recordingTempRoot(), input.recordingId);
15791
16376
  const session = await RecordingSession.start({ ...input, outputDirectory });
15792
16377
  const status = session.status();
15793
16378
  const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, startedAt: status.startedAt }, state: "recording" };
15794
16379
  sessions.set(input.recordingId, entry);
15795
- const maxTimer = setTimeout(() => void finalizeEntry(entry, "max_duration"), (input.maxDurationSeconds ?? 600) * 1e3 + 250);
16380
+ const maxTimer = setTimeout(() => void finalizeEntry(entry, "max_duration"), Math.max(0, Date.parse(status.stopsAt) - Date.now()) + 250);
15796
16381
  maxTimer.unref();
15797
16382
  await persistState();
15798
16383
  return json2(response, 201, session.status());
@@ -15827,11 +16412,18 @@ async function finalizeEntry(entry, reason) {
15827
16412
  await persistState();
15828
16413
  }
15829
16414
  function serializeEntry(entry) {
15830
- return { ...entry.base, status: entry.state, result: entry.result, error: entry.error };
16415
+ return {
16416
+ ...entry.base,
16417
+ ...entry.session?.status(),
16418
+ status: entry.state,
16419
+ ...entry.state !== "recording" ? { remainingSeconds: 0, ...entry.result ? { elapsedSeconds: entry.result.durationSeconds } : {} } : {},
16420
+ result: entry.result,
16421
+ error: entry.error
16422
+ };
15831
16423
  }
15832
16424
  async function restoreRecoverableSessions() {
15833
16425
  try {
15834
- const value = JSON.parse(await readFile5(daemonStatePath(), "utf8"));
16426
+ const value = JSON.parse(await readFile7(daemonStatePath(), "utf8"));
15835
16427
  for (const [id, entry] of Object.entries(value)) {
15836
16428
  if (entry.status !== "ready" && entry.status !== "failed") continue;
15837
16429
  sessions.set(id, { base: { recordingId: entry.recordingId, cdpUrl: entry.cdpUrl, startedAt: entry.startedAt }, state: entry.status, result: entry.result, error: entry.error });
@@ -15842,9 +16434,9 @@ async function restoreRecoverableSessions() {
15842
16434
  async function persistState() {
15843
16435
  const state = Object.fromEntries([...sessions.entries()].map(([id, entry]) => [id, serializeEntry(entry)]));
15844
16436
  const temporary = daemonStatePath() + "." + crypto.randomUUID() + ".tmp";
15845
- await writeFile4(temporary, `${JSON.stringify(state, null, 2)}
16437
+ await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
15846
16438
  `, { mode: 384 });
15847
- await rename(temporary, daemonStatePath());
16439
+ await rename2(temporary, daemonStatePath());
15848
16440
  }
15849
16441
  async function readJson(request3) {
15850
16442
  const chunks = [];
@@ -15865,7 +16457,7 @@ async function purgeExpiredCaptures() {
15865
16457
  const removed = await purgeExpiredRemoteCaptures(recordingTempRoot());
15866
16458
  for (const id of removed) sessions.delete(id);
15867
16459
  let changed = removed.length > 0;
15868
- for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat3(entry.result.frameDirectory).catch(() => null)) {
16460
+ for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat4(entry.result.frameDirectory).catch(() => null)) {
15869
16461
  sessions.delete(id);
15870
16462
  changed = true;
15871
16463
  }
@@ -15910,7 +16502,7 @@ function emit(value, pretty = false) {
15910
16502
  function failureValue(error51) {
15911
16503
  const message = error51 instanceof Error ? error51.message : String(error51);
15912
16504
  const [candidate] = message.split(":", 1);
15913
- const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "scope_denied", "quota_exceeded", "browser_unavailable", "recording_not_found", "upload_interrupted", "processing_failed", "share_revoked", "validation_failed"]);
16505
+ const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "authorization_not_found", "expired_token", "access_denied", "unsupported_platform", "scope_denied", "quota_exceeded", "browser_unavailable", "recording_not_found", "upload_interrupted", "processing_pending", "processing_failed", "share_revoked", "validation_failed"]);
15914
16506
  const structured = error51 && typeof error51 === "object" ? error51 : {};
15915
16507
  const code = structured.code && knownCodes.has(structured.code) ? structured.code : knownCodes.has(candidate) ? candidate : "internal_error";
15916
16508
  return { error: { code, message, ...structured.recovery ? { recovery: structured.recovery } : {}, ...structured.details ? { details: structured.details } : {} } };
@@ -15977,7 +16569,7 @@ async function withRunFeedback(session, start) {
15977
16569
  }
15978
16570
 
15979
16571
  // src/doctor.ts
15980
- import { spawn as spawn5 } from "child_process";
16572
+ import { spawn as spawn7 } from "child_process";
15981
16573
 
15982
16574
  // src/version.ts
15983
16575
  import { createRequire } from "module";
@@ -15986,6 +16578,7 @@ var CLI_VERSION = packageJson.version;
15986
16578
 
15987
16579
  // src/doctor.ts
15988
16580
  var defaultDependencies = {
16581
+ platform: process.platform,
15989
16582
  encoding: "remote",
15990
16583
  encodingCapabilities: (apiUrl, token) => new ControlPlaneClient(apiUrl, token).encodingCapabilities(),
15991
16584
  nodeVersion: process.version,
@@ -16001,6 +16594,7 @@ async function runDoctor(apiUrl, overrides = {}) {
16001
16594
  const dependencies = { ...defaultDependencies, ...overrides };
16002
16595
  const checks = [];
16003
16596
  let feedbackEnabled = false;
16597
+ checks.push(recorderTransportCheck(dependencies.platform));
16004
16598
  if (compareVersions(normalizeVersion(dependencies.nodeVersion), "22.0.0") >= 0) {
16005
16599
  checks.push({ name: "node", status: "pass", message: `Node.js ${dependencies.nodeVersion} is supported.`, version: dependencies.nodeVersion });
16006
16600
  } else {
@@ -16028,8 +16622,16 @@ async function runDoctor(apiUrl, overrides = {}) {
16028
16622
  if (!health) {
16029
16623
  checks.push({ name: "authentication", status: "skip", message: "Authentication was not checked because the API is unreachable.", recovery: `Restore API reachability, then run \`rill --api-url ${apiUrl} doctor\` again.` });
16030
16624
  } else {
16031
- const token = await dependencies.loadCredential(apiUrl);
16032
- if (!token) {
16625
+ let token = null;
16626
+ let credentialError;
16627
+ try {
16628
+ token = await dependencies.loadCredential(apiUrl);
16629
+ } catch (error51) {
16630
+ credentialError = error51;
16631
+ }
16632
+ if (credentialError) {
16633
+ checks.push({ name: "authentication", status: "fail", message: errorMessage(credentialError), recovery: "Unlock the OS credential store, or use RILL_TOKEN. On Windows, use the same Windows account that saved the credential." });
16634
+ } else if (!token) {
16033
16635
  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.` });
16034
16636
  } else {
16035
16637
  try {
@@ -16043,7 +16645,7 @@ async function runDoctor(apiUrl, overrides = {}) {
16043
16645
  }
16044
16646
  checks.push(await executableCheck("chrome", dependencies.chromePath, ["--version"], dependencies.commandVersion, "Install Google Chrome or Chromium, or set RILL_CHROME_PATH to its executable."));
16045
16647
  if (dependencies.encoding === "local") {
16046
- checks.push(await executableCheck("ffmpeg", dependencies.ffmpegPath, ["-version"], dependencies.commandVersion, process.platform === "darwin" ? "Install ffmpeg with `brew install ffmpeg`, or set RILL_FFMPEG." : "Install ffmpeg using the system package manager, or set RILL_FFMPEG."));
16648
+ checks.push(await executableCheck("ffmpeg", dependencies.ffmpegPath, ["-version"], dependencies.commandVersion, dependencies.platform === "darwin" ? "Install ffmpeg with `brew install ffmpeg`, or set RILL_FFMPEG." : "Install ffmpeg using the system package manager, or set RILL_FFMPEG."));
16047
16649
  } else {
16048
16650
  checks.push({ name: "ffmpeg", status: "skip", message: "Remote encoding does not require local FFmpeg." });
16049
16651
  try {
@@ -16051,7 +16653,7 @@ async function runDoctor(apiUrl, overrides = {}) {
16051
16653
  if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
16052
16654
  checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
16053
16655
  } catch (error51) {
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." });
16656
+ checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "Run doctor --encoding local to check local FFmpeg, then record start --encoding local, or ask the workspace owner to enable remote encoding." });
16055
16657
  }
16056
16658
  }
16057
16659
  return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
@@ -16066,7 +16668,7 @@ async function executableCheck(name, path, args, run2, recovery) {
16066
16668
  }
16067
16669
  function commandVersion(command, args) {
16068
16670
  return new Promise((resolve, reject) => {
16069
- const child = spawn5(command, args, { stdio: ["ignore", "pipe", "pipe"] });
16671
+ const child = spawn7(command, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
16070
16672
  const stdout = [];
16071
16673
  const stderr = [];
16072
16674
  const timeout = setTimeout(() => {
@@ -16127,31 +16729,53 @@ function formatCloudRun(run2) {
16127
16729
  }
16128
16730
 
16129
16731
  // src/recording-lifecycle.ts
16130
- async function startRecordingWithCompensation(remote, dependencies) {
16732
+ async function startRecordingWithCompensation(remote, dependencies, apiUrl = "https://userill.dev") {
16131
16733
  try {
16132
16734
  return await dependencies.startLocal();
16133
16735
  } catch (startupError) {
16134
16736
  try {
16135
16737
  await dependencies.cancelRemote(remote.recordingId, "local_start_failed");
16136
16738
  } catch (cleanupError) {
16137
- throw new AggregateError(
16138
- [startupError, cleanupError],
16139
- "Local recording startup failed and the remote reservation could not be cancelled. Run `rill record cancel <recording-id>`."
16739
+ const nextCommand = cliCommand(apiUrl, "record", "cancel", remote.recordingId);
16740
+ throw Object.assign(
16741
+ new AggregateError(
16742
+ [startupError, cleanupError],
16743
+ "browser_unavailable: Local startup failed and its remote reservation could not be cancelled."
16744
+ ),
16745
+ {
16746
+ code: "browser_unavailable",
16747
+ recovery: `Run ${nextCommand} to release this failed startup reservation.`,
16748
+ details: { recordingId: remote.recordingId, nextCommand }
16749
+ }
16140
16750
  );
16141
16751
  }
16142
16752
  throw startupError;
16143
16753
  }
16144
16754
  }
16145
- async function remoteAwareRecordingStatus(recordingId, dependencies) {
16755
+ async function remoteAwareRecordingStatus(recordingId, dependencies, apiUrl = "https://userill.dev") {
16756
+ const remote = await dependencies.remoteStatus(recordingId);
16757
+ const remoteView = {
16758
+ ...remote,
16759
+ recordingId: typeof remote.recordingId === "string" ? remote.recordingId : remote.id ?? recordingId,
16760
+ source: "remote",
16761
+ ...recordingNextStep(apiUrl, recordingId, String(remote.status))
16762
+ };
16763
+ if (["uploading", "processing", "ready", "failed", "expired", "cancelled"].includes(String(remote.status))) return remoteView;
16146
16764
  try {
16147
- return { ...await dependencies.localStatus(recordingId), source: "local" };
16148
- } catch {
16149
- const remote = await dependencies.remoteStatus(recordingId);
16765
+ const local = await dependencies.localStatus(recordingId);
16766
+ const status = local.status === "ready" ? "captured" : local.status;
16150
16767
  return {
16151
- ...remote,
16152
- recordingId: typeof remote.recordingId === "string" ? remote.recordingId : remote.id ?? recordingId,
16153
- source: "remote"
16768
+ ...local,
16769
+ status,
16770
+ source: "local",
16771
+ ...recordingNextStep(apiUrl, recordingId, String(status)),
16772
+ ...status === "failed" ? {
16773
+ nextCommand: cliCommand(apiUrl, "record", "cancel", recordingId),
16774
+ message: "Local capture failed; the remote reservation may still hold a slot. Preserve any local draft you need, then cancel this failed attempt to release its slot. Cancellation does not delete local drafts."
16775
+ } : {}
16154
16776
  };
16777
+ } catch {
16778
+ return remoteView;
16155
16779
  }
16156
16780
  }
16157
16781
 
@@ -16165,10 +16789,10 @@ function controlPlaneUrl(environmentUrl) {
16165
16789
  async function removeLocalCapture(id) {
16166
16790
  await ensureDaemon(process.argv[1]);
16167
16791
  const local = await recordingStatus(id).catch(() => null);
16168
- if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16792
+ if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
16169
16793
  }
16170
16794
  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 } });
16795
+ const child = spawn8(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env }, windowsHide: true });
16172
16796
  child.on("error", () => void 0);
16173
16797
  child.unref();
16174
16798
  }
@@ -16190,7 +16814,7 @@ async function authorize(quiet = false) {
16190
16814
  process.stderr.write(`Open ${authorization.verificationUri} and enter ${authorization.userCode}
16191
16815
  `);
16192
16816
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
16193
- const child = spawn6(opener, [authorization.verificationUri], { stdio: "ignore", detached: true });
16817
+ const child = spawn8(opener, [authorization.verificationUri], { stdio: "ignore", detached: true });
16194
16818
  child.on("error", () => void 0);
16195
16819
  child.unref();
16196
16820
  const deadline = Date.now() + authorization.expiresIn * 1e3;
@@ -16211,9 +16835,16 @@ program.command("whoami").description("Show the current verified agent identity
16211
16835
  emit(identity, options.pretty);
16212
16836
  if (!identity.authenticated) process.exitCode = 1;
16213
16837
  });
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) => {
16838
+ var connect = program.command("connect").description("Connect this agent once, with renewable account access.").option("--email <email>", "Email of the Rill account approving access").option("--no-wait", "Return approval instructions immediately; poll with connect status").action(async (input) => {
16839
+ if (!input.email) throw new Error("validation_failed: Supply --email <email> to start a connection.");
16215
16840
  const options = program.opts();
16216
- emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email) }, options.pretty);
16841
+ emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email, { wait: input.wait }) }, options.pretty);
16842
+ });
16843
+ connect.command("status").argument("<authorization-id>").description("Poll a saved connection once without starting another approval.").action(async (id) => {
16844
+ const options = program.opts();
16845
+ const result = await pollConnection(options.apiUrl, id);
16846
+ emit({ schemaVersion: 1, ...result }, options.pretty);
16847
+ if (result.status === "expired" || result.status === "denied") process.exitCode = 1;
16217
16848
  });
16218
16849
  program.command("login").description("Authorize Rill using the existing device login flow.").action(async () => {
16219
16850
  await authorize();
@@ -16252,6 +16883,7 @@ for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<r
16252
16883
  });
16253
16884
  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) => {
16254
16885
  const options = program.opts();
16886
+ assertRecorderTransport();
16255
16887
  await ensureAnonymousCredential(options.apiUrl);
16256
16888
  const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16257
16889
  const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
@@ -16268,7 +16900,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
16268
16900
  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 });
16269
16901
  },
16270
16902
  cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
16271
- });
16903
+ }, options.apiUrl);
16272
16904
  await client.markRecordingStarted(remote.recordingId, {
16273
16905
  cliVersion: CLI_VERSION,
16274
16906
  os: process.platform,
@@ -16276,7 +16908,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
16276
16908
  headed: Boolean(commandOptions.headed),
16277
16909
  attached: Boolean(commandOptions.cdpUrl)
16278
16910
  }).catch(() => void 0);
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) };
16911
+ return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, browserAttachment: browserAttachment(local.cdpUrl), startedAt: local.startedAt, stopsAt: local.stopsAt, elapsedSeconds: local.elapsedSeconds, remainingSeconds: local.remainingSeconds, bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
16280
16912
  });
16281
16913
  emit(result, options.pretty);
16282
16914
  });
@@ -16296,12 +16928,12 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
16296
16928
  await client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics);
16297
16929
  const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
16298
16930
  const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
16299
- emit({ schemaVersion: 1, ...result2, ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
16300
- if (result2.status === "ready") await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16931
+ emit({ schemaVersion: 1, ...result2, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
16932
+ if (result2.status === "ready") await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
16301
16933
  else watchEncoding(options.apiUrl, recordingId);
16302
16934
  return;
16303
16935
  }
16304
- const video = await stat4(local.result.videoPath);
16936
+ const video = await stat5(local.result.videoPath);
16305
16937
  const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
16306
16938
  await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
16307
16939
  const remote = commandOptions.wait === false || record2.opts().wait === false ? null : await client.waitUntilPlayable(recordingId);
@@ -16315,10 +16947,11 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
16315
16947
  durationSeconds: local.result.durationSeconds,
16316
16948
  stoppedReason: local.result.stoppedReason,
16317
16949
  diagnostics: local.result.diagnostics,
16950
+ ...recordingNextStep(options.apiUrl, recordingId, remote ? "ready" : "processing"),
16318
16951
  ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
16319
16952
  };
16320
16953
  emit(result, options.pretty);
16321
- if (remote) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16954
+ if (remote) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
16322
16955
  });
16323
16956
  record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
16324
16957
  const options = program.opts();
@@ -16338,7 +16971,7 @@ record2.command("retry").argument("<recording-id>").option("--no-wait").action(a
16338
16971
  if (!accepted) throw new Error("Encoding job is unavailable.");
16339
16972
  if (["expired", "cancelled"].includes(accepted.status)) throw new Error(`Recording ${accepted.status}.`);
16340
16973
  const result = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
16341
- emit({ schemaVersion: 1, ...result }, options.pretty);
16974
+ emit({ schemaVersion: 1, ...result, ...recordingNextStep(options.apiUrl, recordingId, result.status, { resumableFrames: true }) }, options.pretty);
16342
16975
  if (result.status === "ready") await removeLocalCapture(recordingId);
16343
16976
  else watchEncoding(options.apiUrl, recordingId);
16344
16977
  });
@@ -16350,7 +16983,7 @@ program.command("encoding-watch", { hidden: true }).argument("<recording-id>").a
16350
16983
  if (result.status === "ready") {
16351
16984
  await ensureDaemon(process.argv[1]);
16352
16985
  const local = await recordingStatus(id).catch(() => null);
16353
- if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16986
+ if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
16354
16987
  }
16355
16988
  });
16356
16989
  var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
@@ -16380,11 +17013,11 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
16380
17013
  try {
16381
17014
  const encoding = await client.encodingStatus(recordingId);
16382
17015
  const remote = encoding.status === "ready" ? await client.recordingStatus(recordingId) : {};
16383
- emit({ schemaVersion: 1, ...encoding, ...remote, recordingId, source: "remote" }, options.pretty);
17016
+ emit({ schemaVersion: 1, ...encoding, ...remote, recordingId, source: "remote", ...recordingNextStep(options.apiUrl, recordingId, encoding.status, { resumableFrames: true }) }, options.pretty);
16384
17017
  if (encoding.status === "ready") await removeLocalCapture(recordingId);
16385
17018
  return;
16386
17019
  } catch (error51) {
16387
- if (!(error51 instanceof Error) || !error51.message.includes("recording_not_found")) throw error51;
17020
+ if (!(error51 instanceof ControlPlaneError) || error51.code !== "recording_not_found") throw error51;
16388
17021
  }
16389
17022
  const status = await remoteAwareRecordingStatus(recordingId, {
16390
17023
  localStatus: async (id) => {
@@ -16392,7 +17025,7 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
16392
17025
  return recordingStatus(id);
16393
17026
  },
16394
17027
  remoteStatus: (id) => client.recordingStatus(id)
16395
- });
17028
+ }, options.apiUrl);
16396
17029
  emit({ schemaVersion: 1, ...status }, options.pretty);
16397
17030
  });
16398
17031
  record2.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
@@ -16405,41 +17038,56 @@ program.command("upload").argument("<video-file>").option("--title <title>").opt
16405
17038
  const options = program.opts();
16406
17039
  await ensureAnonymousCredential(options.apiUrl);
16407
17040
  const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16408
- const file2 = await stat4(videoFile);
17041
+ const file2 = await stat5(videoFile);
16409
17042
  const recording = await client.createRecording({ title: commandOptions.title ?? basename(videoFile), sourceUrl: null });
16410
17043
  const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file2.size, filename: basename(videoFile), uploadKind: "existing_video" });
16411
17044
  await client.uploadVideo(upload.uploadUrl, videoFile);
16412
17045
  const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recording.recordingId);
16413
17046
  if (remote) process.stderr.write("\n");
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);
17047
+ emit({ schemaVersion: 1, recordingId: recording.recordingId, status: remote ? "ready" : "processing", shareUrl: remote?.shareUrl ?? null, expiresAt: remote?.expires_at ?? recording.expiresAt ?? null, diagnosticsAvailable: false, ...recordingNextStep(options.apiUrl, recording.recordingId, remote ? "ready" : "processing") }, options.pretty);
16415
17048
  });
16416
- program.command("inspect").argument("<share-url>").action(async (shareUrl) => {
17049
+ program.command("inspect").argument("<share-url>").option("--cursor <cursor>", "Read the next page of discussions").action(async (shareUrl, commandOptions) => {
16417
17050
  const options = program.opts();
16418
- emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).inspect(shareUrl), options.pretty);
17051
+ emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).inspect(shareUrl, commandOptions.cursor), options.pretty);
17052
+ });
17053
+ var comments = program.command("comments").description("Reply to pinned review discussions.");
17054
+ comments.command("reply").argument("<share-url>").requiredOption("--discussion <id>").requiredOption("--body <text>").option("--request-id <uuid>", "Reuse on retry to prevent duplicate replies").action(async (shareUrl, input) => {
17055
+ const options = program.opts();
17056
+ const requestId = input.requestId ?? crypto.randomUUID();
17057
+ process.stderr.write(`Reply request ID: ${requestId} (reuse with --request-id when retrying)
17058
+ `);
17059
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
17060
+ emit(await client.reply(shareUrl, input.discussion, input.body, requestId), options.pretty);
17061
+ });
17062
+ comments.command("edit").argument("<share-url>").requiredOption("--comment <id>").requiredOption("--body <text>").action(async (shareUrl, input) => {
17063
+ const options = program.opts();
17064
+ const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
17065
+ emit(await client.editReply(shareUrl, input.comment, input.body), options.pretty);
16419
17066
  });
16420
17067
  var drafts = program.command("drafts");
16421
17068
  drafts.command("list").action(async () => {
16422
17069
  const options = program.opts();
16423
- const root = process.env.RILL_TEMP ?? `${process.env.TMPDIR ?? "/tmp"}/rill-recordings`;
17070
+ const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
16424
17071
  const names = await readdir2(root).catch(() => []);
16425
17072
  emit({ schemaVersion: 1, drafts: names }, options.pretty);
16426
17073
  });
16427
17074
  drafts.command("purge").action(async () => {
16428
17075
  const options = program.opts();
16429
- const root = process.env.RILL_TEMP ?? `${process.env.TMPDIR ?? "/tmp"}/rill-recordings`;
17076
+ const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
16430
17077
  const names = await readdir2(root).catch(() => []);
16431
17078
  let purged = 0;
16432
17079
  for (const name of names) {
16433
- const path = `${root}/${name}`;
16434
- const metadata = await stat4(path);
17080
+ const path = join11(root, name);
17081
+ const metadata = await stat5(path);
16435
17082
  if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
16436
- await rm5(path, { recursive: true, force: true });
17083
+ await rm7(path, { recursive: true, force: true });
16437
17084
  purged += 1;
16438
17085
  }
16439
17086
  }
16440
17087
  emit({ schemaVersion: 1, purged }, options.pretty);
16441
17088
  });
16442
17089
  program.hook("preAction", async (_command, actionCommand) => {
17090
+ if (commandPath(actionCommand).join(" ") === "record start") assertRecorderTransport();
16443
17091
  const command = cliCommandCategory(commandPath(actionCommand));
16444
17092
  if (!command) return;
16445
17093
  const options = program.opts();