@runalabs/rill-cli 0.1.13 → 0.1.15

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 +42 -9
  2. package/dist/cli.js +1081 -392
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5,23 +5,109 @@ 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
+
8
90
  // 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";
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";
13
95
 
14
96
  // src/recording-guidance.ts
15
- function cliCommand(apiUrl, ...args) {
16
- const quote = (value) => /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
97
+ function cliCommandForPlatform(platform, apiUrl, ...args) {
98
+ const quote = (value) => /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : platform === "win32" ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", "'\\''")}'`;
17
99
  return ["rill", "--api-url", apiUrl, ...args].map(quote).join(" ");
18
100
  }
19
- var transportRecovery = "Use macOS or Linux for browser recording. On Windows, use RILL_TOKEN with rill upload <video-file> for video-only sharing; diagnostics are not imported. RILL_SOCKET does not enable Windows named-pipe or TCP support in this release.";
20
- function recorderTransportCheck(platform = process.platform) {
21
- return platform === "win32" ? { name: "recorder_transport", status: "fail", message: "The local recorder transport does not support Windows.", recovery: transportRecovery } : { name: "recorder_transport", status: "pass", message: "The local recorder uses a Unix-domain socket." };
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." };
22
108
  }
23
- function assertRecorderTransport(platform = process.platform) {
24
- const check2 = recorderTransportCheck(platform);
109
+ function assertRecorderTransport(platform = process.platform, socketOverride = process.env.RILL_SOCKET) {
110
+ const check2 = recorderTransportCheck(platform, socketOverride);
25
111
  if (check2.status === "fail") throw Object.assign(new Error(`unsupported_platform: ${check2.message}`), { code: "unsupported_platform", recovery: check2.recovery });
26
112
  }
27
113
  function recordingNextStep(apiUrl, recordingId, status, options = {}) {
@@ -30,12 +116,13 @@ function recordingNextStep(apiUrl, recordingId, status, options = {}) {
30
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." };
31
117
  return {};
32
118
  }
33
- function browserAttachment(cdpUrl) {
119
+ function browserAttachment(cdpUrl, tabId) {
34
120
  return {
35
121
  protocol: "cdp",
36
122
  cdpUrl,
123
+ ...tabId ? { tabId } : {},
37
124
  playwrightMethod: "chromium.connectOverCDP",
38
- 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
+ instructions: (tabId ? "Record only the returned tabId. Match it using a page CDP session and Target.getTargetInfo; do not pick the first page. " : "") + "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.",
39
126
  shellFallback: "See the shell-only Playwright example in the Rill CLI README; install playwright-core in a temporary directory outside the project."
40
127
  };
41
128
  }
@@ -48,6 +135,7 @@ var AgentAuthError = class extends Error {
48
135
  super(`${code}: ${message}`);
49
136
  this.code = code;
50
137
  }
138
+ code;
51
139
  };
52
140
  function parseAuthorization(value) {
53
141
  if (!value?.startsWith("{")) return null;
@@ -63,7 +151,7 @@ async function request(apiUrl, path, body, form = false) {
63
151
  redirect: "error",
64
152
  signal: AbortSignal.timeout(15e3),
65
153
  headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
66
- body: form ? new URLSearchParams(Object.entries(body).map(([key, value2]) => [key, String(value2)])) : JSON.stringify(body)
154
+ body: form ? new URLSearchParams(Object.entries(body).map(([key2, value2]) => [key2, String(value2)])) : JSON.stringify(body)
67
155
  });
68
156
  const value = await response.json();
69
157
  if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
@@ -107,19 +195,19 @@ async function ensureAnonymousCredential(apiUrl) {
107
195
  return withAuthorizationLock(apiUrl, () => createAnonymousCredential(apiUrl));
108
196
  }
109
197
  async function withAuthorizationLock(apiUrl, action) {
110
- const root = join(homedir(), ".rill", "auth-locks");
111
- await mkdir(root, { recursive: true, mode: 448 });
112
- const lock = join(root, createHash("sha256").update(apiUrl).digest("hex"));
198
+ const root = join2(homedir2(), ".rill", "auth-locks");
199
+ await mkdir2(root, { recursive: true, mode: 448 });
200
+ const lock = join2(root, createHash2("sha256").update(apiUrl).digest("hex"));
113
201
  const deadline = Date.now() + 3e4;
114
202
  while (true) {
115
203
  try {
116
- await mkdir(lock, { mode: 448 });
204
+ await mkdir2(lock, { mode: 448 });
117
205
  break;
118
206
  } catch (error51) {
119
207
  if (error51.code !== "EEXIST") throw error51;
120
- const info = await stat(lock).catch(() => null);
208
+ const info = await stat2(lock).catch(() => null);
121
209
  if (info && Date.now() - info.mtimeMs > 12e4) {
122
- await rm(lock, { recursive: true, force: true });
210
+ await rm2(lock, { recursive: true, force: true });
123
211
  continue;
124
212
  }
125
213
  if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
@@ -129,7 +217,7 @@ async function withAuthorizationLock(apiUrl, action) {
129
217
  try {
130
218
  return await action();
131
219
  } finally {
132
- await rm(lock, { recursive: true, force: true });
220
+ await rm2(lock, { recursive: true, force: true });
133
221
  }
134
222
  }
135
223
  async function createAnonymousCredential(apiUrl) {
@@ -246,9 +334,10 @@ async function pollSavedConnection(apiUrl, authorizationId) {
246
334
  }
247
335
 
248
336
  // src/keychain.ts
249
- import { spawn } from "child_process";
337
+ import { spawn as spawn2 } from "child_process";
250
338
  var service = "rill";
251
339
  async function saveCredential(apiUrl, token) {
340
+ if (process.platform === "win32") return saveWindowsCredential(apiUrl, token);
252
341
  if (process.platform === "darwin") {
253
342
  await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
254
343
  return;
@@ -263,6 +352,7 @@ function loadCredential(apiUrl) {
263
352
  return loadAgentCredential(apiUrl);
264
353
  }
265
354
  async function loadStoredCredential(apiUrl) {
355
+ if (process.platform === "win32") return loadWindowsCredential(apiUrl);
266
356
  try {
267
357
  if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
268
358
  if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
@@ -276,7 +366,7 @@ async function loadStoredCredential(apiUrl) {
276
366
  }
277
367
  function run(command, args, input) {
278
368
  return new Promise((resolve, reject) => {
279
- const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
369
+ const child = spawn2(command, args, { stdio: ["pipe", "pipe", "pipe"] });
280
370
  const stdout = [];
281
371
  const stderr = [];
282
372
  child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
@@ -290,7 +380,7 @@ function run(command, args, input) {
290
380
 
291
381
  // src/control-plane.ts
292
382
  import { createReadStream } from "fs";
293
- import { readFile, stat as stat2 } from "fs/promises";
383
+ import { readFile as readFile2, stat as stat3 } from "fs/promises";
294
384
  import { Upload } from "tus-js-client";
295
385
  var ControlPlaneError = class extends Error {
296
386
  constructor(code, message, recovery, details, httpStatus) {
@@ -301,6 +391,10 @@ var ControlPlaneError = class extends Error {
301
391
  this.httpStatus = httpStatus;
302
392
  this.name = "ControlPlaneError";
303
393
  }
394
+ code;
395
+ recovery;
396
+ details;
397
+ httpStatus;
304
398
  };
305
399
  var ControlPlaneHttpError = class extends Error {
306
400
  constructor(httpStatus) {
@@ -308,12 +402,15 @@ var ControlPlaneHttpError = class extends Error {
308
402
  this.httpStatus = httpStatus;
309
403
  this.name = "ControlPlaneHttpError";
310
404
  }
405
+ httpStatus;
311
406
  };
312
407
  var ControlPlaneClient = class {
313
408
  constructor(apiUrl, token) {
314
409
  this.apiUrl = apiUrl;
315
410
  this.token = token;
316
411
  }
412
+ apiUrl;
413
+ token;
317
414
  recordingStatusCommand(id) {
318
415
  return cliCommand(this.apiUrl, "record", "status", id);
319
416
  }
@@ -356,6 +453,9 @@ var ControlPlaneClient = class {
356
453
  createRecording(input) {
357
454
  return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
358
455
  }
456
+ failLocalCapture(recordingId) {
457
+ return this.request(`/api/recordings/${encodeURIComponent(recordingId)}/local-failed`, { method: "POST", body: "{}" });
458
+ }
359
459
  markRecordingStarted(recordingId, input) {
360
460
  return this.request(`/api/recordings/${recordingId}/local-started`, {
361
461
  method: "POST",
@@ -402,7 +502,7 @@ var ControlPlaneClient = class {
402
502
  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) });
403
503
  }
404
504
  async uploadVideo(uploadUrl, filePath) {
405
- const file2 = await stat2(filePath);
505
+ const file2 = await stat3(filePath);
406
506
  if (uploadUrl.startsWith(this.apiUrl)) {
407
507
  const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
408
508
  if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
@@ -424,7 +524,7 @@ var ControlPlaneClient = class {
424
524
  process.stderr.write("\nUpload complete; playback processing may continue.\n");
425
525
  }
426
526
  async uploadDiagnostics(recordingId, filePath, summary) {
427
- const body = await readFile(filePath);
527
+ const body = await readFile2(filePath);
428
528
  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 });
429
529
  }
430
530
  listRecordings() {
@@ -436,16 +536,33 @@ var ControlPlaneClient = class {
436
536
  pollDeviceAuthorization(deviceCode) {
437
537
  return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
438
538
  }
439
- inspect(shareUrl, cursor) {
539
+ async inspect(shareUrl, cursor) {
440
540
  if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
441
541
  const url2 = new URL(shareUrl);
442
542
  const slug = url2.pathname.split("/").filter(Boolean).at(-1);
443
543
  if (!slug) throw new Error("The share URL does not contain a slug.");
444
- return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`, { headers: { Accept: "application/json" } }).then(async (response) => {
544
+ return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`, { headers: await this.reviewHeaders(url2.origin) }).then(async (response) => {
445
545
  if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
446
546
  return response.json();
447
547
  });
448
548
  }
549
+ async reviewHeaders(origin) {
550
+ const headers = new Headers({ Accept: "application/json" });
551
+ if (origin !== new URL(this.apiUrl).origin) return headers;
552
+ const token = typeof this.token === "function" ? await this.token() : this.token;
553
+ if (token) headers.set("Authorization", `Bearer ${token}`);
554
+ this.identifyAgent(headers);
555
+ const id = process.env.RILL_AGENT_ID;
556
+ if (id && /^[a-zA-Z0-9_-]{8,80}$/.test(id)) headers.set("x-rill-agent-id", id);
557
+ if (id && process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1") headers.set("x-rill-analytics", "1");
558
+ return headers;
559
+ }
560
+ identifyAgent(headers) {
561
+ if (process.env.DO_NOT_TRACK === "1" || process.env.RILL_ANONYMOUS_ANALYTICS === "0") headers.set("x-rill-analytics", "0");
562
+ 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);
563
+ if (client) headers.set("x-rill-agent-client", client.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 80));
564
+ 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));
565
+ }
449
566
  reviewPath(shareUrl) {
450
567
  const url2 = new URL(shareUrl);
451
568
  if (url2.origin !== new URL(this.apiUrl).origin) throw new Error("The share URL must match --api-url before sending agent credentials.");
@@ -461,6 +578,7 @@ var ControlPlaneClient = class {
461
578
  }
462
579
  async request(path, init = {}, authenticated = true) {
463
580
  const headers = new Headers(init.headers);
581
+ if (authenticated) this.identifyAgent(headers);
464
582
  if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
465
583
  if (authenticated) {
466
584
  const token = typeof this.token === "function" ? await this.token() : this.token;
@@ -500,8 +618,8 @@ async function whoami(apiUrl) {
500
618
  }
501
619
 
502
620
  // src/remote-encoding.ts
503
- import { open, readFile as readFile2 } from "fs/promises";
504
- import { join as join2 } from "path";
621
+ import { open, readFile as readFile3 } from "fs/promises";
622
+ import { join as join3 } from "path";
505
623
 
506
624
  // ../../node_modules/zod/v4/classic/external.js
507
625
  var external_exports = {};
@@ -1225,9 +1343,9 @@ function floatSafeRemainder(val, step) {
1225
1343
  return ratio - roundedRatio;
1226
1344
  }
1227
1345
  var EVALUATING = /* @__PURE__ */ Symbol("evaluating");
1228
- function defineLazy(object2, key, getter) {
1346
+ function defineLazy(object2, key2, getter) {
1229
1347
  let value = void 0;
1230
- Object.defineProperty(object2, key, {
1348
+ Object.defineProperty(object2, key2, {
1231
1349
  get() {
1232
1350
  if (value === EVALUATING) {
1233
1351
  return void 0;
@@ -1239,7 +1357,7 @@ function defineLazy(object2, key, getter) {
1239
1357
  return value;
1240
1358
  },
1241
1359
  set(v) {
1242
- Object.defineProperty(object2, key, {
1360
+ Object.defineProperty(object2, key2, {
1243
1361
  value: v
1244
1362
  // configurable: true,
1245
1363
  });
@@ -1272,11 +1390,11 @@ function cloneDef(schema) {
1272
1390
  function getElementAtPath(obj, path) {
1273
1391
  if (!path)
1274
1392
  return obj;
1275
- return path.reduce((acc, key) => acc?.[key], obj);
1393
+ return path.reduce((acc, key2) => acc?.[key2], obj);
1276
1394
  }
1277
1395
  function promiseAllObject(promisesObj) {
1278
1396
  const keys = Object.keys(promisesObj);
1279
- const promises = keys.map((key) => promisesObj[key]);
1397
+ const promises = keys.map((key2) => promisesObj[key2]);
1280
1398
  return Promise.all(promises).then((results) => {
1281
1399
  const resolvedObj = {};
1282
1400
  for (let i = 0; i < keys.length; i++) {
@@ -1348,8 +1466,8 @@ function shallowClone(o) {
1348
1466
  }
1349
1467
  function numKeys(data) {
1350
1468
  let keyCount = 0;
1351
- for (const key in data) {
1352
- if (Object.prototype.hasOwnProperty.call(data, key)) {
1469
+ for (const key2 in data) {
1470
+ if (Object.prototype.hasOwnProperty.call(data, key2)) {
1353
1471
  keyCount++;
1354
1472
  }
1355
1473
  }
@@ -1499,13 +1617,13 @@ function pick(schema, mask) {
1499
1617
  const def = mergeDefs(schema._zod.def, {
1500
1618
  get shape() {
1501
1619
  const newShape = {};
1502
- for (const key in mask) {
1503
- if (!(key in currDef.shape)) {
1504
- throw new Error(`Unrecognized key: "${key}"`);
1620
+ for (const key2 in mask) {
1621
+ if (!(key2 in currDef.shape)) {
1622
+ throw new Error(`Unrecognized key: "${key2}"`);
1505
1623
  }
1506
- if (!mask[key])
1624
+ if (!mask[key2])
1507
1625
  continue;
1508
- newShape[key] = currDef.shape[key];
1626
+ newShape[key2] = currDef.shape[key2];
1509
1627
  }
1510
1628
  assignProp(this, "shape", newShape);
1511
1629
  return newShape;
@@ -1524,13 +1642,13 @@ function omit(schema, mask) {
1524
1642
  const def = mergeDefs(schema._zod.def, {
1525
1643
  get shape() {
1526
1644
  const newShape = { ...schema._zod.def.shape };
1527
- for (const key in mask) {
1528
- if (!(key in currDef.shape)) {
1529
- throw new Error(`Unrecognized key: "${key}"`);
1645
+ for (const key2 in mask) {
1646
+ if (!(key2 in currDef.shape)) {
1647
+ throw new Error(`Unrecognized key: "${key2}"`);
1530
1648
  }
1531
- if (!mask[key])
1649
+ if (!mask[key2])
1532
1650
  continue;
1533
- delete newShape[key];
1651
+ delete newShape[key2];
1534
1652
  }
1535
1653
  assignProp(this, "shape", newShape);
1536
1654
  return newShape;
@@ -1547,8 +1665,8 @@ function extend(schema, shape) {
1547
1665
  const hasChecks = checks && checks.length > 0;
1548
1666
  if (hasChecks) {
1549
1667
  const existingShape = schema._zod.def.shape;
1550
- for (const key in shape) {
1551
- if (Object.getOwnPropertyDescriptor(existingShape, key) !== void 0) {
1668
+ for (const key2 in shape) {
1669
+ if (Object.getOwnPropertyDescriptor(existingShape, key2) !== void 0) {
1552
1670
  throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
1553
1671
  }
1554
1672
  }
@@ -1604,23 +1722,23 @@ function partial(Class2, schema, mask) {
1604
1722
  const oldShape = schema._zod.def.shape;
1605
1723
  const shape = { ...oldShape };
1606
1724
  if (mask) {
1607
- for (const key in mask) {
1608
- if (!(key in oldShape)) {
1609
- throw new Error(`Unrecognized key: "${key}"`);
1725
+ for (const key2 in mask) {
1726
+ if (!(key2 in oldShape)) {
1727
+ throw new Error(`Unrecognized key: "${key2}"`);
1610
1728
  }
1611
- if (!mask[key])
1729
+ if (!mask[key2])
1612
1730
  continue;
1613
- shape[key] = Class2 ? new Class2({
1731
+ shape[key2] = Class2 ? new Class2({
1614
1732
  type: "optional",
1615
- innerType: oldShape[key]
1616
- }) : oldShape[key];
1733
+ innerType: oldShape[key2]
1734
+ }) : oldShape[key2];
1617
1735
  }
1618
1736
  } else {
1619
- for (const key in oldShape) {
1620
- shape[key] = Class2 ? new Class2({
1737
+ for (const key2 in oldShape) {
1738
+ shape[key2] = Class2 ? new Class2({
1621
1739
  type: "optional",
1622
- innerType: oldShape[key]
1623
- }) : oldShape[key];
1740
+ innerType: oldShape[key2]
1741
+ }) : oldShape[key2];
1624
1742
  }
1625
1743
  }
1626
1744
  assignProp(this, "shape", shape);
@@ -1636,22 +1754,22 @@ function required(Class2, schema, mask) {
1636
1754
  const oldShape = schema._zod.def.shape;
1637
1755
  const shape = { ...oldShape };
1638
1756
  if (mask) {
1639
- for (const key in mask) {
1640
- if (!(key in shape)) {
1641
- throw new Error(`Unrecognized key: "${key}"`);
1757
+ for (const key2 in mask) {
1758
+ if (!(key2 in shape)) {
1759
+ throw new Error(`Unrecognized key: "${key2}"`);
1642
1760
  }
1643
- if (!mask[key])
1761
+ if (!mask[key2])
1644
1762
  continue;
1645
- shape[key] = new Class2({
1763
+ shape[key2] = new Class2({
1646
1764
  type: "nonoptional",
1647
- innerType: oldShape[key]
1765
+ innerType: oldShape[key2]
1648
1766
  });
1649
1767
  }
1650
1768
  } else {
1651
- for (const key in oldShape) {
1652
- shape[key] = new Class2({
1769
+ for (const key2 in oldShape) {
1770
+ shape[key2] = new Class2({
1653
1771
  type: "nonoptional",
1654
- innerType: oldShape[key]
1772
+ innerType: oldShape[key2]
1655
1773
  });
1656
1774
  }
1657
1775
  }
@@ -3432,13 +3550,13 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
3432
3550
  return payload;
3433
3551
  };
3434
3552
  });
3435
- function handlePropertyResult(result, final, key, input, isOptionalIn, isOptionalOut) {
3436
- const isPresent = key in input;
3553
+ function handlePropertyResult(result, final, key2, input, isOptionalIn, isOptionalOut) {
3554
+ const isPresent = key2 in input;
3437
3555
  if (result.issues.length) {
3438
3556
  if (isOptionalIn && isOptionalOut && !isPresent) {
3439
3557
  return;
3440
3558
  }
3441
- final.issues.push(...prefixIssues(key, result.issues));
3559
+ final.issues.push(...prefixIssues(key2, result.issues));
3442
3560
  }
3443
3561
  if (!isPresent && !isOptionalIn) {
3444
3562
  if (!result.issues.length) {
@@ -3446,17 +3564,17 @@ function handlePropertyResult(result, final, key, input, isOptionalIn, isOptiona
3446
3564
  code: "invalid_type",
3447
3565
  expected: "nonoptional",
3448
3566
  input: void 0,
3449
- path: [key]
3567
+ path: [key2]
3450
3568
  });
3451
3569
  }
3452
3570
  return;
3453
3571
  }
3454
3572
  if (result.value === void 0) {
3455
3573
  if (isPresent) {
3456
- final.value[key] = void 0;
3574
+ final.value[key2] = void 0;
3457
3575
  }
3458
3576
  } else {
3459
- final.value[key] = result.value;
3577
+ final.value[key2] = result.value;
3460
3578
  }
3461
3579
  }
3462
3580
  function normalizeDef(def) {
@@ -3482,20 +3600,20 @@ function handleCatchall(proms, input, payload, ctx, def, inst) {
3482
3600
  const t = _catchall.def.type;
3483
3601
  const isOptionalIn = _catchall.optin === "optional";
3484
3602
  const isOptionalOut = _catchall.optout === "optional";
3485
- for (const key in input) {
3486
- if (key === "__proto__")
3603
+ for (const key2 in input) {
3604
+ if (key2 === "__proto__")
3487
3605
  continue;
3488
- if (keySet.has(key))
3606
+ if (keySet.has(key2))
3489
3607
  continue;
3490
3608
  if (t === "never") {
3491
- unrecognized.push(key);
3609
+ unrecognized.push(key2);
3492
3610
  continue;
3493
3611
  }
3494
- const r = _catchall.run({ value: input[key], issues: [] }, ctx);
3612
+ const r = _catchall.run({ value: input[key2], issues: [] }, ctx);
3495
3613
  if (r instanceof Promise) {
3496
- proms.push(r.then((r2) => handlePropertyResult(r2, payload, key, input, isOptionalIn, isOptionalOut)));
3614
+ proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalIn, isOptionalOut)));
3497
3615
  } else {
3498
- handlePropertyResult(r, payload, key, input, isOptionalIn, isOptionalOut);
3616
+ handlePropertyResult(r, payload, key2, input, isOptionalIn, isOptionalOut);
3499
3617
  }
3500
3618
  }
3501
3619
  if (unrecognized.length) {
@@ -3531,12 +3649,12 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
3531
3649
  defineLazy(inst._zod, "propValues", () => {
3532
3650
  const shape = def.shape;
3533
3651
  const propValues = {};
3534
- for (const key in shape) {
3535
- const field = shape[key]._zod;
3652
+ for (const key2 in shape) {
3653
+ const field = shape[key2]._zod;
3536
3654
  if (field.values) {
3537
- propValues[key] ?? (propValues[key] = /* @__PURE__ */ new Set());
3655
+ propValues[key2] ?? (propValues[key2] = /* @__PURE__ */ new Set());
3538
3656
  for (const v of field.values)
3539
- propValues[key].add(v);
3657
+ propValues[key2].add(v);
3540
3658
  }
3541
3659
  }
3542
3660
  return propValues;
@@ -3559,15 +3677,15 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
3559
3677
  payload.value = {};
3560
3678
  const proms = [];
3561
3679
  const shape = value.shape;
3562
- for (const key of value.keys) {
3563
- const el = shape[key];
3680
+ for (const key2 of value.keys) {
3681
+ const el = shape[key2];
3564
3682
  const isOptionalIn = el._zod.optin === "optional";
3565
3683
  const isOptionalOut = el._zod.optout === "optional";
3566
- const r = el._zod.run({ value: input[key], issues: [] }, ctx);
3684
+ const r = el._zod.run({ value: input[key2], issues: [] }, ctx);
3567
3685
  if (r instanceof Promise) {
3568
- proms.push(r.then((r2) => handlePropertyResult(r2, payload, key, input, isOptionalIn, isOptionalOut)));
3686
+ proms.push(r.then((r2) => handlePropertyResult(r2, payload, key2, input, isOptionalIn, isOptionalOut)));
3569
3687
  } else {
3570
- handlePropertyResult(r, payload, key, input, isOptionalIn, isOptionalOut);
3688
+ handlePropertyResult(r, payload, key2, input, isOptionalIn, isOptionalOut);
3571
3689
  }
3572
3690
  }
3573
3691
  if (!catchall) {
@@ -3583,24 +3701,24 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
3583
3701
  const generateFastpass = (shape) => {
3584
3702
  const doc = new Doc(["shape", "payload", "ctx"]);
3585
3703
  const normalized = _normalized.value;
3586
- const parseStr = (key) => {
3587
- const k = esc(key);
3704
+ const parseStr = (key2) => {
3705
+ const k = esc(key2);
3588
3706
  return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;
3589
3707
  };
3590
3708
  doc.write(`const input = payload.value;`);
3591
3709
  const ids = /* @__PURE__ */ Object.create(null);
3592
3710
  let counter = 0;
3593
- for (const key of normalized.keys) {
3594
- ids[key] = `key_${counter++}`;
3711
+ for (const key2 of normalized.keys) {
3712
+ ids[key2] = `key_${counter++}`;
3595
3713
  }
3596
3714
  doc.write(`const newResult = {};`);
3597
- for (const key of normalized.keys) {
3598
- const id = ids[key];
3599
- const k = esc(key);
3600
- const schema = shape[key];
3715
+ for (const key2 of normalized.keys) {
3716
+ const id = ids[key2];
3717
+ const k = esc(key2);
3718
+ const schema = shape[key2];
3601
3719
  const isOptionalIn = schema?._zod?.optin === "optional";
3602
3720
  const isOptionalOut = schema?._zod?.optout === "optional";
3603
- doc.write(`const ${id} = ${parseStr(key)};`);
3721
+ doc.write(`const ${id} = ${parseStr(key2)};`);
3604
3722
  if (isOptionalIn && isOptionalOut) {
3605
3723
  doc.write(`
3606
3724
  if (${id}.issues.length) {
@@ -3912,17 +4030,17 @@ function mergeValues(a, b) {
3912
4030
  }
3913
4031
  if (isPlainObject(a) && isPlainObject(b)) {
3914
4032
  const bKeys = Object.keys(b);
3915
- const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
4033
+ const sharedKeys = Object.keys(a).filter((key2) => bKeys.indexOf(key2) !== -1);
3916
4034
  const newObj = { ...a, ...b };
3917
- for (const key of sharedKeys) {
3918
- const sharedValue = mergeValues(a[key], b[key]);
4035
+ for (const key2 of sharedKeys) {
4036
+ const sharedValue = mergeValues(a[key2], b[key2]);
3919
4037
  if (!sharedValue.valid) {
3920
4038
  return {
3921
4039
  valid: false,
3922
- mergeErrorPath: [key, ...sharedValue.mergeErrorPath]
4040
+ mergeErrorPath: [key2, ...sharedValue.mergeErrorPath]
3923
4041
  };
3924
4042
  }
3925
- newObj[key] = sharedValue.data;
4043
+ newObj[key2] = sharedValue.data;
3926
4044
  }
3927
4045
  return { valid: true, data: newObj };
3928
4046
  }
@@ -4057,9 +4175,9 @@ var $ZodTuple = /* @__PURE__ */ $constructor("$ZodTuple", (inst, def) => {
4057
4175
  return handleTupleResults(itemResults, payload, items, input, optoutStart);
4058
4176
  };
4059
4177
  });
4060
- function getTupleOptStart(items, key) {
4178
+ function getTupleOptStart(items, key2) {
4061
4179
  for (let i = items.length - 1; i >= 0; i--) {
4062
- if (items[i]._zod[key] !== "optional")
4180
+ if (items[i]._zod[key2] !== "optional")
4063
4181
  return i + 1;
4064
4182
  }
4065
4183
  return 0;
@@ -4110,10 +4228,10 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
4110
4228
  if (values) {
4111
4229
  payload.value = {};
4112
4230
  const recordKeys = /* @__PURE__ */ new Set();
4113
- for (const key of values) {
4114
- if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
4115
- recordKeys.add(typeof key === "number" ? key.toString() : key);
4116
- const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
4231
+ for (const key2 of values) {
4232
+ if (typeof key2 === "string" || typeof key2 === "number" || typeof key2 === "symbol") {
4233
+ recordKeys.add(typeof key2 === "number" ? key2.toString() : key2);
4234
+ const keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
4117
4235
  if (keyResult instanceof Promise) {
4118
4236
  throw new Error("Async schemas not supported in object keys currently");
4119
4237
  }
@@ -4122,34 +4240,34 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
4122
4240
  code: "invalid_key",
4123
4241
  origin: "record",
4124
4242
  issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
4125
- input: key,
4126
- path: [key],
4243
+ input: key2,
4244
+ path: [key2],
4127
4245
  inst
4128
4246
  });
4129
4247
  continue;
4130
4248
  }
4131
4249
  const outKey = keyResult.value;
4132
- const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
4250
+ const result = def.valueType._zod.run({ value: input[key2], issues: [] }, ctx);
4133
4251
  if (result instanceof Promise) {
4134
4252
  proms.push(result.then((result2) => {
4135
4253
  if (result2.issues.length) {
4136
- payload.issues.push(...prefixIssues(key, result2.issues));
4254
+ payload.issues.push(...prefixIssues(key2, result2.issues));
4137
4255
  }
4138
4256
  payload.value[outKey] = result2.value;
4139
4257
  }));
4140
4258
  } else {
4141
4259
  if (result.issues.length) {
4142
- payload.issues.push(...prefixIssues(key, result.issues));
4260
+ payload.issues.push(...prefixIssues(key2, result.issues));
4143
4261
  }
4144
4262
  payload.value[outKey] = result.value;
4145
4263
  }
4146
4264
  }
4147
4265
  }
4148
4266
  let unrecognized;
4149
- for (const key in input) {
4150
- if (!recordKeys.has(key)) {
4267
+ for (const key2 in input) {
4268
+ if (!recordKeys.has(key2)) {
4151
4269
  unrecognized = unrecognized ?? [];
4152
- unrecognized.push(key);
4270
+ unrecognized.push(key2);
4153
4271
  }
4154
4272
  }
4155
4273
  if (unrecognized && unrecognized.length > 0) {
@@ -4162,18 +4280,18 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
4162
4280
  }
4163
4281
  } else {
4164
4282
  payload.value = {};
4165
- for (const key of Reflect.ownKeys(input)) {
4166
- if (key === "__proto__")
4283
+ for (const key2 of Reflect.ownKeys(input)) {
4284
+ if (key2 === "__proto__")
4167
4285
  continue;
4168
- if (!Object.prototype.propertyIsEnumerable.call(input, key))
4286
+ if (!Object.prototype.propertyIsEnumerable.call(input, key2))
4169
4287
  continue;
4170
- let keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
4288
+ let keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
4171
4289
  if (keyResult instanceof Promise) {
4172
4290
  throw new Error("Async schemas not supported in object keys currently");
4173
4291
  }
4174
- const checkNumericKey = typeof key === "string" && number.test(key) && keyResult.issues.length;
4292
+ const checkNumericKey = typeof key2 === "string" && number.test(key2) && keyResult.issues.length;
4175
4293
  if (checkNumericKey) {
4176
- const retryResult = def.keyType._zod.run({ value: Number(key), issues: [] }, ctx);
4294
+ const retryResult = def.keyType._zod.run({ value: Number(key2), issues: [] }, ctx);
4177
4295
  if (retryResult instanceof Promise) {
4178
4296
  throw new Error("Async schemas not supported in object keys currently");
4179
4297
  }
@@ -4183,30 +4301,30 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
4183
4301
  }
4184
4302
  if (keyResult.issues.length) {
4185
4303
  if (def.mode === "loose") {
4186
- payload.value[key] = input[key];
4304
+ payload.value[key2] = input[key2];
4187
4305
  } else {
4188
4306
  payload.issues.push({
4189
4307
  code: "invalid_key",
4190
4308
  origin: "record",
4191
4309
  issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
4192
- input: key,
4193
- path: [key],
4310
+ input: key2,
4311
+ path: [key2],
4194
4312
  inst
4195
4313
  });
4196
4314
  }
4197
4315
  continue;
4198
4316
  }
4199
- const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
4317
+ const result = def.valueType._zod.run({ value: input[key2], issues: [] }, ctx);
4200
4318
  if (result instanceof Promise) {
4201
4319
  proms.push(result.then((result2) => {
4202
4320
  if (result2.issues.length) {
4203
- payload.issues.push(...prefixIssues(key, result2.issues));
4321
+ payload.issues.push(...prefixIssues(key2, result2.issues));
4204
4322
  }
4205
4323
  payload.value[keyResult.value] = result2.value;
4206
4324
  }));
4207
4325
  } else {
4208
4326
  if (result.issues.length) {
4209
- payload.issues.push(...prefixIssues(key, result.issues));
4327
+ payload.issues.push(...prefixIssues(key2, result.issues));
4210
4328
  }
4211
4329
  payload.value[keyResult.value] = result.value;
4212
4330
  }
@@ -4233,15 +4351,15 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
4233
4351
  }
4234
4352
  const proms = [];
4235
4353
  payload.value = /* @__PURE__ */ new Map();
4236
- for (const [key, value] of input) {
4237
- const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
4354
+ for (const [key2, value] of input) {
4355
+ const keyResult = def.keyType._zod.run({ value: key2, issues: [] }, ctx);
4238
4356
  const valueResult = def.valueType._zod.run({ value, issues: [] }, ctx);
4239
4357
  if (keyResult instanceof Promise || valueResult instanceof Promise) {
4240
4358
  proms.push(Promise.all([keyResult, valueResult]).then(([keyResult2, valueResult2]) => {
4241
- handleMapResult(keyResult2, valueResult2, payload, key, input, inst, ctx);
4359
+ handleMapResult(keyResult2, valueResult2, payload, key2, input, inst, ctx);
4242
4360
  }));
4243
4361
  } else {
4244
- handleMapResult(keyResult, valueResult, payload, key, input, inst, ctx);
4362
+ handleMapResult(keyResult, valueResult, payload, key2, input, inst, ctx);
4245
4363
  }
4246
4364
  }
4247
4365
  if (proms.length)
@@ -4249,10 +4367,10 @@ var $ZodMap = /* @__PURE__ */ $constructor("$ZodMap", (inst, def) => {
4249
4367
  return payload;
4250
4368
  };
4251
4369
  });
4252
- function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
4370
+ function handleMapResult(keyResult, valueResult, final, key2, input, inst, ctx) {
4253
4371
  if (keyResult.issues.length) {
4254
- if (propertyKeyTypes.has(typeof key)) {
4255
- final.issues.push(...prefixIssues(key, keyResult.issues));
4372
+ if (propertyKeyTypes.has(typeof key2)) {
4373
+ final.issues.push(...prefixIssues(key2, keyResult.issues));
4256
4374
  } else {
4257
4375
  final.issues.push({
4258
4376
  code: "invalid_key",
@@ -4264,15 +4382,15 @@ function handleMapResult(keyResult, valueResult, final, key, input, inst, ctx) {
4264
4382
  }
4265
4383
  }
4266
4384
  if (valueResult.issues.length) {
4267
- if (propertyKeyTypes.has(typeof key)) {
4268
- final.issues.push(...prefixIssues(key, valueResult.issues));
4385
+ if (propertyKeyTypes.has(typeof key2)) {
4386
+ final.issues.push(...prefixIssues(key2, valueResult.issues));
4269
4387
  } else {
4270
4388
  final.issues.push({
4271
4389
  origin: "map",
4272
4390
  code: "invalid_element",
4273
4391
  input,
4274
4392
  inst,
4275
- key,
4393
+ key: key2,
4276
4394
  issues: valueResult.issues.map((iss) => finalizeIssue(iss, ctx, config()))
4277
4395
  });
4278
4396
  }
@@ -12033,8 +12151,8 @@ function extractDefs(ctx, schema) {
12033
12151
  if (defId)
12034
12152
  seen.defId = defId;
12035
12153
  const schema2 = seen.schema;
12036
- for (const key in schema2) {
12037
- delete schema2[key];
12154
+ for (const key2 in schema2) {
12155
+ delete schema2[key2];
12038
12156
  }
12039
12157
  schema2.$ref = ref;
12040
12158
  };
@@ -12103,20 +12221,20 @@ function finalize(ctx, schema) {
12103
12221
  Object.assign(schema2, _cached);
12104
12222
  const isParentRef = zodSchema._zod.parent === ref;
12105
12223
  if (isParentRef) {
12106
- for (const key in schema2) {
12107
- if (key === "$ref" || key === "allOf")
12224
+ for (const key2 in schema2) {
12225
+ if (key2 === "$ref" || key2 === "allOf")
12108
12226
  continue;
12109
- if (!(key in _cached)) {
12110
- delete schema2[key];
12227
+ if (!(key2 in _cached)) {
12228
+ delete schema2[key2];
12111
12229
  }
12112
12230
  }
12113
12231
  }
12114
12232
  if (refSchema.$ref && refSeen.def) {
12115
- for (const key in schema2) {
12116
- if (key === "$ref" || key === "allOf")
12233
+ for (const key2 in schema2) {
12234
+ if (key2 === "$ref" || key2 === "allOf")
12117
12235
  continue;
12118
- if (key in refSeen.def && JSON.stringify(schema2[key]) === JSON.stringify(refSeen.def[key])) {
12119
- delete schema2[key];
12236
+ if (key2 in refSeen.def && JSON.stringify(schema2[key2]) === JSON.stringify(refSeen.def[key2])) {
12237
+ delete schema2[key2];
12120
12238
  }
12121
12239
  }
12122
12240
  }
@@ -12128,11 +12246,11 @@ function finalize(ctx, schema) {
12128
12246
  if (parentSeen?.schema.$ref) {
12129
12247
  schema2.$ref = parentSeen.schema.$ref;
12130
12248
  if (parentSeen.def) {
12131
- for (const key in schema2) {
12132
- if (key === "$ref" || key === "allOf")
12249
+ for (const key2 in schema2) {
12250
+ if (key2 === "$ref" || key2 === "allOf")
12133
12251
  continue;
12134
- if (key in parentSeen.def && JSON.stringify(schema2[key]) === JSON.stringify(parentSeen.def[key])) {
12135
- delete schema2[key];
12252
+ if (key2 in parentSeen.def && JSON.stringify(schema2[key2]) === JSON.stringify(parentSeen.def[key2])) {
12253
+ delete schema2[key2];
12136
12254
  }
12137
12255
  }
12138
12256
  }
@@ -12233,8 +12351,8 @@ function isTransforming(_schema, _ctx) {
12233
12351
  return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);
12234
12352
  }
12235
12353
  if (def.type === "object") {
12236
- for (const key in def.shape) {
12237
- if (isTransforming(def.shape[key], ctx))
12354
+ for (const key2 in def.shape) {
12355
+ if (isTransforming(def.shape[key2], ctx))
12238
12356
  return true;
12239
12357
  }
12240
12358
  return false;
@@ -12523,15 +12641,15 @@ var objectProcessor = (schema, ctx, _json, params) => {
12523
12641
  json3.type = "object";
12524
12642
  json3.properties = {};
12525
12643
  const shape = def.shape;
12526
- for (const key in shape) {
12527
- json3.properties[key] = process2(shape[key], ctx, {
12644
+ for (const key2 in shape) {
12645
+ json3.properties[key2] = process2(shape[key2], ctx, {
12528
12646
  ...params,
12529
- path: [...params.path, "properties", key]
12647
+ path: [...params.path, "properties", key2]
12530
12648
  });
12531
12649
  }
12532
12650
  const allKeys = new Set(Object.keys(shape));
12533
- const requiredKeys = new Set([...allKeys].filter((key) => {
12534
- const v = def.shape[key]._zod;
12651
+ const requiredKeys = new Set([...allKeys].filter((key2) => {
12652
+ const v = def.shape[key2]._zod;
12535
12653
  if (ctx.io === "input") {
12536
12654
  return v.optin === void 0;
12537
12655
  } else {
@@ -12797,9 +12915,9 @@ function toJSONSchema(input, params) {
12797
12915
  };
12798
12916
  ctx2.external = external;
12799
12917
  for (const entry of registry2._idmap.entries()) {
12800
- const [key, schema] = entry;
12918
+ const [key2, schema] = entry;
12801
12919
  extractDefs(ctx2, schema);
12802
- schemas[key] = finalize(ctx2, schema);
12920
+ schemas[key2] = finalize(ctx2, schema);
12803
12921
  }
12804
12922
  if (Object.keys(defs).length > 0) {
12805
12923
  const defsSegment = ctx2.target === "draft-2020-12" ? "$defs" : "definitions";
@@ -13205,14 +13323,14 @@ function _installLazyMethods(inst, group, methods) {
13205
13323
  if (installed.has(group))
13206
13324
  return;
13207
13325
  installed.add(group);
13208
- for (const key in methods) {
13209
- const fn = methods[key];
13210
- Object.defineProperty(proto, key, {
13326
+ for (const key2 in methods) {
13327
+ const fn = methods[key2];
13328
+ Object.defineProperty(proto, key2, {
13211
13329
  configurable: true,
13212
13330
  enumerable: false,
13213
13331
  get() {
13214
13332
  const bound = fn.bind(this);
13215
- Object.defineProperty(this, key, {
13333
+ Object.defineProperty(this, key2, {
13216
13334
  configurable: true,
13217
13335
  writable: true,
13218
13336
  enumerable: true,
@@ -13221,7 +13339,7 @@ function _installLazyMethods(inst, group, methods) {
13221
13339
  return bound;
13222
13340
  },
13223
13341
  set(v) {
13224
- Object.defineProperty(this, key, {
13342
+ Object.defineProperty(this, key2, {
13225
13343
  configurable: true,
13226
13344
  writable: true,
13227
13345
  enumerable: true,
@@ -14609,11 +14727,11 @@ function resolveRef(ref, ctx) {
14609
14727
  }
14610
14728
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
14611
14729
  if (path[0] === defsKey) {
14612
- const key = path[1];
14613
- if (!key || !ctx.defs[key]) {
14730
+ const key2 = path[1];
14731
+ if (!key2 || !ctx.defs[key2]) {
14614
14732
  throw new Error(`Reference not found: ${ref}`);
14615
14733
  }
14616
- return ctx.defs[key];
14734
+ return ctx.defs[key2];
14617
14735
  }
14618
14736
  throw new Error(`Reference not found: ${ref}`);
14619
14737
  }
@@ -14799,9 +14917,9 @@ function convertBaseSchema(schema, ctx) {
14799
14917
  const shape = {};
14800
14918
  const properties = schema.properties || {};
14801
14919
  const requiredSet = new Set(schema.required || []);
14802
- for (const [key, propSchema] of Object.entries(properties)) {
14920
+ for (const [key2, propSchema] of Object.entries(properties)) {
14803
14921
  const propZodSchema = convertSchema(propSchema, ctx);
14804
- shape[key] = requiredSet.has(key) ? propZodSchema : propZodSchema.optional();
14922
+ shape[key2] = requiredSet.has(key2) ? propZodSchema : propZodSchema.optional();
14805
14923
  }
14806
14924
  if (schema.propertyNames) {
14807
14925
  const keySchema = convertSchema(schema.propertyNames, ctx);
@@ -14942,20 +15060,20 @@ function convertSchema(schema, ctx) {
14942
15060
  }
14943
15061
  const extraMeta = {};
14944
15062
  const coreMetadataKeys = ["$id", "id", "$comment", "$anchor", "$vocabulary", "$dynamicRef", "$dynamicAnchor"];
14945
- for (const key of coreMetadataKeys) {
14946
- if (key in schema) {
14947
- extraMeta[key] = schema[key];
15063
+ for (const key2 of coreMetadataKeys) {
15064
+ if (key2 in schema) {
15065
+ extraMeta[key2] = schema[key2];
14948
15066
  }
14949
15067
  }
14950
15068
  const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
14951
- for (const key of contentMetadataKeys) {
14952
- if (key in schema) {
14953
- extraMeta[key] = schema[key];
15069
+ for (const key2 of contentMetadataKeys) {
15070
+ if (key2 in schema) {
15071
+ extraMeta[key2] = schema[key2];
14954
15072
  }
14955
15073
  }
14956
- for (const key of Object.keys(schema)) {
14957
- if (!RECOGNIZED_KEYS.has(key)) {
14958
- extraMeta[key] = schema[key];
15074
+ for (const key2 of Object.keys(schema)) {
15075
+ if (!RECOGNIZED_KEYS.has(key2)) {
15076
+ extraMeta[key2] = schema[key2];
14959
15077
  }
14960
15078
  }
14961
15079
  if (Object.keys(extraMeta).length > 0) {
@@ -15033,47 +15151,78 @@ var frameManifestSchema = external_exports.object({
15033
15151
  });
15034
15152
 
15035
15153
  // src/remote-encoding.ts
15036
- async function withFrameUploadRetry(upload, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
15154
+ async function withFrameUploadRetry(upload, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), canRetry = () => true) {
15037
15155
  for (let attempt = 0; ; attempt++) {
15038
15156
  try {
15039
15157
  return await upload();
15040
15158
  } catch (error51) {
15041
15159
  const status = error51 instanceof ControlPlaneError || error51 instanceof ControlPlaneHttpError ? error51.httpStatus : void 0;
15042
15160
  const transient = status !== void 0 ? status === 408 || status === 429 || status >= 500 : error51 instanceof TypeError || error51 instanceof DOMException && ["TimeoutError", "AbortError"].includes(error51.name);
15043
- if (!transient || attempt >= 7) throw error51;
15161
+ if (!transient || attempt >= 7 || !canRetry()) throw error51;
15044
15162
  const delay = Math.min(15e3, 1e3 * 2 ** attempt);
15045
15163
  process.stderr.write(`
15046
15164
  Frame upload interrupted; retrying in ${delay / 1e3}s.
15047
15165
  `);
15048
15166
  await sleep(delay);
15167
+ if (!canRetry()) throw error51;
15049
15168
  }
15050
15169
  }
15051
15170
  }
15052
15171
  async function submitFrameCapture(client, id, directory) {
15053
- const manifest = frameManifestSchema.parse(JSON.parse(await readFile2(join2(directory, "capture-manifest.json"), "utf8")));
15172
+ const manifest = frameManifestSchema.parse(JSON.parse(await readFile3(join3(directory, "capture-manifest.json"), "utf8")));
15054
15173
  let status = await client.beginEncoding(id, manifest);
15055
15174
  if (status.status === "failed") status = await client.retryEncoding(id);
15056
15175
  if (status.status !== "uploading") {
15057
15176
  if (["expired", "cancelled"].includes(status.status)) throw new Error(`Capture ${status.status}; it cannot be retried.`);
15058
15177
  return status;
15059
15178
  }
15060
- const file2 = await open(join2(directory, "frames.bin"), "r");
15179
+ const file2 = await open(join3(directory, "frames.bin"), "r");
15061
15180
  try {
15062
15181
  const total = manifest.frames.reduce((n, f) => n + f.size, 0);
15063
15182
  if ((await file2.stat()).size !== total) throw new Error("Capture bytes do not match the manifest.");
15064
- const uploaded = new Set(status.uploadedParts);
15065
- for (let part = 0; part < Math.ceil(total / ENCODING_PART_BYTES); part++) {
15066
- if (uploaded.has(part)) continue;
15067
- const bytes = Buffer.alloc(Math.min(ENCODING_PART_BYTES, total - part * ENCODING_PART_BYTES));
15068
- let offset = 0;
15069
- while (offset < bytes.length) {
15070
- const r = await file2.read(bytes, offset, bytes.length - offset, part * ENCODING_PART_BYTES + offset);
15071
- if (!r.bytesRead) throw new Error("Incomplete capture bytes.");
15072
- offset += r.bytesRead;
15073
- }
15074
- await withFrameUploadRetry(() => client.uploadEncodingPart(id, part, bytes));
15075
- process.stderr.write(`\ruploading frames ${Math.round(Math.min(total, (part + 1) * ENCODING_PART_BYTES) / total * 100)}%`);
15076
- }
15183
+ const parts = Math.ceil(total / ENCODING_PART_BYTES);
15184
+ const uploaded = new Set(status.uploadedParts.filter((part) => Number.isInteger(part) && part >= 0 && part < parts));
15185
+ const missing = Array.from({ length: parts }, (_, part) => part).filter((part) => !uploaded.has(part));
15186
+ let next = 0, failed = false;
15187
+ let firstFailure;
15188
+ let acceptedBytes = [...uploaded].reduce((sum, part) => sum + Math.min(ENCODING_PART_BYTES, total - part * ENCODING_PART_BYTES), 0);
15189
+ let transferredBytes = 0;
15190
+ const startedAt = Date.now();
15191
+ const progress = () => {
15192
+ const seconds = (Date.now() - startedAt) / 1e3;
15193
+ const rate = seconds > 0 ? transferredBytes / seconds : 0;
15194
+ const remaining = rate > 0 ? ` \xB7 about ${Math.ceil((total - acceptedBytes) / rate)}s left` : "";
15195
+ 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}`);
15196
+ };
15197
+ progress();
15198
+ const transfer = async () => {
15199
+ while (!failed) {
15200
+ const part = missing[next++];
15201
+ if (part === void 0) return;
15202
+ try {
15203
+ const bytes = Buffer.alloc(Math.min(ENCODING_PART_BYTES, total - part * ENCODING_PART_BYTES));
15204
+ let offset = 0;
15205
+ while (offset < bytes.length) {
15206
+ const r = await file2.read(bytes, offset, bytes.length - offset, part * ENCODING_PART_BYTES + offset);
15207
+ if (!r.bytesRead) throw new Error("Incomplete capture bytes.");
15208
+ offset += r.bytesRead;
15209
+ }
15210
+ if (failed) return;
15211
+ await withFrameUploadRetry(() => client.uploadEncodingPart(id, part, bytes), void 0, () => !failed);
15212
+ acceptedBytes += bytes.length;
15213
+ transferredBytes += bytes.length;
15214
+ progress();
15215
+ } catch (error51) {
15216
+ if (!failed) {
15217
+ firstFailure = error51;
15218
+ failed = true;
15219
+ }
15220
+ throw error51;
15221
+ }
15222
+ }
15223
+ };
15224
+ await Promise.allSettled(Array.from({ length: Math.min(3, missing.length) }, transfer));
15225
+ if (failed) throw firstFailure;
15077
15226
  } finally {
15078
15227
  await file2.close();
15079
15228
  }
@@ -15093,41 +15242,244 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
15093
15242
  }
15094
15243
 
15095
15244
  // src/cli.ts
15096
- import { spawn as spawn6 } from "child_process";
15097
- import { hostname as hostname4 } from "os";
15245
+ import { spawn as spawn8 } from "child_process";
15246
+ import { hostname as hostname4, tmpdir as tmpdir3 } from "os";
15098
15247
  import { Command } from "commander";
15099
- import { basename, dirname as dirname2 } from "path";
15100
- import { readdir as readdir2, rm as rm5, stat as stat4 } from "fs/promises";
15248
+ import { basename, dirname as dirname2, join as join11 } from "path";
15249
+ import { readdir as readdir2, rm as rm7, stat as stat5 } from "fs/promises";
15250
+
15251
+ // ../recorder/src/windows-ipc.ts
15252
+ import { spawn as spawn3 } from "child_process";
15253
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
15254
+ import { link, lstat, mkdir as mkdir3, readFile as readFile4, rm as rm3, writeFile as writeFile2 } from "fs/promises";
15255
+ import { join as join5, win32 as win322 } from "path";
15256
+
15257
+ // ../recorder/src/paths.ts
15258
+ import { homedir as homedir3, tmpdir } from "os";
15259
+ import { join as join4 } from "path";
15260
+ var rillDirectory = () => process.env.RILL_HOME ?? join4(homedir3(), ".rill");
15261
+ var daemonSocketPath = () => process.env.RILL_SOCKET ?? join4(rillDirectory(), "recorder-v2.sock");
15262
+ var daemonStatePath = () => join4(rillDirectory(), "sessions-v2.json");
15263
+ var recordingTempRoot = () => process.env.RILL_TEMP ?? join4(tmpdir(), "rill-recordings");
15264
+
15265
+ // ../recorder/src/windows-ipc.ts
15266
+ var aclScript = String.raw`$ErrorActionPreference='Stop';try {
15267
+ $path=([Console]::In.ReadToEnd() | ConvertFrom-Json).path
15268
+ $sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User
15269
+ $acl=[System.Security.AccessControl.DirectorySecurity]::new()
15270
+ $acl.SetOwner($sid)
15271
+ $acl.SetAccessRuleProtection($true,$false)
15272
+ $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)
15273
+ $acl.AddAccessRule($rule)
15274
+ ([System.IO.DirectoryInfo]::new($path)).SetAccessControl($acl)
15275
+ [Console]::Out.Write('secured')
15276
+ }catch{[Console]::Error.Write('Recorder directory protection failed.');exit 1}`;
15277
+ async function secureWindowsIpcDirectory(path) {
15278
+ if (process.platform !== "win32") throw new Error("Windows IPC directory protection requires Windows.");
15279
+ await new Promise((resolve, reject) => {
15280
+ const executable = win322.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15281
+ const child = spawn3(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", aclScript], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
15282
+ let output = "", failed = false;
15283
+ const fail2 = () => {
15284
+ if (failed) return;
15285
+ failed = true;
15286
+ child.kill();
15287
+ reject(new Error("Cannot secure the Windows recorder directory. Recording has not started."));
15288
+ };
15289
+ const timer = setTimeout(fail2, 15e3);
15290
+ child.once("error", fail2);
15291
+ child.stdin.once("error", fail2);
15292
+ child.stderr.resume();
15293
+ child.stdout.on("data", (chunk) => {
15294
+ output += chunk.toString("ascii");
15295
+ if (output.length > 1024) fail2();
15296
+ });
15297
+ child.once("close", (code) => {
15298
+ clearTimeout(timer);
15299
+ if (failed) return;
15300
+ if (code !== 0 || output !== "secured") {
15301
+ fail2();
15302
+ return;
15303
+ }
15304
+ resolve();
15305
+ });
15306
+ child.stdin.end(JSON.stringify({ path }));
15307
+ });
15308
+ }
15309
+ function validWindowsIpcIdentity(value) {
15310
+ const v = value;
15311
+ 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);
15312
+ }
15313
+ function acceptsWindowsIpcToken(expected, header) {
15314
+ if (typeof header !== "string" || !/^Bearer [a-f0-9]{64}$/.test(header)) return false;
15315
+ const supplied = header.slice(7);
15316
+ return supplied.length === expected.length && timingSafeEqual(Buffer.from(supplied), Buffer.from(expected));
15317
+ }
15318
+ async function windowsIpcIdentity(directory = join5(rillDirectory(), "windows-ipc-v3"), secure = secureWindowsIpcDirectory) {
15319
+ await mkdir3(directory, { recursive: true, mode: 448 });
15320
+ if ((await lstat(directory)).isSymbolicLink()) throw new Error("Recorder directory cannot be a symbolic link.");
15321
+ await secure(directory);
15322
+ const path = join5(directory, "identity.json");
15323
+ const load = async () => {
15324
+ const info = await lstat(path);
15325
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 4096) throw new Error("Invalid recorder identity.");
15326
+ const value = JSON.parse(await readFile4(path, "utf8"));
15327
+ if (!validWindowsIpcIdentity(value)) throw new Error("Invalid recorder identity.");
15328
+ return value;
15329
+ };
15330
+ try {
15331
+ return await load();
15332
+ } catch (e) {
15333
+ if (e.code !== "ENOENT") throw new Error("Cannot read the protected recorder identity.");
15334
+ }
15335
+ const identity = { version: 1, pipe: "\\\\.\\pipe\\rill-v3-" + randomBytes2(32).toString("hex"), token: randomBytes2(32).toString("hex") };
15336
+ const temporary = join5(directory, randomBytes2(16).toString("hex") + ".tmp");
15337
+ try {
15338
+ await writeFile2(temporary, JSON.stringify(identity), { mode: 384, flag: "wx" });
15339
+ try {
15340
+ await link(temporary, path);
15341
+ } catch (e) {
15342
+ if (e.code !== "EEXIST") throw e;
15343
+ }
15344
+ return await load();
15345
+ } finally {
15346
+ await rm3(temporary, { force: true });
15347
+ }
15348
+ }
15101
15349
 
15102
15350
  // ../recorder/src/client.ts
15103
15351
  import { request as request2 } from "http";
15104
- import { spawn as spawn2 } from "child_process";
15352
+ import { spawn as spawn4 } from "child_process";
15105
15353
 
15106
- // ../recorder/src/paths.ts
15107
- import { homedir as homedir2, tmpdir } from "os";
15108
- import { join as join3 } from "path";
15109
- var rillDirectory = () => process.env.RILL_HOME ?? join3(homedir2(), ".rill");
15110
- var daemonSocketPath = () => process.env.RILL_SOCKET ?? join3(rillDirectory(), "recorder-v2.sock");
15111
- var daemonStatePath = () => join3(rillDirectory(), "sessions-v2.json");
15112
- var recordingTempRoot = () => process.env.RILL_TEMP ?? join3(tmpdir(), "rill-recordings");
15354
+ // ../recorder/src/browser-tabs.ts
15355
+ import { chromium } from "playwright-core";
15356
+
15357
+ // ../recorder/src/redaction.ts
15358
+ var secretKeyPattern = /(?:pass(?:word)?|secret|token|api[_-]?key|authorization|cookie|session|access[_-]?token|refresh[_-]?token|client[_-]?secret)/i;
15359
+ var tokenValuePattern = /\b(?:sk|pk|ghp|github_pat|xox[baprs]|eyJ)[-_a-zA-Z0-9.]{16,}\b/g;
15360
+ function sanitizeUrl(input) {
15361
+ try {
15362
+ const url2 = new URL(input);
15363
+ url2.username = "";
15364
+ url2.password = "";
15365
+ url2.hash = "";
15366
+ for (const key2 of [...url2.searchParams.keys()]) url2.searchParams.set(key2, "[REDACTED]");
15367
+ return url2.toString();
15368
+ } catch {
15369
+ return "[invalid-url]";
15370
+ }
15371
+ }
15372
+ function redactText(value, maxLength = 8e3) {
15373
+ return value.replace(tokenValuePattern, "[REDACTED]").slice(0, maxLength);
15374
+ }
15375
+ function redactBody(value, contentType, maxBytes = 65536) {
15376
+ if (Buffer.byteLength(value) > maxBytes) return { body: void 0, omittedReason: "body_too_large" };
15377
+ if (!isAllowedBodyType(contentType)) return { body: void 0, omittedReason: "unsupported_content_type" };
15378
+ if (contentType?.includes("json")) {
15379
+ try {
15380
+ const parsed = JSON.parse(value);
15381
+ return { body: JSON.stringify(redactJson(parsed)) };
15382
+ } catch {
15383
+ return { body: redactText(value) };
15384
+ }
15385
+ }
15386
+ return { body: redactText(value) };
15387
+ }
15388
+ function isAllowedBodyType(contentType) {
15389
+ if (!contentType) return false;
15390
+ const normalized = contentType.toLowerCase();
15391
+ return normalized.includes("application/json") || normalized.includes("application/problem+json") || normalized.includes("application/graphql-response+json") || normalized.includes("text/plain") || normalized.includes("application/xml") || normalized.includes("text/xml");
15392
+ }
15393
+ function redactJson(value, depth = 0) {
15394
+ if (depth > 12) return "[TRUNCATED]";
15395
+ if (Array.isArray(value)) return value.slice(0, 100).map((item) => redactJson(item, depth + 1));
15396
+ if (value && typeof value === "object") {
15397
+ return Object.fromEntries(Object.entries(value).slice(0, 200).map(([key2, child]) => [key2, secretKeyPattern.test(key2) ? "[REDACTED]" : redactJson(child, depth + 1)]));
15398
+ }
15399
+ if (typeof value === "string") return redactText(value, 8e3);
15400
+ return value;
15401
+ }
15402
+
15403
+ // ../recorder/src/browser-tabs.ts
15404
+ function validateTabSelection(options) {
15405
+ if (options.tabId && !options.cdpUrl) throw new Error("validation_failed: --tab-id requires --cdp-url.");
15406
+ }
15407
+ async function tabs(browser) {
15408
+ const result = [];
15409
+ for (const context of browser.contexts()) for (const page of context.pages()) {
15410
+ if (page.isClosed()) continue;
15411
+ const session = await context.newCDPSession(page).catch(() => null);
15412
+ if (!session) {
15413
+ if (page.isClosed()) continue;
15414
+ throw new Error("browser_unavailable: Could not inspect an open tab.");
15415
+ }
15416
+ try {
15417
+ const { targetInfo } = await session.send("Target.getTargetInfo");
15418
+ result.push({ page, context, tab: { tabId: targetInfo.targetId, url: /^(?:https?:|about:)/i.test(page.url()) ? sanitizeUrl(page.url()) : `[${page.url().split(":", 1)[0]} URL omitted]`, title: redactText(targetInfo.title, 200) } });
15419
+ } catch (error51) {
15420
+ if (!page.isClosed()) throw error51;
15421
+ } finally {
15422
+ await session.detach().catch(() => void 0);
15423
+ }
15424
+ }
15425
+ return result;
15426
+ }
15427
+ async function listBrowserTabs(cdpUrl) {
15428
+ const browser = await chromium.connectOverCDP(cdpUrl, { noDefaults: true });
15429
+ try {
15430
+ return (await tabs(browser)).map((entry) => entry.tab);
15431
+ } finally {
15432
+ await browser.close();
15433
+ }
15434
+ }
15435
+ async function connectRecordingTab(cdpUrl, outputDirectory, tabId) {
15436
+ const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: outputDirectory, isLocal: true, noDefaults: true });
15437
+ try {
15438
+ const available = await tabs(browser);
15439
+ const selected = tabId ? available.find((entry) => entry.tab.tabId === tabId) : available.length === 1 ? available[0] : void 0;
15440
+ if (!selected) {
15441
+ if (tabId) throw new Error("browser_unavailable: The selected tab is no longer available. Run record tabs --cdp-url <endpoint> and choose an open tab.");
15442
+ if (!available.length) throw new Error("browser_unavailable: The attached browser has no page target to record.");
15443
+ throw new Error("validation_failed: Several tabs are open. Run record tabs --cdp-url <endpoint>, then record start --cdp-url <endpoint> --tab-id <tab-id>.");
15444
+ }
15445
+ return { cdpUrl, browser, context: selected.context, page: selected.page, tabId: selected.tab.tabId };
15446
+ } catch (error51) {
15447
+ await browser.close().catch(() => void 0);
15448
+ throw error51;
15449
+ }
15450
+ }
15113
15451
 
15114
15452
  // ../recorder/src/client.ts
15115
- async function ensureDaemon(cliEntryPath) {
15453
+ async function ensureDaemon(cliEntryPath, requireTabSelection = false) {
15454
+ const check2 = (health2) => {
15455
+ if (requireTabSelection && !health2.capabilities?.includes("tab-selection-v1")) throw new Error("browser_unavailable: The running Rill recorder predates tab selection. Stop active recordings, then restart the recorder service before attaching.");
15456
+ };
15457
+ let health;
15116
15458
  try {
15117
- await callDaemon("GET", "/health");
15118
- return;
15459
+ health = await callDaemon("GET", "/health");
15119
15460
  } catch {
15120
15461
  }
15121
- const child = spawn2(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env });
15462
+ if (health) {
15463
+ check2(health);
15464
+ return;
15465
+ }
15466
+ const child = spawn4(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env, windowsHide: true });
15467
+ let startupError;
15468
+ child.once("error", () => {
15469
+ startupError = new Error("The local recorder process could not be launched.");
15470
+ });
15122
15471
  child.unref();
15123
15472
  const deadline = Date.now() + 1e4;
15124
15473
  while (Date.now() < deadline) {
15474
+ if (startupError) throw startupError;
15125
15475
  try {
15126
- await callDaemon("GET", "/health");
15127
- return;
15476
+ health = await callDaemon("GET", "/health");
15128
15477
  } catch {
15129
15478
  await new Promise((resolve) => setTimeout(resolve, 100));
15479
+ continue;
15130
15480
  }
15481
+ check2(health);
15482
+ return;
15131
15483
  }
15132
15484
  throw new Error("The local recorder service did not start.");
15133
15485
  }
@@ -15140,10 +15492,16 @@ function stopRecording(recordingId) {
15140
15492
  function recordingStatus(recordingId) {
15141
15493
  return callDaemon("GET", `/sessions/${encodeURIComponent(recordingId)}`);
15142
15494
  }
15143
- function callDaemon(method, path, body) {
15495
+ var windowsIdentityPromise;
15496
+ async function callDaemon(method, path, body) {
15497
+ 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.");
15498
+ const identity = process.platform === "win32" ? await (windowsIdentityPromise ??= windowsIpcIdentity().catch((error51) => {
15499
+ windowsIdentityPromise = void 0;
15500
+ throw error51;
15501
+ })) : void 0;
15144
15502
  return new Promise((resolve, reject) => {
15145
15503
  const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
15146
- const outgoing = request2({ socketPath: daemonSocketPath(), method, path, headers: payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : void 0 }, (incoming) => {
15504
+ 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) => {
15147
15505
  const chunks = [];
15148
15506
  incoming.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
15149
15507
  incoming.on("end", () => {
@@ -15156,6 +15514,7 @@ function callDaemon(method, path, body) {
15156
15514
  }
15157
15515
  });
15158
15516
  });
15517
+ outgoing.setTimeout(path === "/health" ? 2e3 : 12e4, () => outgoing.destroy(new Error("Local recorder request timed out.")));
15159
15518
  outgoing.once("error", reject);
15160
15519
  if (payload) outgoing.write(payload);
15161
15520
  outgoing.end();
@@ -15163,17 +15522,17 @@ function callDaemon(method, path, body) {
15163
15522
  }
15164
15523
 
15165
15524
  // ../recorder/src/retention.ts
15166
- import { readdir, readFile as readFile3, rm as rm2 } from "fs/promises";
15167
- import { join as join4 } from "path";
15525
+ import { readdir, readFile as readFile5, rm as rm4 } from "fs/promises";
15526
+ import { join as join6 } from "path";
15168
15527
  async function purgeExpiredRemoteCaptures(root, now = Date.now()) {
15169
15528
  const removed = [];
15170
15529
  for (const id of await readdir(root).catch(() => [])) {
15171
15530
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
15172
- const directory = join4(root, id);
15531
+ const directory = join6(root, id);
15173
15532
  try {
15174
- const marker = JSON.parse(await readFile3(join4(directory, "remote-capture.json"), "utf8"));
15533
+ const marker = JSON.parse(await readFile5(join6(directory, "remote-capture.json"), "utf8"));
15175
15534
  if (typeof marker.expiresAt !== "number" || marker.expiresAt > now) continue;
15176
- await rm2(directory, { recursive: true, force: true });
15535
+ await rm4(directory, { recursive: true, force: true });
15177
15536
  removed.push(id);
15178
15537
  } catch {
15179
15538
  }
@@ -15184,7 +15543,7 @@ async function hasRetainedRemoteCaptures(root) {
15184
15543
  for (const id of await readdir(root).catch(() => [])) {
15185
15544
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
15186
15545
  try {
15187
- const marker = JSON.parse(await readFile3(join4(root, id, "remote-capture.json"), "utf8"));
15546
+ const marker = JSON.parse(await readFile5(join6(root, id, "remote-capture.json"), "utf8"));
15188
15547
  if (typeof marker.expiresAt === "number" && Number.isFinite(marker.expiresAt)) return true;
15189
15548
  } catch {
15190
15549
  }
@@ -15194,8 +15553,238 @@ async function hasRetainedRemoteCaptures(root) {
15194
15553
 
15195
15554
  // ../recorder/src/daemon.ts
15196
15555
  import { createServer as createServer2 } from "http";
15197
- import { mkdir as mkdir5, readFile as readFile5, unlink, writeFile as writeFile4, rename, stat as stat3 } from "fs/promises";
15198
- import { dirname, join as join8 } from "path";
15556
+ import { mkdir as mkdir7, readFile as readFile7, unlink, writeFile as writeFile6, rename as rename2, stat as stat4, chmod } from "fs/promises";
15557
+ import { dirname, join as join10 } from "path";
15558
+
15559
+ // ../recorder/src/click-capture.ts
15560
+ import { randomUUID as randomUUID2 } from "crypto";
15561
+ async function captureClicks(page, session, onClick) {
15562
+ const binding = `__rillClick_${randomUUID2().replaceAll("-", "")}`;
15563
+ const source = `(() => {
15564
+ const key = ${JSON.stringify(binding)};
15565
+ if (window[key + '_listener']) return;
15566
+ const listener = event => {
15567
+ const target = event.target;
15568
+ const label = target?.innerText?.trim().slice(0, 80) || target?.getAttribute?.('aria-label') || target?.tagName || 'Element';
15569
+ try { window[key](JSON.stringify({ x: event.clientX, y: event.clientY, label })); } catch {}
15570
+ };
15571
+ window[key + '_listener'] = listener;
15572
+ document.addEventListener('click', listener, { capture: true });
15573
+ })();`;
15574
+ const receive = (event) => {
15575
+ if (event.name !== binding) return;
15576
+ try {
15577
+ const value = JSON.parse(event.payload);
15578
+ if (typeof value.x === "number" && Number.isFinite(value.x) && typeof value.y === "number" && Number.isFinite(value.y) && typeof value.label === "string") onClick({ x: value.x, y: value.y, label: value.label.slice(0, 80) });
15579
+ } catch {
15580
+ }
15581
+ };
15582
+ let identifier;
15583
+ session.on("Runtime.bindingCalled", receive);
15584
+ const cleanup = async () => {
15585
+ session.off("Runtime.bindingCalled", receive);
15586
+ if (identifier) await session.send("Page.removeScriptToEvaluateOnNewDocument", { identifier }).catch(() => void 0);
15587
+ await Promise.all(page.frames().map((frame) => frame.evaluate((key2) => {
15588
+ const target = window;
15589
+ const listener = target[key2 + "_listener"];
15590
+ if (typeof listener === "function") document.removeEventListener("click", listener, { capture: true });
15591
+ delete target[key2 + "_listener"];
15592
+ delete target[key2];
15593
+ }, binding).catch(() => void 0)));
15594
+ await session.send("Runtime.removeBinding", { name: binding }).catch(() => void 0);
15595
+ };
15596
+ try {
15597
+ await session.send("Runtime.enable");
15598
+ await session.send("Runtime.addBinding", { name: binding });
15599
+ ({ identifier } = await session.send("Page.addScriptToEvaluateOnNewDocument", { source }));
15600
+ await Promise.all(page.frames().map((frame) => frame.evaluate(source)));
15601
+ return cleanup;
15602
+ } catch (error51) {
15603
+ await cleanup();
15604
+ throw error51;
15605
+ }
15606
+ }
15607
+
15608
+ // ../contracts/src/cloud-blocklist.ts
15609
+ var blockedCloudDomains = [
15610
+ // Torrent indexes and unofficial software-download destinations.
15611
+ // Explicit domains only: do not block legitimate torrent clients or all downloads.
15612
+ "yasdl.com",
15613
+ "soft98.ir",
15614
+ "downloadha.com",
15615
+ "patoghu.com",
15616
+ "thepiratebay.org",
15617
+ "1337x.to",
15618
+ "yts.mx",
15619
+ "eztvx.to",
15620
+ "fitgirl-repacks.site",
15621
+ "dodi-repacks.site",
15622
+ // Search and reference
15623
+ "google.com",
15624
+ "google.co.uk",
15625
+ "google.co.in",
15626
+ "google.com.ng",
15627
+ "google.ca",
15628
+ "google.com.au",
15629
+ "google.de",
15630
+ "google.fr",
15631
+ "google.co.jp",
15632
+ "google.com.br",
15633
+ "google.es",
15634
+ "bing.com",
15635
+ "yahoo.com",
15636
+ "duckduckgo.com",
15637
+ "baidu.com",
15638
+ "yandex.com",
15639
+ "yandex.ru",
15640
+ "wikipedia.org",
15641
+ "quora.com",
15642
+ // Social networks and media
15643
+ "reddit.com",
15644
+ "redd.it",
15645
+ "facebook.com",
15646
+ "fb.com",
15647
+ "instagram.com",
15648
+ "threads.net",
15649
+ "threads.com",
15650
+ "twitter.com",
15651
+ "x.com",
15652
+ "t.co",
15653
+ "linkedin.com",
15654
+ "tiktok.com",
15655
+ "pinterest.com",
15656
+ "snapchat.com",
15657
+ "youtube.com",
15658
+ "youtu.be",
15659
+ "twitch.tv",
15660
+ "discord.com",
15661
+ "discord.gg",
15662
+ "telegram.org",
15663
+ "t.me",
15664
+ "whatsapp.com",
15665
+ // Shopping and marketplaces
15666
+ "amazon.com",
15667
+ "amazon.co.uk",
15668
+ "amazon.ca",
15669
+ "amazon.de",
15670
+ "amazon.fr",
15671
+ "amazon.in",
15672
+ "amazon.co.jp",
15673
+ "amazon.com.au",
15674
+ "amazon.it",
15675
+ "amazon.es",
15676
+ "ebay.com",
15677
+ "ebay.co.uk",
15678
+ "etsy.com",
15679
+ "walmart.com",
15680
+ "target.com",
15681
+ "aliexpress.com",
15682
+ "alibaba.com",
15683
+ "temu.com",
15684
+ "shein.com",
15685
+ "craigslist.org",
15686
+ "facebookmarketplace.com",
15687
+ "bestbuy.com",
15688
+ "costco.com",
15689
+ "homedepot.com",
15690
+ "wayfair.com",
15691
+ "mercadolibre.com",
15692
+ "shopee.com",
15693
+ "lazada.com",
15694
+ "jiji.ng",
15695
+ "jumia.com.ng",
15696
+ "konga.com",
15697
+ "propertypro.ng",
15698
+ "property24.com",
15699
+ // Travel, property, jobs and business directories
15700
+ "airbnb.com",
15701
+ "airbnb.co.uk",
15702
+ "airbnb.ca",
15703
+ "airbnb.com.au",
15704
+ "airbnb.fr",
15705
+ "airbnb.de",
15706
+ "booking.com",
15707
+ "expedia.com",
15708
+ "tripadvisor.com",
15709
+ "agoda.com",
15710
+ "hotels.com",
15711
+ "kayak.com",
15712
+ "skyscanner.net",
15713
+ "zillow.com",
15714
+ "realtor.com",
15715
+ "rightmove.co.uk",
15716
+ "zoopla.co.uk",
15717
+ "indeed.com",
15718
+ "glassdoor.com",
15719
+ "yelp.com",
15720
+ "yellowpages.com",
15721
+ "trustpilot.com",
15722
+ "crunchbase.com",
15723
+ "g2.com",
15724
+ "capterra.com",
15725
+ "clutch.co",
15726
+ "zoominfo.com",
15727
+ "apollo.io",
15728
+ "monster.com",
15729
+ "ziprecruiter.com",
15730
+ "seek.com.au",
15731
+ "naukri.com",
15732
+ "redfin.com",
15733
+ "trulia.com",
15734
+ "apartments.com",
15735
+ "realtor.ca",
15736
+ "realestate.com.au",
15737
+ "vrbo.com",
15738
+ "trip.com",
15739
+ "trivago.com",
15740
+ "hostelworld.com"
15741
+ ];
15742
+ var blockedCloudTargetMessage = "This website is not supported for cloud runs. Use your own website or app instead.";
15743
+ function isBlockedCloudTarget(value) {
15744
+ try {
15745
+ const hostname5 = new URL(value).hostname.toLowerCase().replace(/\.+$/, "");
15746
+ return blockedCloudDomains.some((domain2) => hostname5 === domain2 || hostname5.endsWith(`.${domain2}`));
15747
+ } catch {
15748
+ return false;
15749
+ }
15750
+ }
15751
+
15752
+ // ../contracts/src/cloud-run.ts
15753
+ var publicTarget = external_exports.string().url().max(2048).refine((value) => {
15754
+ let u;
15755
+ try {
15756
+ u = new URL(value);
15757
+ } catch {
15758
+ return false;
15759
+ }
15760
+ 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");
15761
+ }, "Use a public HTTP(S) URL without credentials, fragments, or a custom port.");
15762
+ var cloudRunTarget = publicTarget.refine((value) => !isBlockedCloudTarget(value), blockedCloudTargetMessage);
15763
+ var interactionFields = {
15764
+ mode: external_exports.enum(["browse", "interact"]).optional(),
15765
+ interactionConfirmed: external_exports.boolean().optional(),
15766
+ // Legacy payload compatibility only; Interact automatically permits API destinations.
15767
+ allowedWriteOrigins: external_exports.array(cloudRunTarget.refine((v) => new URL(v).origin === v, "Use an exact origin.")).max(5).optional()
15768
+ };
15769
+ var sideEffectSchema = external_exports.object({
15770
+ action: external_exports.enum(["fill", "check", "select", "press", "click"]),
15771
+ page: external_exports.string().max(2048),
15772
+ target: external_exports.string().max(160),
15773
+ status: external_exports.enum(["performed", "unknown"])
15774
+ }).strict();
15775
+ var interactionReportFields = {
15776
+ mode: external_exports.enum(["browse", "interact"]).optional(),
15777
+ sideEffects: external_exports.array(sideEffectSchema).max(30).optional(),
15778
+ cleanup: external_exports.literal("not_attempted").optional()
15779
+ };
15780
+ 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();
15781
+ var siteAccountSchema = external_exports.object({
15782
+ name: external_exports.string().regex(/^[a-z0-9-]{1,60}$/),
15783
+ origin: publicTarget.refine((v) => new URL(v).protocol === "https:" && new URL(v).origin === v, "Use an exact HTTPS origin."),
15784
+ loginUrl: publicTarget.refine((v) => new URL(v).protocol === "https:").optional(),
15785
+ username: external_exports.string().min(1).max(320),
15786
+ password: external_exports.string().min(1).max(1e3)
15787
+ }).strict().refine((v) => !v.loginUrl || new URL(v.loginUrl).origin === v.origin, "Login must use the saved website origin.");
15199
15788
 
15200
15789
  // ../contracts/src/recording.ts
15201
15790
  var cursorAppearanceSchema = external_exports.object({
@@ -15236,6 +15825,7 @@ var recordingSchema = external_exports.object({
15236
15825
  })
15237
15826
  });
15238
15827
  var agentOutcomeSchema = external_exports.object({
15828
+ ...interactionReportFields,
15239
15829
  outcome: external_exports.enum(["walkthrough", "passed", "failed", "inconclusive"]),
15240
15830
  summary: external_exports.string().min(1).max(1e3),
15241
15831
  evidence: external_exports.string().max(1e3)
@@ -15323,15 +15913,15 @@ async function attachCursorOverlay(page, cdp, input) {
15323
15913
  const appearance = cursorAppearanceSchema.parse(input ?? {});
15324
15914
  if (!appearance.enabled) return async () => {
15325
15915
  };
15326
- const key = `__rillCursor_${crypto.randomUUID().replaceAll("-", "")}`;
15327
- const source = `(${installCursorOverlay.toString()})(${JSON.stringify({ ...appearance, key })})`;
15916
+ const key2 = `__rillCursor_${crypto.randomUUID().replaceAll("-", "")}`;
15917
+ const source = `(${installCursorOverlay.toString()})(${JSON.stringify({ ...appearance, key: key2 })})`;
15328
15918
  await cdp.send("Page.enable");
15329
15919
  const { identifier } = await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source });
15330
15920
  let stopped = false;
15331
- const removeFromFrame = (frame) => frame.evaluate((key2) => {
15332
- const cleanup2 = window[key2];
15921
+ const removeFromFrame = (frame) => frame.evaluate((key3) => {
15922
+ const cleanup2 = window[key3];
15333
15923
  if (typeof cleanup2 === "function") cleanup2();
15334
- }, key).catch(() => void 0);
15924
+ }, key2).catch(() => void 0);
15335
15925
  const onNavigation = (frame) => {
15336
15926
  if (stopped) return;
15337
15927
  void frame.evaluate(source).then(() => {
@@ -15355,11 +15945,11 @@ async function attachCursorOverlay(page, cdp, input) {
15355
15945
  }
15356
15946
 
15357
15947
  // ../recorder/src/session.ts
15358
- import { chromium } from "playwright-core";
15359
- import { spawn as spawn4 } from "child_process";
15948
+ import { chromium as chromium2 } from "playwright-core";
15949
+ import { spawn as spawn6 } from "child_process";
15360
15950
  import { createServer } from "net";
15361
- import { mkdir as mkdir4, mkdtemp, rm as rm3, writeFile as writeFile3 } from "fs/promises";
15362
- import { join as join7 } from "path";
15951
+ import { mkdir as mkdir6, mkdtemp, rm as rm5, writeFile as writeFile5 } from "fs/promises";
15952
+ import { join as join9 } from "path";
15363
15953
  import { tmpdir as tmpdir2 } from "os";
15364
15954
 
15365
15955
  // ../recorder/src/bundle.ts
@@ -15385,63 +15975,22 @@ async function writeBundle(bundle, outputPath) {
15385
15975
 
15386
15976
  // ../recorder/src/environment.ts
15387
15977
  import { existsSync } from "fs";
15388
- function findChromeExecutable() {
15389
- if (process.env.RILL_CHROME_PATH) return process.env.RILL_CHROME_PATH;
15390
- if (process.platform === "darwin") return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
15391
- const candidates = ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser"];
15392
- return candidates.find((candidate) => existsSync(candidate)) ?? "google-chrome";
15393
- }
15394
-
15395
- // ../recorder/src/redaction.ts
15396
- var secretKeyPattern = /(?:pass(?:word)?|secret|token|api[_-]?key|authorization|cookie|session|access[_-]?token|refresh[_-]?token|client[_-]?secret)/i;
15397
- var tokenValuePattern = /\b(?:sk|pk|ghp|github_pat|xox[baprs]|eyJ)[-_a-zA-Z0-9.]{16,}\b/g;
15398
- function sanitizeUrl(input) {
15399
- try {
15400
- const url2 = new URL(input);
15401
- url2.username = "";
15402
- url2.password = "";
15403
- url2.hash = "";
15404
- for (const key of [...url2.searchParams.keys()]) url2.searchParams.set(key, "[REDACTED]");
15405
- return url2.toString();
15406
- } catch {
15407
- return "[invalid-url]";
15978
+ import { win32 as win323 } from "path";
15979
+ function findChromeExecutable(platform = process.platform, env = process.env, exists = existsSync) {
15980
+ if (env.RILL_CHROME_PATH) return env.RILL_CHROME_PATH;
15981
+ if (platform === "darwin") return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
15982
+ if (platform === "win32") {
15983
+ 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"));
15984
+ return candidates2.find(exists) ?? win323.join(env.PROGRAMFILES ?? env.ProgramFiles ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe");
15408
15985
  }
15409
- }
15410
- function redactText(value, maxLength = 8e3) {
15411
- return value.replace(tokenValuePattern, "[REDACTED]").slice(0, maxLength);
15412
- }
15413
- function redactBody(value, contentType, maxBytes = 65536) {
15414
- if (Buffer.byteLength(value) > maxBytes) return { body: void 0, omittedReason: "body_too_large" };
15415
- if (!isAllowedBodyType(contentType)) return { body: void 0, omittedReason: "unsupported_content_type" };
15416
- if (contentType?.includes("json")) {
15417
- try {
15418
- const parsed = JSON.parse(value);
15419
- return { body: JSON.stringify(redactJson(parsed)) };
15420
- } catch {
15421
- return { body: redactText(value) };
15422
- }
15423
- }
15424
- return { body: redactText(value) };
15425
- }
15426
- function isAllowedBodyType(contentType) {
15427
- if (!contentType) return false;
15428
- const normalized = contentType.toLowerCase();
15429
- return normalized.includes("application/json") || normalized.includes("application/problem+json") || normalized.includes("application/graphql-response+json") || normalized.includes("text/plain") || normalized.includes("application/xml") || normalized.includes("text/xml");
15430
- }
15431
- function redactJson(value, depth = 0) {
15432
- if (depth > 12) return "[TRUNCATED]";
15433
- if (Array.isArray(value)) return value.slice(0, 100).map((item) => redactJson(item, depth + 1));
15434
- if (value && typeof value === "object") {
15435
- return Object.fromEntries(Object.entries(value).slice(0, 200).map(([key, child]) => [key, secretKeyPattern.test(key) ? "[REDACTED]" : redactJson(child, depth + 1)]));
15436
- }
15437
- if (typeof value === "string") return redactText(value, 8e3);
15438
- return value;
15986
+ const candidates = ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", "/usr/bin/chromium-browser"];
15987
+ return candidates.find(exists) ?? "google-chrome";
15439
15988
  }
15440
15989
 
15441
15990
  // ../recorder/src/encoding.ts
15442
- import { open as open2, writeFile, mkdir as mkdir2 } from "fs/promises";
15443
- import { spawn as spawn3 } from "child_process";
15444
- import { join as join5 } from "path";
15991
+ import { open as open2, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
15992
+ import { spawn as spawn5 } from "child_process";
15993
+ import { join as join7 } from "path";
15445
15994
  var active = 0;
15446
15995
  var waiting = [];
15447
15996
  function encodingConcurrency() {
@@ -15449,8 +15998,22 @@ function encodingConcurrency() {
15449
15998
  if (!Number.isInteger(n) || n < 1 || n > 8) throw new Error("Invalid RILL_ENCODING_CONCURRENCY");
15450
15999
  return n;
15451
16000
  }
15452
- async function withEncodingSlot(work) {
15453
- if (active >= encodingConcurrency()) await new Promise((resolve) => waiting.push(resolve));
16001
+ async function withEncodingSlot(work, signal) {
16002
+ signal?.throwIfAborted();
16003
+ if (active >= encodingConcurrency()) await new Promise((resolve, reject) => {
16004
+ const granted = () => {
16005
+ signal?.removeEventListener("abort", abort);
16006
+ resolve();
16007
+ };
16008
+ const abort = () => {
16009
+ const i = waiting.indexOf(granted);
16010
+ if (i >= 0) waiting.splice(i, 1);
16011
+ reject(new Error("encoding_aborted"));
16012
+ };
16013
+ waiting.push(granted);
16014
+ signal?.addEventListener("abort", abort, { once: true });
16015
+ if (signal?.aborted) abort();
16016
+ });
15454
16017
  else active++;
15455
16018
  try {
15456
16019
  return await work();
@@ -15471,11 +16034,15 @@ async function encodeFrames(directory, manifest, output, signal) {
15471
16034
  lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
15472
16035
  }
15473
16036
  lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
15474
- await writeFile(join5(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
16037
+ await writeFile3(join7(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
15475
16038
  const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
15476
16039
  if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
16040
+ signal?.throwIfAborted();
16041
+ 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`;
16042
+ const filters = output.endsWith(".mp4") ? `${dimensions},fps=30,tpad=stop_mode=clone:stop_duration=1` : dimensions;
16043
+ 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"];
15477
16044
  await new Promise((resolve, reject) => {
15478
- 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"] });
16045
+ 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"] });
15479
16046
  let stderr = "";
15480
16047
  child.stderr.on("data", (chunk) => {
15481
16048
  stderr = (stderr + String(chunk)).slice(-2e3);
@@ -15483,6 +16050,7 @@ async function encodeFrames(directory, manifest, output, signal) {
15483
16050
  const abort = () => child.kill("SIGKILL");
15484
16051
  const timer = setTimeout(abort, 20 * 6e4);
15485
16052
  signal?.addEventListener("abort", abort, { once: true });
16053
+ if (signal?.aborted) abort();
15486
16054
  const clean = () => {
15487
16055
  clearTimeout(timer);
15488
16056
  signal?.removeEventListener("abort", abort);
@@ -15496,12 +16064,12 @@ async function encodeFrames(directory, manifest, output, signal) {
15496
16064
  code === 0 ? resolve() : reject(new Error(`ffmpeg failed (${code}): ${stderr}`));
15497
16065
  });
15498
16066
  });
15499
- });
16067
+ }, signal);
15500
16068
  }
15501
16069
 
15502
16070
  // ../recorder/src/screencast.ts
15503
- import { mkdir as mkdir3, writeFile as writeFile2, readFile as readFile4, open as open3 } from "fs/promises";
15504
- import { join as join6 } from "path";
16071
+ import { mkdir as mkdir5, writeFile as writeFile4, readFile as readFile6, open as open3 } from "fs/promises";
16072
+ import { join as join8 } from "path";
15505
16073
  var ScreencastRecorder = class {
15506
16074
  constructor(session, directory, width, height) {
15507
16075
  this.session = session;
@@ -15509,6 +16077,10 @@ var ScreencastRecorder = class {
15509
16077
  this.width = width;
15510
16078
  this.height = height;
15511
16079
  }
16080
+ session;
16081
+ directory;
16082
+ width;
16083
+ height;
15512
16084
  frames = [];
15513
16085
  pendingWrites = /* @__PURE__ */ new Set();
15514
16086
  started = false;
@@ -15516,7 +16088,7 @@ var ScreencastRecorder = class {
15516
16088
  frameBytes = 0;
15517
16089
  captureError;
15518
16090
  async start() {
15519
- await mkdir3(this.directory, { recursive: true, mode: 448 });
16091
+ await mkdir5(this.directory, { recursive: true, mode: 448 });
15520
16092
  this.session.on("Page.screencastFrame", (payload) => {
15521
16093
  const bytes = Buffer.from(payload.data, "base64");
15522
16094
  if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
@@ -15525,9 +16097,9 @@ var ScreencastRecorder = class {
15525
16097
  return;
15526
16098
  }
15527
16099
  this.frameBytes += bytes.length;
15528
- const path = join6(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
16100
+ const path = join8(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
15529
16101
  this.frameCounter += 1;
15530
- const write = writeFile2(path, bytes, { mode: 384 }).then(() => {
16102
+ const write = writeFile4(path, bytes, { mode: 384 }).then(() => {
15531
16103
  this.frames.push({ path, timestamp: payload.metadata.timestamp ?? Date.now() / 1e3 });
15532
16104
  }).catch((error51) => {
15533
16105
  this.captureError = error51 instanceof Error ? error51 : new Error(String(error51));
@@ -15541,7 +16113,7 @@ var ScreencastRecorder = class {
15541
16113
  await this.session.send("Page.startScreencast", { format: "jpeg", quality: 82, maxWidth: this.width, maxHeight: this.height, everyNthFrame: 1 });
15542
16114
  this.started = true;
15543
16115
  }
15544
- async stop(outputPath, expectedDurationSeconds, captureOnly = false) {
16116
+ async stop(outputPath, expectedDurationSeconds, captureOnly = false, signal) {
15545
16117
  if (this.started) await this.session.send("Page.stopScreencast").catch(() => void 0);
15546
16118
  await new Promise((resolve) => setTimeout(resolve, 120));
15547
16119
  await Promise.all(this.pendingWrites);
@@ -15549,10 +16121,10 @@ var ScreencastRecorder = class {
15549
16121
  if (!this.frames.length) throw new Error("No video frames were captured.");
15550
16122
  const ordered = this.frames.toSorted((a, b) => a.timestamp - b.timestamp);
15551
16123
  const frames = [];
15552
- const bundle = await open3(join6(this.directory, "frames.bin"), "w", 384);
16124
+ const bundle = await open3(join8(this.directory, "frames.bin"), "w", 384);
15553
16125
  try {
15554
16126
  for (const frame of ordered) {
15555
- const bytes = await readFile4(frame.path);
16127
+ const bytes = await readFile6(frame.path);
15556
16128
  await bundle.writeFile(bytes);
15557
16129
  frames.push({ timestamp: frame.timestamp, size: bytes.length });
15558
16130
  }
@@ -15560,8 +16132,8 @@ var ScreencastRecorder = class {
15560
16132
  await bundle.close();
15561
16133
  }
15562
16134
  const manifest = frameManifestSchema.parse({ version: 1, durationSeconds: Math.min(600, expectedDurationSeconds), width: this.width, height: this.height, frames });
15563
- await writeFile2(join6(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
15564
- if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath);
16135
+ await writeFile4(join8(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
16136
+ if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath, signal);
15565
16137
  return { frameCount: this.frames.length, outputPath };
15566
16138
  }
15567
16139
  };
@@ -15572,6 +16144,7 @@ var RecordingSession = class _RecordingSession {
15572
16144
  recordingId;
15573
16145
  startedAt = /* @__PURE__ */ new Date();
15574
16146
  cdpUrl;
16147
+ tabId;
15575
16148
  browser;
15576
16149
  context;
15577
16150
  page;
@@ -15585,6 +16158,8 @@ var RecordingSession = class _RecordingSession {
15585
16158
  requestEvents = /* @__PURE__ */ new Map();
15586
16159
  stopPromise;
15587
16160
  maxTimer;
16161
+ maxDurationSeconds;
16162
+ removeClickCapture;
15588
16163
  removeCursorOverlay;
15589
16164
  diagnosticsPaused = false;
15590
16165
  secretValues = [];
@@ -15595,9 +16170,13 @@ var RecordingSession = class _RecordingSession {
15595
16170
  bodyBytes = 0;
15596
16171
  truncated = false;
15597
16172
  constructor(options, runtime) {
16173
+ this.maxDurationSeconds = options.maxDurationSeconds ?? 600;
15598
16174
  this.encoding = options.encoding ?? "local";
16175
+ this.videoFormat = options.videoFormat ?? "webm";
16176
+ this.captureSignal = options.signal;
15599
16177
  this.recordingId = options.recordingId;
15600
16178
  this.cdpUrl = runtime.cdpUrl;
16179
+ this.tabId = runtime.tabId;
15601
16180
  this.browser = runtime.browser;
15602
16181
  this.context = runtime.context;
15603
16182
  this.page = runtime.page;
@@ -15607,17 +16186,28 @@ var RecordingSession = class _RecordingSession {
15607
16186
  this.outputDirectory = options.outputDirectory;
15608
16187
  }
15609
16188
  static async start(options) {
16189
+ validateTabSelection(options);
15610
16190
  const width = options.width ?? 1280;
15611
16191
  const height = options.height ?? 720;
15612
- await mkdir4(options.outputDirectory, { recursive: true, mode: 448 });
15613
- if (options.encoding === "remote") await writeFile3(join7(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
15614
- const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer });
15615
- const cdpSession = await runtime.context.newCDPSession(runtime.page);
15616
- const screencast = new ScreencastRecorder(cdpSession, join7(options.outputDirectory, "frames"), width, height);
16192
+ await mkdir6(options.outputDirectory, { recursive: true, mode: 448 });
16193
+ if (options.encoding === "remote") await writeFile5(join9(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
16194
+ const runtime = options.cdpUrl ? await connectRecordingTab(options.cdpUrl, options.outputDirectory, options.tabId) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer, blockServiceWorkers: options.blockServiceWorkers });
16195
+ let cdpSession;
16196
+ try {
16197
+ cdpSession = await runtime.context.newCDPSession(runtime.page);
16198
+ } catch (error51) {
16199
+ await runtime.browser.close().catch(() => void 0);
16200
+ if ("browserProcess" in runtime && runtime.browserProcess) await terminateBrowser(runtime.browserProcess);
16201
+ if ("profileDirectory" in runtime && runtime.profileDirectory) await rm5(runtime.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
16202
+ throw error51;
16203
+ }
16204
+ const screencast = new ScreencastRecorder(cdpSession, join9(options.outputDirectory, "frames"), width, height);
15617
16205
  const session = new _RecordingSession(options, { ...runtime, screencast });
15618
16206
  try {
15619
- await session.attachDiagnostics(options.captureNetworkBodies ?? false);
15620
- session.removeCursorOverlay = await attachCursorOverlay(runtime.page, cdpSession, options.cursorAppearance);
16207
+ await session.attachDiagnostics(options.captureNetworkBodies ?? false, cdpSession);
16208
+ if (!options.hideCursor) session.removeCursorOverlay = await attachCursorOverlay(runtime.page, cdpSession, options.cursorAppearance);
16209
+ await options.beforeCapture?.(runtime.page);
16210
+ session.startedAt = /* @__PURE__ */ new Date();
15621
16211
  await screencast.start();
15622
16212
  if (options.url) await session.page.goto(options.url, { waitUntil: "domcontentloaded" });
15623
16213
  } catch (error51) {
@@ -15626,7 +16216,7 @@ var RecordingSession = class _RecordingSession {
15626
16216
  }
15627
16217
  session.maxTimer = setTimeout(() => {
15628
16218
  void session.stop("max_duration").catch(() => void 0);
15629
- }, (options.maxDurationSeconds ?? 600) * 1e3);
16219
+ }, Math.max(0, session.startedAt.getTime() + session.maxDurationSeconds * 1e3 - Date.now()));
15630
16220
  return session;
15631
16221
  }
15632
16222
  // In-process agents share this connection so exposed diagnostic bindings are
@@ -15635,7 +16225,18 @@ var RecordingSession = class _RecordingSession {
15635
16225
  return this.page;
15636
16226
  }
15637
16227
  status() {
15638
- return { recordingId: this.recordingId, cdpUrl: this.cdpUrl, startedAt: this.startedAt.toISOString(), status: this.stopPromise ? "finalizing" : "recording" };
16228
+ const elapsedSeconds = Math.max(0, (Date.now() - this.startedAt.getTime()) / 1e3);
16229
+ return {
16230
+ recordingId: this.recordingId,
16231
+ cdpUrl: this.cdpUrl,
16232
+ ...this.tabId ? { tabId: this.tabId } : {},
16233
+ startedAt: this.startedAt.toISOString(),
16234
+ maxDurationSeconds: this.maxDurationSeconds,
16235
+ elapsedSeconds,
16236
+ remainingSeconds: this.stopPromise ? 0 : Math.max(0, this.maxDurationSeconds - elapsedSeconds),
16237
+ stopsAt: new Date(this.startedAt.getTime() + this.maxDurationSeconds * 1e3).toISOString(),
16238
+ status: this.stopPromise ? "finalizing" : "recording"
16239
+ };
15639
16240
  }
15640
16241
  async stop(reason = "agent_request") {
15641
16242
  this.stopPromise ??= this.finalize(reason);
@@ -15645,8 +16246,8 @@ var RecordingSession = class _RecordingSession {
15645
16246
  if (this.maxTimer) clearTimeout(this.maxTimer);
15646
16247
  const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
15647
16248
  const outputDirectory = this.outputDirectory;
15648
- const videoPath = join7(outputDirectory, "recording.webm");
15649
- const diagnosticsPath = join7(outputDirectory, "diagnostics-v1.ndjson.gz");
16249
+ const videoPath = join9(outputDirectory, `recording.${this.videoFormat}`);
16250
+ const diagnosticsPath = join9(outputDirectory, "diagnostics-v1.ndjson.gz");
15650
16251
  const bundle = {
15651
16252
  schemaVersion: 1,
15652
16253
  capturePolicyVersion: "safe-default-v1",
@@ -15663,24 +16264,27 @@ var RecordingSession = class _RecordingSession {
15663
16264
  try {
15664
16265
  const safeBundle = JSON.parse(this.secretValues.reduce((text, secret) => text.split(JSON.stringify(secret).slice(1, -1)).join("[redacted]"), JSON.stringify(bundle)));
15665
16266
  await writeBundle(safeBundle, diagnosticsPath);
15666
- await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote");
16267
+ await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote", this.captureSignal);
15667
16268
  } finally {
15668
- await this.removeCursorOverlay?.();
16269
+ await this.removeClickCapture?.().catch(() => void 0);
16270
+ await this.removeCursorOverlay?.().catch(() => void 0);
16271
+ await this.browser.close().catch(() => void 0);
15669
16272
  if (this.browserProcess) {
15670
- await this.browser.close().catch(() => void 0);
15671
16273
  await terminateBrowser(this.browserProcess);
15672
- if (this.profileDirectory) await rm3(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
16274
+ if (this.profileDirectory) await rm5(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
15673
16275
  }
15674
16276
  }
15675
16277
  const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
15676
16278
  const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
15677
16279
  const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
15678
- 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 } };
16280
+ 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 } };
15679
16281
  }
15680
16282
  captureNetworkBodies = false;
16283
+ captureSignal;
16284
+ videoFormat = "webm";
15681
16285
  encoding = "local";
15682
16286
  outputDirectory = "";
15683
- async attachDiagnostics(captureNetworkBodies) {
16287
+ async attachDiagnostics(captureNetworkBodies, cdpSession) {
15684
16288
  this.captureNetworkBodies = captureNetworkBodies;
15685
16289
  const offset = () => Date.now() - this.startedAt.getTime();
15686
16290
  const pushTimeline = (event) => {
@@ -15691,14 +16295,7 @@ var RecordingSession = class _RecordingSession {
15691
16295
  }
15692
16296
  this.timelineEvents.push({ id: `time_${this.timelineEvents.length + 1}`, ...event });
15693
16297
  };
15694
- await this.page.exposeFunction("__rillClick", (payload) => pushTimeline({ offsetMs: offset(), type: "click", label: `${payload.label || "Element"} at ${Math.round(payload.x)},${Math.round(payload.y)}` })).catch(() => void 0);
15695
- await this.page.addInitScript(() => {
15696
- document.addEventListener("click", (event) => {
15697
- const target = event.target;
15698
- const label = target?.innerText?.trim().slice(0, 80) || target?.getAttribute("aria-label") || target?.tagName || "Element";
15699
- void window.__rillClick?.({ x: event.clientX, y: event.clientY, label });
15700
- }, { capture: true });
15701
- });
16298
+ this.removeClickCapture = await captureClicks(this.page, cdpSession, (payload) => pushTimeline({ offsetMs: offset(), type: "click", label: `${payload.label || "Element"} at ${Math.round(payload.x)},${Math.round(payload.y)}` }));
15702
16299
  this.page.on("framenavigated", (frame) => {
15703
16300
  if (frame === this.page.mainFrame()) pushTimeline({ offsetMs: offset(), type: "navigation", label: sanitizeUrl(frame.url()) });
15704
16301
  });
@@ -15787,7 +16384,7 @@ var RecordingSession = class _RecordingSession {
15787
16384
  };
15788
16385
  async function launchManaged(options) {
15789
16386
  const port = await getAvailablePort();
15790
- const profileDirectory = await mkdtemp(join7(tmpdir2(), "rill-chrome-"));
16387
+ const profileDirectory = await mkdtemp(join9(tmpdir2(), "rill-chrome-"));
15791
16388
  const executable = findChromeExecutable();
15792
16389
  const args = [
15793
16390
  `--remote-debugging-port=${port}`,
@@ -15804,29 +16401,22 @@ async function launchManaged(options) {
15804
16401
  ...options.headed ? [] : ["--headless=new", "--hide-scrollbars"],
15805
16402
  "about:blank"
15806
16403
  ];
15807
- 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;
15808
- const browserProcess = spawn4(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, env });
16404
+ 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;
16405
+ const browserProcess = spawn6(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, windowsHide: true, env });
15809
16406
  const cdpUrl = `http://127.0.0.1:${port}`;
15810
16407
  try {
15811
16408
  await waitForCdp(cdpUrl, browserProcess);
15812
- const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
15813
- const context = browser.contexts()[0];
16409
+ const browser = await chromium2.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
16410
+ const context = options.blockServiceWorkers ? await browser.newContext({ serviceWorkers: "block" }) : browser.contexts()[0];
15814
16411
  const page = context.pages()[0] ?? await context.newPage();
15815
16412
  await page.setViewportSize({ width: options.width, height: options.height });
15816
16413
  return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
15817
16414
  } catch (error51) {
15818
16415
  await terminateBrowser(browserProcess);
15819
- await rm3(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
16416
+ await rm5(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
15820
16417
  throw error51;
15821
16418
  }
15822
16419
  }
15823
- async function connectAttached(cdpUrl, outputDirectory) {
15824
- const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: outputDirectory, isLocal: true, noDefaults: true });
15825
- const context = browser.contexts()[0];
15826
- const page = context.pages()[0];
15827
- if (!page) throw new Error("The attached browser has no page target to record.");
15828
- return { cdpUrl, browser, context, page };
15829
- }
15830
16420
  async function getAvailablePort() {
15831
16421
  const server = createServer();
15832
16422
  await new Promise((resolve, reject) => server.listen(0, "127.0.0.1", resolve).once("error", reject));
@@ -15866,16 +16456,31 @@ async function waitForCdp(cdpUrl, browserProcess) {
15866
16456
  var sessions = /* @__PURE__ */ new Map();
15867
16457
  var lastActivity = Date.now();
15868
16458
  async function runDaemon() {
15869
- const socketPath = daemonSocketPath();
15870
- await mkdir5(dirname(socketPath), { recursive: true, mode: 448 });
15871
- await mkdir5(recordingTempRoot(), { recursive: true, mode: 448 });
16459
+ if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC.");
16460
+ const identity = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
16461
+ const socketPath = identity?.pipe ?? daemonSocketPath();
16462
+ await mkdir7(identity ? dirname(daemonStatePath()) : dirname(socketPath), { recursive: true, mode: 448 });
16463
+ await mkdir7(recordingTempRoot(), { recursive: true, mode: 448 });
15872
16464
  await restoreRecoverableSessions();
15873
16465
  await purgeExpiredCaptures();
15874
- await unlink(socketPath).catch((error51) => {
16466
+ if (!identity) await unlink(socketPath).catch((error51) => {
15875
16467
  if (error51.code !== "ENOENT") throw error51;
15876
16468
  });
15877
- const server = createServer2((request3, response) => void route(request3, response));
15878
- server.listen(socketPath, () => process.platform !== "win32" && import("fs/promises").then(({ chmod }) => chmod(socketPath, 384)));
16469
+ const server = createServer2((request3, response) => {
16470
+ if (identity && !acceptsWindowsIpcToken(identity.token, request3.headers.authorization)) {
16471
+ request3.resume();
16472
+ json2(response, 401, { error: { code: "authentication_required", message: "Recorder authentication required." } });
16473
+ return;
16474
+ }
16475
+ void route(request3, response);
16476
+ });
16477
+ await new Promise((resolve, reject) => {
16478
+ server.once("error", reject);
16479
+ server.listen(socketPath, () => {
16480
+ if (identity) resolve();
16481
+ else void chmod(socketPath, 384).then(resolve, reject);
16482
+ });
16483
+ });
15879
16484
  const idleTimer = setInterval(() => {
15880
16485
  void (async () => {
15881
16486
  await purgeExpiredCaptures();
@@ -15888,7 +16493,7 @@ async function runDaemon() {
15888
16493
  const close = () => {
15889
16494
  clearInterval(idleTimer);
15890
16495
  server.close();
15891
- void unlink(socketPath).catch(() => void 0);
16496
+ if (!identity) void unlink(socketPath).catch(() => void 0);
15892
16497
  };
15893
16498
  process.once("SIGTERM", close);
15894
16499
  process.once("SIGINT", close);
@@ -15896,18 +16501,18 @@ async function runDaemon() {
15896
16501
  async function route(request3, response) {
15897
16502
  lastActivity = Date.now();
15898
16503
  const url2 = new URL(request3.url ?? "/", "http://daemon.local");
15899
- if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid });
16504
+ if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid, capabilities: ["tab-selection-v1"] });
15900
16505
  if (request3.method === "POST" && url2.pathname === "/sessions") {
15901
16506
  if ([...sessions.values()].filter((entry) => entry.state === "recording").length >= 2) return json2(response, 429, { error: { code: "quota_exceeded", message: "This recorder already has two active sessions." } });
15902
16507
  try {
15903
16508
  const input = await readJson(request3);
15904
16509
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
15905
- const outputDirectory = join8(recordingTempRoot(), input.recordingId);
16510
+ const outputDirectory = join10(recordingTempRoot(), input.recordingId);
15906
16511
  const session = await RecordingSession.start({ ...input, outputDirectory });
15907
16512
  const status = session.status();
15908
- const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, startedAt: status.startedAt }, state: "recording" };
16513
+ const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, tabId: status.tabId, startedAt: status.startedAt }, state: "recording" };
15909
16514
  sessions.set(input.recordingId, entry);
15910
- const maxTimer = setTimeout(() => void finalizeEntry(entry, "max_duration"), (input.maxDurationSeconds ?? 600) * 1e3 + 250);
16515
+ const maxTimer = setTimeout(() => void finalizeEntry(entry, "max_duration"), Math.max(0, Date.parse(status.stopsAt) - Date.now()) + 250);
15911
16516
  maxTimer.unref();
15912
16517
  await persistState();
15913
16518
  return json2(response, 201, session.status());
@@ -15942,14 +16547,21 @@ async function finalizeEntry(entry, reason) {
15942
16547
  await persistState();
15943
16548
  }
15944
16549
  function serializeEntry(entry) {
15945
- return { ...entry.base, status: entry.state, result: entry.result, error: entry.error };
16550
+ return {
16551
+ ...entry.base,
16552
+ ...entry.session?.status(),
16553
+ status: entry.state,
16554
+ ...entry.state !== "recording" ? { remainingSeconds: 0, ...entry.result ? { elapsedSeconds: entry.result.durationSeconds } : {} } : {},
16555
+ result: entry.result,
16556
+ error: entry.error
16557
+ };
15946
16558
  }
15947
16559
  async function restoreRecoverableSessions() {
15948
16560
  try {
15949
- const value = JSON.parse(await readFile5(daemonStatePath(), "utf8"));
16561
+ const value = JSON.parse(await readFile7(daemonStatePath(), "utf8"));
15950
16562
  for (const [id, entry] of Object.entries(value)) {
15951
16563
  if (entry.status !== "ready" && entry.status !== "failed") continue;
15952
- sessions.set(id, { base: { recordingId: entry.recordingId, cdpUrl: entry.cdpUrl, startedAt: entry.startedAt }, state: entry.status, result: entry.result, error: entry.error });
16564
+ sessions.set(id, { base: { recordingId: entry.recordingId, cdpUrl: entry.cdpUrl, tabId: entry.tabId, startedAt: entry.startedAt }, state: entry.status, result: entry.result, error: entry.error });
15953
16565
  }
15954
16566
  } catch {
15955
16567
  }
@@ -15957,9 +16569,9 @@ async function restoreRecoverableSessions() {
15957
16569
  async function persistState() {
15958
16570
  const state = Object.fromEntries([...sessions.entries()].map(([id, entry]) => [id, serializeEntry(entry)]));
15959
16571
  const temporary = daemonStatePath() + "." + crypto.randomUUID() + ".tmp";
15960
- await writeFile4(temporary, `${JSON.stringify(state, null, 2)}
16572
+ await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
15961
16573
  `, { mode: 384 });
15962
- await rename(temporary, daemonStatePath());
16574
+ await rename2(temporary, daemonStatePath());
15963
16575
  }
15964
16576
  async function readJson(request3) {
15965
16577
  const chunks = [];
@@ -15980,7 +16592,7 @@ async function purgeExpiredCaptures() {
15980
16592
  const removed = await purgeExpiredRemoteCaptures(recordingTempRoot());
15981
16593
  for (const id of removed) sessions.delete(id);
15982
16594
  let changed = removed.length > 0;
15983
- for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat3(entry.result.frameDirectory).catch(() => null)) {
16595
+ for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat4(entry.result.frameDirectory).catch(() => null)) {
15984
16596
  sessions.delete(id);
15985
16597
  changed = true;
15986
16598
  }
@@ -16025,7 +16637,7 @@ function emit(value, pretty = false) {
16025
16637
  function failureValue(error51) {
16026
16638
  const message = error51 instanceof Error ? error51.message : String(error51);
16027
16639
  const [candidate] = message.split(":", 1);
16028
- 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"]);
16640
+ const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "authorization_not_found", "expired_token", "access_denied", "unsupported_platform", "scope_denied", "quota_exceeded", "browser_unavailable", "local_capture_failed", "recording_not_found", "upload_interrupted", "processing_pending", "processing_failed", "share_revoked", "validation_failed"]);
16029
16641
  const structured = error51 && typeof error51 === "object" ? error51 : {};
16030
16642
  const code = structured.code && knownCodes.has(structured.code) ? structured.code : knownCodes.has(candidate) ? candidate : "internal_error";
16031
16643
  return { error: { code, message, ...structured.recovery ? { recovery: structured.recovery } : {}, ...structured.details ? { details: structured.details } : {} } };
@@ -16092,7 +16704,7 @@ async function withRunFeedback(session, start) {
16092
16704
  }
16093
16705
 
16094
16706
  // src/doctor.ts
16095
- import { spawn as spawn5 } from "child_process";
16707
+ import { spawn as spawn7 } from "child_process";
16096
16708
 
16097
16709
  // src/version.ts
16098
16710
  import { createRequire } from "module";
@@ -16153,7 +16765,7 @@ async function runDoctor(apiUrl, overrides = {}) {
16153
16765
  credentialError = error51;
16154
16766
  }
16155
16767
  if (credentialError) {
16156
- checks.push({ name: "authentication", status: "fail", message: errorMessage(credentialError), recovery: "Unlock the OS credential store, or use RILL_TOKEN. Windows local recording remains unsupported." });
16768
+ 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." });
16157
16769
  } else if (!token) {
16158
16770
  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.` });
16159
16771
  } else {
@@ -16168,7 +16780,7 @@ async function runDoctor(apiUrl, overrides = {}) {
16168
16780
  }
16169
16781
  checks.push(await executableCheck("chrome", dependencies.chromePath, ["--version"], dependencies.commandVersion, "Install Google Chrome or Chromium, or set RILL_CHROME_PATH to its executable."));
16170
16782
  if (dependencies.encoding === "local") {
16171
- 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."));
16783
+ 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."));
16172
16784
  } else {
16173
16785
  checks.push({ name: "ffmpeg", status: "skip", message: "Remote encoding does not require local FFmpeg." });
16174
16786
  try {
@@ -16176,7 +16788,7 @@ async function runDoctor(apiUrl, overrides = {}) {
16176
16788
  if (!(token ? (await dependencies.encodingCapabilities(apiUrl, token)).enabled : health?.anonymousAccess && health.anonymousRemoteEncoding)) throw new Error("Remote encoding is not enabled for this workspace.");
16177
16789
  checks.push({ name: "encoding", status: "pass", message: token ? "Remote encoding is enabled." : "Remote encoding is available for the first anonymous recording." });
16178
16790
  } catch (error51) {
16179
- checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "Remote encoding must be enabled on the server before using the default recording workflow." });
16791
+ checks.push({ name: "encoding", status: "fail", message: errorMessage(error51), recovery: "Run doctor --encoding local to check local FFmpeg, then record start --encoding local, or ask the workspace owner to enable remote encoding." });
16180
16792
  }
16181
16793
  }
16182
16794
  return { schemaVersion: 1, status: checks.some((check2) => check2.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
@@ -16191,7 +16803,7 @@ async function executableCheck(name, path, args, run2, recovery) {
16191
16803
  }
16192
16804
  function commandVersion(command, args) {
16193
16805
  return new Promise((resolve, reject) => {
16194
- const child = spawn5(command, args, { stdio: ["ignore", "pipe", "pipe"] });
16806
+ const child = spawn7(command, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
16195
16807
  const stdout = [];
16196
16808
  const stderr = [];
16197
16809
  const timeout = setTimeout(() => {
@@ -16252,16 +16864,24 @@ function formatCloudRun(run2) {
16252
16864
  }
16253
16865
 
16254
16866
  // src/recording-lifecycle.ts
16255
- async function startRecordingWithCompensation(remote, dependencies) {
16867
+ async function startRecordingWithCompensation(remote, dependencies, apiUrl = "https://userill.dev") {
16256
16868
  try {
16257
16869
  return await dependencies.startLocal();
16258
16870
  } catch (startupError) {
16259
16871
  try {
16260
16872
  await dependencies.cancelRemote(remote.recordingId, "local_start_failed");
16261
16873
  } catch (cleanupError) {
16262
- throw new AggregateError(
16263
- [startupError, cleanupError],
16264
- "Local recording startup failed and the remote reservation could not be cancelled. Run `rill record cancel <recording-id>`."
16874
+ const nextCommand = cliCommand(apiUrl, "record", "cancel", remote.recordingId);
16875
+ throw Object.assign(
16876
+ new AggregateError(
16877
+ [startupError, cleanupError],
16878
+ "browser_unavailable: Local startup failed and its remote reservation could not be cancelled."
16879
+ ),
16880
+ {
16881
+ code: "browser_unavailable",
16882
+ recovery: `Run ${nextCommand} to release this failed startup reservation.`,
16883
+ details: { recordingId: remote.recordingId, nextCommand }
16884
+ }
16265
16885
  );
16266
16886
  }
16267
16887
  throw startupError;
@@ -16279,11 +16899,62 @@ async function remoteAwareRecordingStatus(recordingId, dependencies, apiUrl = "h
16279
16899
  try {
16280
16900
  const local = await dependencies.localStatus(recordingId);
16281
16901
  const status = local.status === "ready" ? "captured" : local.status;
16282
- return { ...local, status, source: "local", ...recordingNextStep(apiUrl, recordingId, String(status)) };
16902
+ return {
16903
+ ...local,
16904
+ status,
16905
+ source: "local",
16906
+ ...recordingNextStep(apiUrl, recordingId, String(status)),
16907
+ ...status === "failed" ? {
16908
+ nextCommand: cliCommand(apiUrl, "record", "cancel", recordingId),
16909
+ 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."
16910
+ } : {}
16911
+ };
16283
16912
  } catch {
16284
16913
  return remoteView;
16285
16914
  }
16286
16915
  }
16916
+ async function releaseFailedLocalCapture(id, dependencies) {
16917
+ const local = await dependencies.localStatus(id).catch(() => null);
16918
+ if (!local || local.recordingId !== id || local.status !== "failed" || local.result) return false;
16919
+ try {
16920
+ await dependencies.failRemote(id);
16921
+ return true;
16922
+ } catch {
16923
+ return false;
16924
+ }
16925
+ }
16926
+ async function createRecordingWithRecovery(dependencies) {
16927
+ try {
16928
+ return await dependencies.createRemote();
16929
+ } catch (error51) {
16930
+ if (!(error51 instanceof ControlPlaneError) || error51.code !== "quota_exceeded") throw error51;
16931
+ const quota = error51.details?.quota;
16932
+ if (!quota || !["credential_concurrency", "workspace_concurrency"].includes(String(quota.type)) || typeof quota.blockingRecordingId !== "string" || !/^rec_[a-zA-Z0-9_-]+$/.test(quota.blockingRecordingId)) throw error51;
16933
+ if (!await releaseFailedLocalCapture(quota.blockingRecordingId, dependencies)) throw error51;
16934
+ dependencies.onRecovered?.(quota.blockingRecordingId);
16935
+ return dependencies.createRemote();
16936
+ }
16937
+ }
16938
+ async function stopRecordingWithCompensation(id, dependencies, apiUrl) {
16939
+ let local;
16940
+ try {
16941
+ local = await dependencies.stopLocal(id);
16942
+ } catch (error51) {
16943
+ const known = await dependencies.localStatus(id).catch(() => null);
16944
+ if (!known || known.recordingId !== id || known.status !== "failed" || known.result) throw error51;
16945
+ local = known;
16946
+ }
16947
+ if (local.recordingId === id && local.status === "failed" && !local.result) {
16948
+ const released = await releaseFailedLocalCapture(id, dependencies);
16949
+ throw new ControlPlaneError(
16950
+ "local_capture_failed",
16951
+ local.error ?? "The local capture could not be finalized.",
16952
+ released ? "Its unused recording slot was released. Start a new recording." : `The recording slot could not be released automatically. Check ${cliCommand(apiUrl, "record", "status", id)} before cancelling it.`,
16953
+ { recordingId: id, reservationReleased: released }
16954
+ );
16955
+ }
16956
+ return local;
16957
+ }
16287
16958
 
16288
16959
  // src/config.ts
16289
16960
  var PRODUCTION_API_URL = "https://userill.dev";
@@ -16295,10 +16966,10 @@ function controlPlaneUrl(environmentUrl) {
16295
16966
  async function removeLocalCapture(id) {
16296
16967
  await ensureDaemon(process.argv[1]);
16297
16968
  const local = await recordingStatus(id).catch(() => null);
16298
- if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
16969
+ if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
16299
16970
  }
16300
16971
  function watchEncoding(apiUrl, id) {
16301
- const child = spawn6(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env } });
16972
+ const child = spawn8(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env }, windowsHide: true });
16302
16973
  child.on("error", () => void 0);
16303
16974
  child.unref();
16304
16975
  }
@@ -16320,7 +16991,7 @@ async function authorize(quiet = false) {
16320
16991
  process.stderr.write(`Open ${authorization.verificationUri} and enter ${authorization.userCode}
16321
16992
  `);
16322
16993
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
16323
- const child = spawn6(opener, [authorization.verificationUri], { stdio: "ignore", detached: true });
16994
+ const child = spawn8(opener, [authorization.verificationUri], { stdio: "ignore", detached: true });
16324
16995
  child.on("error", () => void 0);
16325
16996
  child.unref();
16326
16997
  const deadline = Date.now() + authorization.expiresIn * 1e3;
@@ -16387,9 +17058,14 @@ for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<r
16387
17058
  if (options.json || options.pretty) emit(customerRun(run2), options.pretty);
16388
17059
  else process.stdout.write(formatCloudRun(run2));
16389
17060
  });
16390
- 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) => {
17061
+ record2.command("tabs").requiredOption("--cdp-url <url>", "Existing Chromium CDP endpoint").action(async (commandOptions) => {
17062
+ const options = program.opts();
17063
+ emit({ schemaVersion: 1, tabs: await listBrowserTabs(commandOptions.cdpUrl) }, options.pretty);
17064
+ });
17065
+ 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("--tab-id <id>", "Record this existing tab from record tabs").option("--width <pixels>", "", "1280").option("--height <pixels>", "", "720").option("--max-duration <seconds>", "", "600").action(async (commandOptions) => {
16391
17066
  const options = program.opts();
16392
17067
  assertRecorderTransport();
17068
+ validateTabSelection(commandOptions);
16393
17069
  await ensureAnonymousCredential(options.apiUrl);
16394
17070
  const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16395
17071
  const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
@@ -16398,15 +17074,24 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
16398
17074
  if (!Number.isFinite(Number(commandOptions.maxDuration)) || Number(commandOptions.maxDuration) < 1 || Number(commandOptions.maxDuration) > 600) throw new Error("Recording duration must be between 1 and 600 seconds.");
16399
17075
  const width = Number(commandOptions.width);
16400
17076
  const height = Number(commandOptions.height);
16401
- const remote = await client.createRecording({ title: commandOptions.title ?? `Browser recording \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}`, description: commandOptions.description, sourceUrl: commandOptions.url ?? null, width, height });
17077
+ const metadata = { requestId: crypto.randomUUID(), title: commandOptions.title ?? `Browser recording \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}`, description: commandOptions.description, sourceUrl: commandOptions.url ?? null, width, height };
17078
+ const remote = await createRecordingWithRecovery({
17079
+ createRemote: () => client.createRecording(metadata),
17080
+ localStatus: recordingStatus,
17081
+ failRemote: (id) => client.failLocalCapture(id),
17082
+ onRecovered: (id) => {
17083
+ process.stderr.write(`Released the unused slot for failed local capture ${id}.
17084
+ `);
17085
+ }
17086
+ });
16402
17087
  rememberRecording(remote.recordingId);
16403
17088
  const local = await startRecordingWithCompensation(remote, {
16404
17089
  startLocal: async () => {
16405
- await ensureDaemon(process.argv[1]);
16406
- 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 });
17090
+ await ensureDaemon(process.argv[1], Boolean(commandOptions.cdpUrl));
17091
+ return startRecording({ encoding: commandOptions.encoding, recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, tabId: commandOptions.tabId, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600), captureNetworkBodies: remote.bodyCaptureEnabled, cursorAppearance: remote.cursorAppearance });
16407
17092
  },
16408
17093
  cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
16409
- });
17094
+ }, options.apiUrl);
16410
17095
  await client.markRecordingStarted(remote.recordingId, {
16411
17096
  cliVersion: CLI_VERSION,
16412
17097
  os: process.platform,
@@ -16414,16 +17099,20 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
16414
17099
  headed: Boolean(commandOptions.headed),
16415
17100
  attached: Boolean(commandOptions.cdpUrl)
16416
17101
  }).catch(() => void 0);
16417
- return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, browserAttachment: browserAttachment(local.cdpUrl), bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
17102
+ return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, ...local.tabId ? { tabId: local.tabId } : {}, browserAttachment: browserAttachment(local.cdpUrl, local.tabId), startedAt: local.startedAt, stopsAt: local.stopsAt, elapsedSeconds: local.elapsedSeconds, remainingSeconds: local.remainingSeconds, bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
16418
17103
  });
16419
17104
  emit(result, options.pretty);
16420
17105
  });
16421
17106
  record2.command("stop").argument("<recording-id>").option("--no-wait").option("--no-feedback", "Disable feedback only when the user explicitly requests it").action(async (recordingId, commandOptions) => {
16422
17107
  const options = program.opts();
16423
17108
  await ensureDaemon(process.argv[1]);
16424
- const local = await stopRecording(recordingId);
16425
- if (!local.result) throw new Error(local.error ?? "The recorder did not produce an artifact.");
16426
17109
  const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
17110
+ const local = await stopRecordingWithCompensation(recordingId, {
17111
+ stopLocal: stopRecording,
17112
+ localStatus: recordingStatus,
17113
+ failRemote: (id) => client.failLocalCapture(id)
17114
+ }, options.apiUrl);
17115
+ if (!local.result) throw new Error(local.error ?? "The recorder did not produce an artifact.");
16427
17116
  const stopped = await client.markRecordingStopped(recordingId, {
16428
17117
  durationSeconds: local.result.durationSeconds,
16429
17118
  stoppedReason: local.result.stoppedReason,
@@ -16435,11 +17124,11 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
16435
17124
  const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
16436
17125
  const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
16437
17126
  emit({ schemaVersion: 1, ...result2, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
16438
- if (result2.status === "ready") await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
17127
+ if (result2.status === "ready") await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
16439
17128
  else watchEncoding(options.apiUrl, recordingId);
16440
17129
  return;
16441
17130
  }
16442
- const video = await stat4(local.result.videoPath);
17131
+ const video = await stat5(local.result.videoPath);
16443
17132
  const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
16444
17133
  await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
16445
17134
  const remote = commandOptions.wait === false || record2.opts().wait === false ? null : await client.waitUntilPlayable(recordingId);
@@ -16457,7 +17146,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
16457
17146
  ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
16458
17147
  };
16459
17148
  emit(result, options.pretty);
16460
- if (remote) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
17149
+ if (remote) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
16461
17150
  });
16462
17151
  record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
16463
17152
  const options = program.opts();
@@ -16489,7 +17178,7 @@ program.command("encoding-watch", { hidden: true }).argument("<recording-id>").a
16489
17178
  if (result.status === "ready") {
16490
17179
  await ensureDaemon(process.argv[1]);
16491
17180
  const local = await recordingStatus(id).catch(() => null);
16492
- if (local?.result?.frameDirectory) await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
17181
+ if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
16493
17182
  }
16494
17183
  });
16495
17184
  var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
@@ -16544,7 +17233,7 @@ program.command("upload").argument("<video-file>").option("--title <title>").opt
16544
17233
  const options = program.opts();
16545
17234
  await ensureAnonymousCredential(options.apiUrl);
16546
17235
  const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
16547
- const file2 = await stat4(videoFile);
17236
+ const file2 = await stat5(videoFile);
16548
17237
  const recording = await client.createRecording({ title: commandOptions.title ?? basename(videoFile), sourceUrl: null });
16549
17238
  const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file2.size, filename: basename(videoFile), uploadKind: "existing_video" });
16550
17239
  await client.uploadVideo(upload.uploadUrl, videoFile);
@@ -16573,20 +17262,20 @@ comments.command("edit").argument("<share-url>").requiredOption("--comment <id>"
16573
17262
  var drafts = program.command("drafts");
16574
17263
  drafts.command("list").action(async () => {
16575
17264
  const options = program.opts();
16576
- const root = process.env.RILL_TEMP ?? `${process.env.TMPDIR ?? "/tmp"}/rill-recordings`;
17265
+ const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
16577
17266
  const names = await readdir2(root).catch(() => []);
16578
17267
  emit({ schemaVersion: 1, drafts: names }, options.pretty);
16579
17268
  });
16580
17269
  drafts.command("purge").action(async () => {
16581
17270
  const options = program.opts();
16582
- const root = process.env.RILL_TEMP ?? `${process.env.TMPDIR ?? "/tmp"}/rill-recordings`;
17271
+ const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
16583
17272
  const names = await readdir2(root).catch(() => []);
16584
17273
  let purged = 0;
16585
17274
  for (const name of names) {
16586
- const path = `${root}/${name}`;
16587
- const metadata = await stat4(path);
17275
+ const path = join11(root, name);
17276
+ const metadata = await stat5(path);
16588
17277
  if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
16589
- await rm5(path, { recursive: true, force: true });
17278
+ await rm7(path, { recursive: true, force: true });
16590
17279
  purged += 1;
16591
17280
  }
16592
17281
  }