@runalabs/rill-cli 0.1.18 → 0.1.20

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 +21 -2
  2. package/dist/cli.js +357 -98
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @runalabs/rill-cli
2
2
 
3
+ ## Runtime security requirement
4
+
5
+ Use Node.js **24.22.1 LTS or newer on a patched release line**. The minimum
6
+ supported versions are **22.23.4**, **24.22.1**, and **26.12.1**; Node 23, Node 25,
7
+ and prerelease builds are not supported. These releases address the October 2026
8
+ [eMudhra certificate compromise](https://emudhra.we-have-a-problem.com/).
9
+
10
+ Rill 0.1.20 checks Node before command actions, authentication, or telemetry.
11
+ An unsupported runtime receives `unsupported_node_version` and an upgrade step.
12
+ `rill doctor` reports that failure without contacting the API or reading saved
13
+ credentials. `rill --help` and `rill --version` remain available.
14
+
15
+ The CLI uses your installed Node and Chrome/Chromium. Updating Rill alone does
16
+ not replace their certificate stores. Keep Chrome/Chromium updated, and restart
17
+ long-running recorder processes after saving active recordings when updating
18
+ their runtimes. Doctor's browser version check confirms availability, not that
19
+ every certificate revocation is enforced. Rill does not disable TLS validation
20
+ or accept a custom CA workaround in place of the patched Node requirement.
21
+
3
22
  Record a browser flow, keep its video and runtime evidence together, and return an inspectable share link. Use Rill to reproduce a bug, verify a change, or hand a browser run to another developer or agent.
4
23
 
5
24
  Rill starts a local Chromium recorder. Your browser tool drives the returned CDP endpoint; Rill captures the selected page and uploads the result when you stop.
@@ -8,7 +27,7 @@ Rill starts a local Chromium recorder. Your browser tool drives the returned CDP
8
27
 
9
28
  ## Cloud agent runs
10
29
 
11
- Invited workspaces can supply a URL and prompt. Cloud runs need Node.js 22+ and a Rill login; Chrome and ffmpeg run on Rill's server.
30
+ Invited workspaces can supply a URL and prompt. Cloud runs need a patched Node.js version listed above and a Rill login; Chrome and ffmpeg run on Rill's server.
12
31
 
13
32
  ```sh
14
33
  rill record https://example.com -p "Open pricing and describe the plans."
@@ -29,7 +48,7 @@ Check `node --version`, `node -p "process.platform"`, and `rill --version` befor
29
48
 
30
49
  If the CLI is missing, check npm with `npm ping --fetch-timeout=5000 --fetch-retries=0` before installation. See the [canonical skill](https://userill.dev/skill.md) for the bounded Rill connectivity check and Windows Terminal fallback. Report blocked network access instead of repeating installation attempts.
31
50
 
32
- Browser recording runs on macOS, Linux, and Windows. Install Node.js 22+ and Google Chrome or Chromium. Rill encodes recordings remotely by default; local FFmpeg is optional. Chrome stays on your machine and can access localhost. Raw frames upload after recording stops.
51
+ Browser recording runs on macOS, Linux, and Windows. Install a patched Node.js version listed above and up-to-date Google Chrome or Chromium. Rill encodes recordings remotely by default; local FFmpeg is optional. Chrome stays on your machine and can access localhost. Raw frames upload after recording stops.
33
52
 
34
53
  Then install the scoped package:
35
54
 
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
3
+ var __export = (target2, all) => {
4
4
  for (var name in all)
5
- __defProp(target, name, { get: all[name], enumerable: true });
5
+ __defProp(target2, name, { get: all[name], enumerable: true });
6
6
  };
7
7
 
8
8
  // src/windows-credentials.ts
@@ -1390,8 +1390,8 @@ function defineLazy(object2, key2, getter) {
1390
1390
  function objectClone(obj) {
1391
1391
  return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
1392
1392
  }
1393
- function assignProp(target, prop, value) {
1394
- Object.defineProperty(target, prop, {
1393
+ function assignProp(target2, prop, value) {
1394
+ Object.defineProperty(target2, prop, {
1395
1395
  value,
1396
1396
  writable: true,
1397
1397
  enumerable: true,
@@ -1574,35 +1574,35 @@ function normalizeParams(_params) {
1574
1574
  return params;
1575
1575
  }
1576
1576
  function createTransparentProxy(getter) {
1577
- let target;
1577
+ let target2;
1578
1578
  return new Proxy({}, {
1579
1579
  get(_, prop, receiver) {
1580
- target ?? (target = getter());
1581
- return Reflect.get(target, prop, receiver);
1580
+ target2 ?? (target2 = getter());
1581
+ return Reflect.get(target2, prop, receiver);
1582
1582
  },
1583
1583
  set(_, prop, value, receiver) {
1584
- target ?? (target = getter());
1585
- return Reflect.set(target, prop, value, receiver);
1584
+ target2 ?? (target2 = getter());
1585
+ return Reflect.set(target2, prop, value, receiver);
1586
1586
  },
1587
1587
  has(_, prop) {
1588
- target ?? (target = getter());
1589
- return Reflect.has(target, prop);
1588
+ target2 ?? (target2 = getter());
1589
+ return Reflect.has(target2, prop);
1590
1590
  },
1591
1591
  deleteProperty(_, prop) {
1592
- target ?? (target = getter());
1593
- return Reflect.deleteProperty(target, prop);
1592
+ target2 ?? (target2 = getter());
1593
+ return Reflect.deleteProperty(target2, prop);
1594
1594
  },
1595
1595
  ownKeys(_) {
1596
- target ?? (target = getter());
1597
- return Reflect.ownKeys(target);
1596
+ target2 ?? (target2 = getter());
1597
+ return Reflect.ownKeys(target2);
1598
1598
  },
1599
1599
  getOwnPropertyDescriptor(_, prop) {
1600
- target ?? (target = getter());
1601
- return Reflect.getOwnPropertyDescriptor(target, prop);
1600
+ target2 ?? (target2 = getter());
1601
+ return Reflect.getOwnPropertyDescriptor(target2, prop);
1602
1602
  },
1603
1603
  defineProperty(_, prop, descriptor) {
1604
- target ?? (target = getter());
1605
- return Reflect.defineProperty(target, prop, descriptor);
1604
+ target2 ?? (target2 = getter());
1605
+ return Reflect.defineProperty(target2, prop, descriptor);
1606
1606
  }
1607
1607
  });
1608
1608
  }
@@ -2237,10 +2237,10 @@ var nanoid = /^[a-zA-Z0-9_-]{21}$/;
2237
2237
  var duration = /^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/;
2238
2238
  var extendedDuration = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/;
2239
2239
  var guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
2240
- var uuid = (version2) => {
2241
- if (!version2)
2240
+ var uuid = (version3) => {
2241
+ if (!version3)
2242
2242
  return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;
2243
- return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version2}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);
2243
+ return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version3}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);
2244
2244
  };
2245
2245
  var uuid4 = /* @__PURE__ */ uuid(4);
2246
2246
  var uuid6 = /* @__PURE__ */ uuid(6);
@@ -11701,10 +11701,10 @@ function _slugify() {
11701
11701
  return /* @__PURE__ */ _overwrite((input) => slugify(input));
11702
11702
  }
11703
11703
  // @__NO_SIDE_EFFECTS__
11704
- function _array(Class2, element, params) {
11704
+ function _array(Class2, element2, params) {
11705
11705
  return new Class2({
11706
11706
  type: "array",
11707
- element,
11707
+ element: element2,
11708
11708
  // get element() {
11709
11709
  // return element;
11710
11710
  // },
@@ -12054,15 +12054,15 @@ function _stringFormat(Class2, format, fnOrRegex, _params = {}) {
12054
12054
 
12055
12055
  // ../../node_modules/zod/v4/core/to-json-schema.js
12056
12056
  function initializeContext(params) {
12057
- let target = params?.target ?? "draft-2020-12";
12058
- if (target === "draft-4")
12059
- target = "draft-04";
12060
- if (target === "draft-7")
12061
- target = "draft-07";
12057
+ let target2 = params?.target ?? "draft-2020-12";
12058
+ if (target2 === "draft-4")
12059
+ target2 = "draft-04";
12060
+ if (target2 === "draft-7")
12061
+ target2 = "draft-07";
12062
12062
  return {
12063
12063
  processors: params.processors ?? {},
12064
12064
  metadataRegistry: params?.metadata ?? globalRegistry,
12065
- target,
12065
+ target: target2,
12066
12066
  unrepresentable: params?.unrepresentable ?? "throw",
12067
12067
  override: params?.override ?? (() => {
12068
12068
  }),
@@ -12404,8 +12404,8 @@ var createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
12404
12404
  return finalize(ctx, schema);
12405
12405
  };
12406
12406
  var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {
12407
- const { libraryOptions, target } = params ?? {};
12408
- const ctx = initializeContext({ ...libraryOptions ?? {}, target, io, processors });
12407
+ const { libraryOptions, target: target2 } = params ?? {};
12408
+ const ctx = initializeContext({ ...libraryOptions ?? {}, target: target2, io, processors });
12409
12409
  process2(schema, ctx);
12410
12410
  extractDefs(ctx, schema);
12411
12411
  return finalize(ctx, schema);
@@ -13464,8 +13464,8 @@ var ZodType = /* @__PURE__ */ $constructor("ZodType", (inst, def) => {
13464
13464
  catch(params) {
13465
13465
  return _catch2(this, params);
13466
13466
  },
13467
- pipe(target) {
13468
- return pipe(this, target);
13467
+ pipe(target2) {
13468
+ return pipe(this, target2);
13469
13469
  },
13470
13470
  readonly() {
13471
13471
  return readonly(this);
@@ -13986,8 +13986,8 @@ var ZodArray = /* @__PURE__ */ $constructor("ZodArray", (inst, def) => {
13986
13986
  }
13987
13987
  });
13988
13988
  });
13989
- function array(element, params) {
13990
- return _array(ZodArray, element, params);
13989
+ function array(element2, params) {
13990
+ return _array(ZodArray, element2, params);
13991
13991
  }
13992
13992
  function keyof(schema) {
13993
13993
  const shape = schema._zod.def.shape;
@@ -15024,8 +15024,8 @@ function convertBaseSchema(schema, ctx) {
15024
15024
  zodSchema = zodSchema.check(z.maxLength(schema.maxItems));
15025
15025
  }
15026
15026
  } else if (items !== void 0) {
15027
- const element = convertSchema(items, ctx);
15028
- let arraySchema = z.array(element);
15027
+ const element2 = convertSchema(items, ctx);
15028
+ let arraySchema = z.array(element2);
15029
15029
  if (typeof schema.minItems === "number") {
15030
15030
  arraySchema = arraySchema.min(schema.minItems);
15031
15031
  }
@@ -15116,10 +15116,10 @@ function fromJSONSchema(schema, params) {
15116
15116
  } catch {
15117
15117
  throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
15118
15118
  }
15119
- const version2 = detectVersion(normalized, params?.defaultTarget);
15119
+ const version3 = detectVersion(normalized, params?.defaultTarget);
15120
15120
  const defs = normalized.$defs || normalized.definitions || {};
15121
15121
  const ctx = {
15122
- version: version2,
15122
+ version: version3,
15123
15123
  defs,
15124
15124
  refs: /* @__PURE__ */ new Map(),
15125
15125
  processing: /* @__PURE__ */ new Set(),
@@ -15354,10 +15354,10 @@ async function windowsIpcIdentity(directory = join5(rillDirectory(), "windows-ip
15354
15354
  } catch (e) {
15355
15355
  if (e.code !== "ENOENT") throw new Error("Cannot read the protected recorder identity.");
15356
15356
  }
15357
- const identity = { version: 1, pipe: "\\\\.\\pipe\\rill-v3-" + randomBytes2(32).toString("hex"), token: randomBytes2(32).toString("hex") };
15357
+ const identity2 = { version: 1, pipe: "\\\\.\\pipe\\rill-v3-" + randomBytes2(32).toString("hex"), token: randomBytes2(32).toString("hex") };
15358
15358
  const temporary = join5(directory, randomBytes2(16).toString("hex") + ".tmp");
15359
15359
  try {
15360
- await writeFile2(temporary, JSON.stringify(identity), { mode: 384, flag: "wx" });
15360
+ await writeFile2(temporary, JSON.stringify(identity2), { mode: 384, flag: "wx" });
15361
15361
  try {
15362
15362
  await link(temporary, path);
15363
15363
  } catch (e) {
@@ -15428,16 +15428,16 @@ function validateTabSelection(options) {
15428
15428
  }
15429
15429
  async function tabs(browser) {
15430
15430
  const result = [];
15431
- for (const context of browser.contexts()) for (const page of context.pages()) {
15431
+ for (const context2 of browser.contexts()) for (const page of context2.pages()) {
15432
15432
  if (page.isClosed()) continue;
15433
- const session = await context.newCDPSession(page).catch(() => null);
15433
+ const session = await context2.newCDPSession(page).catch(() => null);
15434
15434
  if (!session) {
15435
15435
  if (page.isClosed()) continue;
15436
15436
  throw new Error("browser_unavailable: Could not inspect an open tab.");
15437
15437
  }
15438
15438
  try {
15439
15439
  const { targetInfo } = await session.send("Target.getTargetInfo");
15440
- 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) } });
15440
+ result.push({ page, context: context2, 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) } });
15441
15441
  } catch (error51) {
15442
15442
  if (!page.isClosed()) throw error51;
15443
15443
  } finally {
@@ -15517,13 +15517,13 @@ function recordingStatus(recordingId) {
15517
15517
  var windowsIdentityPromise;
15518
15518
  async function callDaemon(method, path, body) {
15519
15519
  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.");
15520
- const identity = process.platform === "win32" ? await (windowsIdentityPromise ??= windowsIpcIdentity().catch((error51) => {
15520
+ const identity2 = process.platform === "win32" ? await (windowsIdentityPromise ??= windowsIpcIdentity().catch((error51) => {
15521
15521
  windowsIdentityPromise = void 0;
15522
15522
  throw error51;
15523
15523
  })) : void 0;
15524
15524
  return new Promise((resolve, reject) => {
15525
15525
  const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
15526
- 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) => {
15526
+ const outgoing = request2({ socketPath: identity2?.pipe ?? daemonSocketPath(), method, path, headers: { ...payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : {}, ...identity2 ? { Authorization: "Bearer " + identity2.token } : {} } }, (incoming) => {
15527
15527
  const chunks = [];
15528
15528
  incoming.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
15529
15529
  incoming.on("end", () => {
@@ -15604,16 +15604,16 @@ async function captureClicks(page, session, onClick) {
15604
15604
  let identifier;
15605
15605
  session.on("Runtime.bindingCalled", receive);
15606
15606
  const cleanup = async () => {
15607
- session.off("Runtime.bindingCalled", receive);
15608
15607
  if (identifier) await session.send("Page.removeScriptToEvaluateOnNewDocument", { identifier }).catch(() => void 0);
15609
15608
  await Promise.all(page.frames().map((frame) => frame.evaluate((key2) => {
15610
- const target = window;
15611
- const listener = target[key2 + "_listener"];
15609
+ const target2 = window;
15610
+ const listener = target2[key2 + "_listener"];
15612
15611
  if (typeof listener === "function") document.removeEventListener("click", listener, { capture: true });
15613
- delete target[key2 + "_listener"];
15614
- delete target[key2];
15612
+ delete target2[key2 + "_listener"];
15613
+ delete target2[key2];
15615
15614
  }, binding).catch(() => void 0)));
15616
15615
  await session.send("Runtime.removeBinding", { name: binding }).catch(() => void 0);
15616
+ session.off("Runtime.bindingCalled", receive);
15617
15617
  };
15618
15618
  try {
15619
15619
  await session.send("Runtime.enable");
@@ -15800,7 +15800,8 @@ var interactionReportFields = {
15800
15800
  cleanup: external_exports.literal("not_attempted").optional()
15801
15801
  };
15802
15802
  var cloudRunPrompt = external_exports.string().min(1).max(2e3).refine((value) => Boolean(value.trim()), "Describe what you want Rill to do.");
15803
- var cloudSubmission = external_exports.object({ ...interactionFields, id: external_exports.string().uuid(), url: cloudRunTarget, prompt: cloudRunPrompt, expectation: external_exports.string().trim().min(1).max(1e3).optional(), account: external_exports.string().regex(/^[a-z0-9-]{1,60}$/).optional() }).strict();
15803
+ var practiceReference = external_exports.object({ missionId: external_exports.string().regex(/^[a-z]+-[a-z]+$/).max(60), version: external_exports.literal(1) }).strict();
15804
+ var cloudSubmission = external_exports.object({ ...interactionFields, id: external_exports.string().uuid(), url: cloudRunTarget, prompt: cloudRunPrompt, expectation: external_exports.string().trim().min(1).max(1e3).optional(), account: external_exports.string().regex(/^[a-z0-9-]{1,60}$/).optional(), practice: practiceReference.optional() }).strict();
15804
15805
  var siteAccountSchema = external_exports.object({
15805
15806
  name: external_exports.string().regex(/^[a-z0-9-]{1,60}$/),
15806
15807
  origin: publicTarget.refine((v) => new URL(v).protocol === "https:" && new URL(v).origin === v, "Use an exact HTTPS origin."),
@@ -15896,6 +15897,7 @@ var scrollCapsuleSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="20.5" hei
15896
15897
  </svg>`;
15897
15898
  var scrollCapsuleStyles = `
15898
15899
  .scroll-indicator { position:absolute; right:12.75px; bottom:10.75px; width:20.5px; height:42.5px; opacity:0; transform:translateY(4px) scale(.92); transform-origin:center; transition:opacity 180ms ease-out,transform 180ms ease-out; filter:drop-shadow(0 1px 1.5px rgba(12,12,18,.18)) drop-shadow(0 4px 5px rgba(12,12,18,.12)); --rill-cursor-accent-light:color-mix(in srgb,var(--rill-cursor-accent) 78%,#fff); }
15900
+ .scroll-label { left:auto; right:calc(100% + 7px); top:50%; transform:translateY(-50%); }
15899
15901
  .scroll-indicator.is-visible { opacity:1; transform:none; transition:opacity 160ms ease-out,transform 260ms cubic-bezier(.2,.9,.3,1.25); }
15900
15902
  .scroll-indicator svg { position:absolute; left:50%; top:50%; width:20.5px; height:42.5px; translate:-50% -50%; transform-origin:center; }
15901
15903
  .scroll-indicator[data-direction="right"],.scroll-indicator[data-direction="left"] { width:42.5px; height:20.5px; }
@@ -15946,7 +15948,7 @@ function installCursorOverlay(options) {
15946
15948
  pointer.innerHTML = options.pointerMarkup;
15947
15949
  const scrollIndicator = document.createElement("div");
15948
15950
  scrollIndicator.className = "scroll-indicator";
15949
- scrollIndicator.innerHTML = options.scrollMarkup;
15951
+ scrollIndicator.innerHTML = options.scrollMarkup + '<span class="rill-cursor-label scroll-label">Scrolling</span>';
15950
15952
  const chevrons = scrollIndicator.querySelector("#chevrons");
15951
15953
  root.append(style, pointer, scrollIndicator);
15952
15954
  document.documentElement.appendChild(host);
@@ -15993,11 +15995,11 @@ function installCursorOverlay(options) {
15993
15995
  positions.set(document, { x: window.scrollX, y: window.scrollY });
15994
15996
  document.addEventListener("scroll", (event) => {
15995
15997
  if (!event.isTrusted || document.hidden) return;
15996
- const target = event.target;
15997
- if (target !== document && !(target instanceof Element)) return;
15998
- const position = target === document ? { x: window.scrollX, y: window.scrollY } : { x: target.scrollLeft, y: target.scrollTop };
15999
- const previous = positions.get(target);
16000
- positions.set(target, position);
15998
+ const target2 = event.target;
15999
+ if (target2 !== document && !(target2 instanceof Element)) return;
16000
+ const position = target2 === document ? { x: window.scrollX, y: window.scrollY } : { x: target2.scrollLeft, y: target2.scrollTop };
16001
+ const previous = positions.get(target2);
16002
+ positions.set(target2, position);
16001
16003
  const dx = previous ? position.x - previous.x : 0;
16002
16004
  const dy = previous ? position.y - previous.y : 0;
16003
16005
  if (previous && !dx && !dy) return;
@@ -16028,13 +16030,13 @@ function installCursorOverlay(options) {
16028
16030
  document.addEventListener("pointerover", move, listenerOptions);
16029
16031
  document.addEventListener("pointerup", release, listenerOptions);
16030
16032
  document.addEventListener("pointercancel", release, listenerOptions);
16031
- const animateFeedback = (element, frames, duration3, easing) => {
16032
- root.insertBefore(element, pointer);
16033
- const animation = element.animate(frames, { duration: duration3, easing });
16033
+ const animateFeedback = (element2, frames, duration3, easing) => {
16034
+ root.insertBefore(element2, pointer);
16035
+ const animation = element2.animate(frames, { duration: duration3, easing });
16034
16036
  animations.add(animation);
16035
16037
  void animation.finished.catch(() => void 0).finally(() => {
16036
16038
  animations.delete(animation);
16037
- element.remove();
16039
+ element2.remove();
16038
16040
  });
16039
16041
  };
16040
16042
  document.addEventListener("pointerdown", (event) => {
@@ -16125,8 +16127,10 @@ async function writeBundle(bundle, outputPath) {
16125
16127
  const output = createWriteStream(outputPath, { mode: 384 });
16126
16128
  const gzip = createGzip({ level: 9 });
16127
16129
  gzip.pipe(output);
16128
- const { console: consoleEvents, network, timeline, ...manifest } = bundle;
16130
+ const { console: consoleEvents, network, timeline, actions, ...manifest } = bundle;
16129
16131
  gzip.write(`${JSON.stringify({ kind: "manifest", ...manifest })}
16132
+ `);
16133
+ for (const event of actions ?? []) gzip.write(`${JSON.stringify(event)}
16130
16134
  `);
16131
16135
  for (const event of consoleEvents) gzip.write(`${JSON.stringify({ kind: "console", ...event })}
16132
16136
  `);
@@ -16138,6 +16142,203 @@ async function writeBundle(bundle, outputPath) {
16138
16142
  await once(output, "close");
16139
16143
  }
16140
16144
 
16145
+ // ../contracts/src/browser.ts
16146
+ var browserContractVersion = 1;
16147
+ var identity = external_exports.string().min(1).max(100);
16148
+ var element = external_exports.number().int().min(1).max(100);
16149
+ var version2 = { schemaVersion: external_exports.literal(browserContractVersion) };
16150
+ var target = { observationId: identity, element };
16151
+ var browserActionSchema = external_exports.enum(["observe", "click", "fill", "check", "select", "press", "navigate", "scroll", "finish"]);
16152
+ var browserCommandSchema = external_exports.discriminatedUnion("action", [
16153
+ external_exports.object({ ...version2, action: external_exports.literal("observe") }).strict(),
16154
+ external_exports.object({ ...version2, action: external_exports.literal("click"), ...target }).strict(),
16155
+ external_exports.object({ ...version2, action: external_exports.literal("fill"), ...target, value: external_exports.string().max(2e3) }).strict(),
16156
+ external_exports.object({ ...version2, action: external_exports.literal("check"), ...target, checked: external_exports.boolean() }).strict(),
16157
+ external_exports.object({ ...version2, action: external_exports.literal("select"), ...target, value: external_exports.string().max(200) }).strict(),
16158
+ external_exports.object({ ...version2, action: external_exports.literal("press"), ...target, key: external_exports.enum(["Enter", "Tab", "ArrowDown", "ArrowUp", "Escape"]) }).strict(),
16159
+ external_exports.object({ ...version2, action: external_exports.literal("navigate"), url: external_exports.string().url().max(2048) }).strict(),
16160
+ external_exports.object({ ...version2, action: external_exports.literal("scroll"), observationId: identity, direction: external_exports.enum(["up", "down"]) }).strict(),
16161
+ external_exports.object({ ...version2, action: external_exports.literal("finish"), outcome: external_exports.enum(["walkthrough", "passed", "failed", "inconclusive"]), summary: external_exports.string().min(1).max(1e3), evidence: external_exports.string().max(1e3) }).strict()
16162
+ ]);
16163
+ var context = { recordingId: identity, pageId: identity, frameId: external_exports.literal("main"), documentId: identity };
16164
+ var browserObservationSchema = external_exports.object({
16165
+ ...version2,
16166
+ kind: external_exports.literal("observation"),
16167
+ ...context,
16168
+ observationId: identity,
16169
+ offsetMs: external_exports.number().nonnegative(),
16170
+ url: external_exports.string(),
16171
+ title: external_exports.string().max(500),
16172
+ text: external_exports.string().max(12e3),
16173
+ viewport: external_exports.object({ width: external_exports.number().nonnegative(), height: external_exports.number().nonnegative(), scrollX: external_exports.number(), scrollY: external_exports.number(), documentHeight: external_exports.number().nonnegative(), text: external_exports.string().max(12e3), textTruncated: external_exports.boolean(), evidence: external_exports.literal("dom_geometry") }).strict(),
16174
+ elements: external_exports.array(external_exports.object({
16175
+ kind: external_exports.literal("dom"),
16176
+ id: element,
16177
+ tag: external_exports.string(),
16178
+ label: external_exports.string().max(160),
16179
+ type: external_exports.string().nullable(),
16180
+ role: external_exports.string().nullable(),
16181
+ disabled: external_exports.boolean(),
16182
+ checked: external_exports.union([external_exports.boolean(), external_exports.string(), external_exports.null()]),
16183
+ options: external_exports.array(external_exports.object({ value: external_exports.string().max(200), label: external_exports.string().max(100), selected: external_exports.boolean() }).strict()).max(50).optional(),
16184
+ optionsTruncated: external_exports.boolean().optional(),
16185
+ validation: external_exports.string().max(160).optional()
16186
+ }).strict()).max(100),
16187
+ completeness: external_exports.object({
16188
+ scope: external_exports.literal("main_document"),
16189
+ childFrames: external_exports.literal("excluded"),
16190
+ shadowRoots: external_exports.literal("excluded"),
16191
+ targetsTruncated: external_exports.boolean(),
16192
+ textTruncated: external_exports.boolean(),
16193
+ targetLimit: external_exports.literal(100),
16194
+ textLimit: external_exports.literal(12e3)
16195
+ }).strict(),
16196
+ blockedRequests: external_exports.number().int().nonnegative(),
16197
+ submissionOutcomeUnknown: external_exports.boolean()
16198
+ }).strict();
16199
+ var browserErrorSchema = external_exports.enum([
16200
+ "invalid_browser_command",
16201
+ "browser_busy",
16202
+ "run_finished",
16203
+ "stale_observation",
16204
+ "stale_target",
16205
+ "observation_unavailable",
16206
+ "interaction_required",
16207
+ "consequential_action_blocked",
16208
+ "unsupported_control",
16209
+ "target_not_allowed",
16210
+ "submission_outcome_unknown",
16211
+ "action_outcome_unknown",
16212
+ "browser_action_unavailable",
16213
+ "verification_requires_expectation_and_evidence"
16214
+ ]);
16215
+ var browserActionReceiptSchema = external_exports.object({
16216
+ ...version2,
16217
+ kind: external_exports.literal("action"),
16218
+ ...context,
16219
+ actionId: identity,
16220
+ action: browserActionSchema.nullable(),
16221
+ inputObservationId: identity.nullable(),
16222
+ target: external_exports.object({ kind: external_exports.literal("dom"), element }).strict().optional(),
16223
+ startedAtMs: external_exports.number().nonnegative(),
16224
+ finishedAtMs: external_exports.number().nonnegative(),
16225
+ status: external_exports.enum(["completed", "rejected", "failed"]),
16226
+ effectState: external_exports.enum(["none", "dispatched", "unknown"]),
16227
+ next: external_exports.enum(["observe", "inspect_only", "finished"]),
16228
+ errorCode: browserErrorSchema.optional(),
16229
+ blockedRequests: external_exports.number().int().nonnegative()
16230
+ }).strict().refine((value) => value.finishedAtMs >= value.startedAtMs, "Receipt timestamps must be ordered").refine((value) => value.status !== "rejected" || value.effectState === "none", "Rejected actions cannot have effects").refine((value) => value.status === "completed" === (value.errorCode === void 0), "Errors require a non-completed status").refine((value) => value.effectState !== "unknown" || value.next === "inspect_only", "Unknown effects require inspection");
16231
+
16232
+ // ../contracts/src/diagnostics.ts
16233
+ var diagnosticSeveritySchema = external_exports.enum(["debug", "info", "warning", "error"]);
16234
+ var consoleEventSchema = external_exports.object({
16235
+ id: external_exports.string(),
16236
+ offsetMs: external_exports.number().nonnegative(),
16237
+ severity: diagnosticSeveritySchema,
16238
+ text: external_exports.string(),
16239
+ sourceUrl: external_exports.string().optional(),
16240
+ lineNumber: external_exports.number().optional(),
16241
+ stack: external_exports.array(external_exports.string()).default([])
16242
+ });
16243
+ var networkEventSchema = external_exports.object({
16244
+ id: external_exports.string(),
16245
+ offsetMs: external_exports.number().nonnegative(),
16246
+ method: external_exports.string(),
16247
+ url: external_exports.string(),
16248
+ resourceType: external_exports.string(),
16249
+ status: external_exports.number().int().optional(),
16250
+ mimeType: external_exports.string().optional(),
16251
+ durationMs: external_exports.number().nonnegative().optional(),
16252
+ transferSize: external_exports.number().nonnegative().optional(),
16253
+ failureReason: external_exports.string().optional(),
16254
+ completedOffsetMs: external_exports.number().nonnegative().optional(),
16255
+ pageUrl: external_exports.string().optional(),
16256
+ blockedBy: external_exports.enum(["rill"]).optional(),
16257
+ blockReason: external_exports.enum(["write_policy", "navigation_policy", "interception_error"]).optional(),
16258
+ requestBody: external_exports.string().optional(),
16259
+ responseBody: external_exports.string().optional(),
16260
+ bodyOmittedReason: external_exports.string().optional()
16261
+ });
16262
+ var timelineEventSchema = external_exports.object({
16263
+ id: external_exports.string(),
16264
+ offsetMs: external_exports.number().nonnegative(),
16265
+ type: external_exports.enum(["navigation", "click", "console", "network", "exception", "lifecycle"]),
16266
+ label: external_exports.string(),
16267
+ severity: diagnosticSeveritySchema.optional()
16268
+ });
16269
+ var browserActionEventSchema = external_exports.object({
16270
+ schemaVersion: external_exports.literal(1),
16271
+ kind: external_exports.literal("browser_action"),
16272
+ id: external_exports.string().regex(/^action_[1-9][0-9]{0,3}$/),
16273
+ offsetMs: external_exports.number().int().min(0).max(864e5),
16274
+ finishedAtMs: external_exports.number().int().min(0).max(864e5),
16275
+ action: browserActionSchema.nullable(),
16276
+ status: external_exports.enum(["completed", "rejected", "failed"]),
16277
+ effectState: external_exports.enum(["none", "dispatched", "unknown"]),
16278
+ next: external_exports.enum(["observe", "inspect_only", "finished"]),
16279
+ errorCode: browserErrorSchema.optional(),
16280
+ blockedRequests: external_exports.number().int().nonnegative()
16281
+ }).strict().refine((event) => event.finishedAtMs >= event.offsetMs, "Action timestamps must be ordered").refine((event) => event.status !== "rejected" || event.effectState === "none", "Rejected actions cannot have effects").refine((event) => event.status === "completed" === (event.errorCode === void 0), "Errors require a non-completed status").refine((event) => event.effectState !== "unknown" || event.next === "inspect_only", "Unknown effects require inspection");
16282
+ var diagnosticBundleSchema = external_exports.object({
16283
+ schemaVersion: external_exports.literal(1),
16284
+ capturePolicyVersion: external_exports.string(),
16285
+ videoStartOffsetMs: external_exports.number().optional(),
16286
+ recordingId: external_exports.string(),
16287
+ startedAt: external_exports.string().datetime(),
16288
+ durationMs: external_exports.number().nonnegative(),
16289
+ bodyCaptureEnabled: external_exports.boolean(),
16290
+ bodyCaptureRedacted: external_exports.boolean(),
16291
+ truncated: external_exports.boolean(),
16292
+ actions: external_exports.array(browserActionEventSchema).max(1e3).optional(),
16293
+ actionsTruncated: external_exports.boolean().optional(),
16294
+ console: external_exports.array(consoleEventSchema),
16295
+ network: external_exports.array(networkEventSchema),
16296
+ timeline: external_exports.array(timelineEventSchema)
16297
+ });
16298
+
16299
+ // ../recorder/src/browser-actions.ts
16300
+ var BrowserActionCapture = class {
16301
+ constructor(recordingId) {
16302
+ this.recordingId = recordingId;
16303
+ }
16304
+ recordingId;
16305
+ events = [];
16306
+ truncated = false;
16307
+ seen = /* @__PURE__ */ new Set();
16308
+ add(input) {
16309
+ const parsed = browserActionReceiptSchema.safeParse(input);
16310
+ if (!parsed.success || parsed.data.recordingId !== this.recordingId) {
16311
+ this.truncated = true;
16312
+ return;
16313
+ }
16314
+ const receipt = parsed.data;
16315
+ if (this.seen.has(receipt.actionId)) return;
16316
+ if (this.events.length >= 1e3) {
16317
+ this.truncated = true;
16318
+ return;
16319
+ }
16320
+ const event = browserActionEventSchema.safeParse({
16321
+ schemaVersion: 1,
16322
+ kind: "browser_action",
16323
+ id: `action_${this.events.length + 1}`,
16324
+ offsetMs: Math.round(receipt.startedAtMs),
16325
+ finishedAtMs: Math.round(receipt.finishedAtMs),
16326
+ action: receipt.action,
16327
+ status: receipt.status,
16328
+ effectState: receipt.effectState,
16329
+ next: receipt.next,
16330
+ ...receipt.errorCode ? { errorCode: receipt.errorCode } : {},
16331
+ blockedRequests: receipt.blockedRequests
16332
+ });
16333
+ if (!event.success) {
16334
+ this.truncated = true;
16335
+ return;
16336
+ }
16337
+ this.seen.add(receipt.actionId);
16338
+ this.events.push(event.data);
16339
+ }
16340
+ };
16341
+
16141
16342
  // ../recorder/src/environment.ts
16142
16343
  import { existsSync } from "fs";
16143
16344
  import { win32 as win323 } from "path";
@@ -16249,12 +16450,18 @@ var ScreencastRecorder = class {
16249
16450
  frames = [];
16250
16451
  pendingWrites = /* @__PURE__ */ new Set();
16251
16452
  started = false;
16453
+ firstFrameEpochMs;
16454
+ get videoStartedAtMs() {
16455
+ return this.firstFrameEpochMs;
16456
+ }
16252
16457
  frameCounter = 0;
16253
16458
  frameBytes = 0;
16254
16459
  captureError;
16255
16460
  async start() {
16256
16461
  await mkdir5(this.directory, { recursive: true, mode: 448 });
16257
16462
  this.session.on("Page.screencastFrame", (payload) => {
16463
+ const frameEpochMs = (payload.metadata.timestamp ?? Date.now() / 1e3) * 1e3;
16464
+ this.firstFrameEpochMs = Math.min(this.firstFrameEpochMs ?? frameEpochMs, frameEpochMs);
16258
16465
  const bytes = Buffer.from(payload.data, "base64");
16259
16466
  if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
16260
16467
  this.captureError = new Error("Capture exceeds the remote encoding frame or byte limit.");
@@ -16265,7 +16472,7 @@ var ScreencastRecorder = class {
16265
16472
  const path = join8(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
16266
16473
  this.frameCounter += 1;
16267
16474
  const write = writeFile4(path, bytes, { mode: 384 }).then(() => {
16268
- this.frames.push({ path, timestamp: payload.metadata.timestamp ?? Date.now() / 1e3 });
16475
+ this.frames.push({ path, timestamp: frameEpochMs / 1e3 });
16269
16476
  }).catch((error51) => {
16270
16477
  this.captureError = error51 instanceof Error ? error51 : new Error(String(error51));
16271
16478
  }).finally(() => {
@@ -16319,6 +16526,17 @@ var RecordingSession = class _RecordingSession {
16319
16526
  consoleEvents = [];
16320
16527
  networkEvents = [];
16321
16528
  timelineEvents = [];
16529
+ browserActions;
16530
+ captureBrowserAction(receipt) {
16531
+ if (!this.diagnosticsPaused && !this.stopPromise) this.browserActions.add(receipt);
16532
+ }
16533
+ captureRequestBlock(request3, reason) {
16534
+ const event = this.requestEvents.get(request3);
16535
+ if (event) {
16536
+ event.blockedBy = "rill";
16537
+ event.blockReason = reason;
16538
+ }
16539
+ }
16322
16540
  requestStart = /* @__PURE__ */ new Map();
16323
16541
  requestEvents = /* @__PURE__ */ new Map();
16324
16542
  stopPromise;
@@ -16340,6 +16558,7 @@ var RecordingSession = class _RecordingSession {
16340
16558
  this.videoFormat = options.videoFormat ?? "webm";
16341
16559
  this.captureSignal = options.signal;
16342
16560
  this.recordingId = options.recordingId;
16561
+ this.browserActions = new BrowserActionCapture(options.recordingId);
16343
16562
  this.cdpUrl = runtime.cdpUrl;
16344
16563
  this.tabId = runtime.tabId;
16345
16564
  this.browser = runtime.browser;
@@ -16409,6 +16628,10 @@ var RecordingSession = class _RecordingSession {
16409
16628
  }
16410
16629
  async finalize(reason) {
16411
16630
  if (this.maxTimer) clearTimeout(this.maxTimer);
16631
+ await this.removeClickCapture?.().catch(() => {
16632
+ this.truncated = true;
16633
+ });
16634
+ this.removeClickCapture = void 0;
16412
16635
  const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
16413
16636
  const outputDirectory = this.outputDirectory;
16414
16637
  const videoPath = join9(outputDirectory, `recording.${this.videoFormat}`);
@@ -16416,22 +16639,25 @@ var RecordingSession = class _RecordingSession {
16416
16639
  const bundle = {
16417
16640
  schemaVersion: 1,
16418
16641
  capturePolicyVersion: "safe-default-v1",
16642
+ ...this.screencast.videoStartedAtMs == null ? {} : { videoStartOffsetMs: this.screencast.videoStartedAtMs - this.startedAt.getTime() },
16419
16643
  recordingId: this.recordingId,
16420
16644
  startedAt: this.startedAt.toISOString(),
16421
16645
  durationMs: Math.round(durationSeconds * 1e3),
16422
16646
  bodyCaptureEnabled: this.captureNetworkBodies,
16423
16647
  bodyCaptureRedacted: this.captureNetworkBodies,
16424
- truncated: this.truncated,
16648
+ truncated: this.truncated || this.browserActions.truncated,
16649
+ actionsTruncated: this.browserActions.truncated,
16425
16650
  console: this.consoleEvents,
16426
16651
  network: this.networkEvents,
16427
16652
  timeline: this.timelineEvents.toSorted((left, right) => left.offsetMs - right.offsetMs)
16428
16653
  };
16429
16654
  try {
16430
16655
  const safeBundle = JSON.parse(this.secretValues.reduce((text, secret) => text.split(JSON.stringify(secret).slice(1, -1)).join("[redacted]"), JSON.stringify(bundle)));
16656
+ safeBundle.actions = this.browserActions.events;
16657
+ safeBundle.actionsTruncated = this.browserActions.truncated;
16431
16658
  await writeBundle(safeBundle, diagnosticsPath);
16432
16659
  await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote", this.captureSignal);
16433
16660
  } finally {
16434
- await this.removeClickCapture?.().catch(() => void 0);
16435
16661
  await this.removeCursorOverlay?.().catch(() => void 0);
16436
16662
  await this.browser.close().catch(() => void 0);
16437
16663
  if (this.browserProcess) {
@@ -16442,7 +16668,7 @@ var RecordingSession = class _RecordingSession {
16442
16668
  const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
16443
16669
  const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
16444
16670
  const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
16445
- 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 } };
16671
+ return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory: join9(outputDirectory, "frames") } : {}, diagnosticsPath, durationSeconds, stoppedReason: reason, diagnostics: { consoleErrors, failedRequests, httpErrors, truncated: this.truncated || this.browserActions.truncated, bodyCaptureEnabled: this.captureNetworkBodies } };
16446
16672
  }
16447
16673
  captureNetworkBodies = false;
16448
16674
  captureSignal;
@@ -16497,7 +16723,7 @@ var RecordingSession = class _RecordingSession {
16497
16723
  }
16498
16724
  const started = offset();
16499
16725
  this.requestStart.set(request3, started);
16500
- const event = { id: `net_${this.networkEvents.length + 1}`, offsetMs: started, method: request3.method(), url: sanitizeUrl(request3.url()), resourceType: request3.resourceType() };
16726
+ const event = { id: `net_${this.networkEvents.length + 1}`, offsetMs: started, method: request3.method(), url: sanitizeUrl(request3.url()), resourceType: request3.resourceType(), pageUrl: sanitizeUrl(this.page.url()) };
16501
16727
  if (captureNetworkBodies && request3.postData()) {
16502
16728
  const redacted = redactBody(request3.postData(), request3.headers()["content-type"]);
16503
16729
  event.requestBody = redacted.body;
@@ -16512,6 +16738,7 @@ var RecordingSession = class _RecordingSession {
16512
16738
  const event = this.requestEvents.get(request3);
16513
16739
  if (!event) return;
16514
16740
  event.failureReason = request3.failure()?.errorText ?? "request_failed";
16741
+ event.completedOffsetMs = offset();
16515
16742
  event.durationMs = Math.max(0, offset() - (this.requestStart.get(request3) ?? offset()));
16516
16743
  pushTimeline({ offsetMs: event.offsetMs, type: "network", label: `${event.method} ${event.url} \u2192 ${event.failureReason}`, severity: "error" });
16517
16744
  });
@@ -16524,6 +16751,7 @@ var RecordingSession = class _RecordingSession {
16524
16751
  const event = this.requestEvents.get(response.request());
16525
16752
  if (!event) return;
16526
16753
  event.status = response.status();
16754
+ event.completedOffsetMs = Math.max(0, Date.now() - this.startedAt.getTime());
16527
16755
  event.mimeType = response.headers()["content-type"];
16528
16756
  event.durationMs = Math.max(0, Date.now() - this.startedAt.getTime() - event.offsetMs);
16529
16757
  event.transferSize = Number(response.headers()["content-length"] ?? 0) || void 0;
@@ -16572,10 +16800,10 @@ async function launchManaged(options) {
16572
16800
  try {
16573
16801
  await waitForCdp(cdpUrl, browserProcess);
16574
16802
  const browser = await chromium2.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
16575
- const context = options.blockServiceWorkers ? await browser.newContext({ serviceWorkers: "block" }) : browser.contexts()[0];
16576
- const page = context.pages()[0] ?? await context.newPage();
16803
+ const context2 = options.blockServiceWorkers ? await browser.newContext({ serviceWorkers: "block" }) : browser.contexts()[0];
16804
+ const page = context2.pages()[0] ?? await context2.newPage();
16577
16805
  await page.setViewportSize({ width: options.width, height: options.height });
16578
- return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
16806
+ return { cdpUrl, browser, context: context2, page, browserProcess, profileDirectory };
16579
16807
  } catch (error51) {
16580
16808
  await terminateBrowser(browserProcess);
16581
16809
  await rm5(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
@@ -16622,17 +16850,17 @@ var sessions = /* @__PURE__ */ new Map();
16622
16850
  var lastActivity = Date.now();
16623
16851
  async function runDaemon() {
16624
16852
  if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC.");
16625
- const identity = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
16626
- const socketPath = identity?.pipe ?? daemonSocketPath();
16627
- await mkdir7(identity ? dirname(daemonStatePath()) : dirname(socketPath), { recursive: true, mode: 448 });
16853
+ const identity2 = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
16854
+ const socketPath = identity2?.pipe ?? daemonSocketPath();
16855
+ await mkdir7(identity2 ? dirname(daemonStatePath()) : dirname(socketPath), { recursive: true, mode: 448 });
16628
16856
  await mkdir7(recordingTempRoot(), { recursive: true, mode: 448 });
16629
16857
  await restoreRecoverableSessions();
16630
16858
  await purgeExpiredCaptures();
16631
- if (!identity) await unlink(socketPath).catch((error51) => {
16859
+ if (!identity2) await unlink(socketPath).catch((error51) => {
16632
16860
  if (error51.code !== "ENOENT") throw error51;
16633
16861
  });
16634
16862
  const server = createServer2((request3, response) => {
16635
- if (identity && !acceptsWindowsIpcToken(identity.token, request3.headers.authorization)) {
16863
+ if (identity2 && !acceptsWindowsIpcToken(identity2.token, request3.headers.authorization)) {
16636
16864
  request3.resume();
16637
16865
  json2(response, 401, { error: { code: "authentication_required", message: "Recorder authentication required." } });
16638
16866
  return;
@@ -16642,7 +16870,7 @@ async function runDaemon() {
16642
16870
  await new Promise((resolve, reject) => {
16643
16871
  server.once("error", reject);
16644
16872
  server.listen(socketPath, () => {
16645
- if (identity) resolve();
16873
+ if (identity2) resolve();
16646
16874
  else void chmod(socketPath, 384).then(resolve, reject);
16647
16875
  });
16648
16876
  });
@@ -16658,7 +16886,7 @@ async function runDaemon() {
16658
16886
  const close = () => {
16659
16887
  clearInterval(idleTimer);
16660
16888
  server.close();
16661
- if (!identity) void unlink(socketPath).catch(() => void 0);
16889
+ if (!identity2) void unlink(socketPath).catch(() => void 0);
16662
16890
  };
16663
16891
  process.once("SIGTERM", close);
16664
16892
  process.once("SIGINT", close);
@@ -16802,7 +17030,7 @@ function emit(value, pretty = false) {
16802
17030
  function failureValue(error51) {
16803
17031
  const message = error51 instanceof Error ? error51.message : String(error51);
16804
17032
  const [candidate] = message.split(":", 1);
16805
- 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"]);
17033
+ const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "authorization_not_found", "expired_token", "access_denied", "unsupported_platform", "unsupported_node_version", "scope_denied", "quota_exceeded", "browser_unavailable", "local_capture_failed", "recording_not_found", "upload_interrupted", "processing_pending", "processing_failed", "share_revoked", "validation_failed"]);
16806
17034
  const structured = error51 && typeof error51 === "object" ? error51 : {};
16807
17035
  const code = structured.code && knownCodes.has(structured.code) ? structured.code : knownCodes.has(candidate) ? candidate : "internal_error";
16808
17036
  return { error: { code, message, ...structured.recovery ? { recovery: structured.recovery } : {}, ...structured.details ? { details: structured.details } : {} } };
@@ -16816,17 +17044,17 @@ function fail(error51) {
16816
17044
  function terminalColor(enabled = Boolean(process.stdout.isTTY) && !("NO_COLOR" in process.env) && process.env.TERM !== "dumb") {
16817
17045
  return (text, code) => enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
16818
17046
  }
16819
- function formatIdentity(identity, color = terminalColor()) {
16820
- if (identity.status === "not_connected") return `${color("! Not connected to Rill", 33)}
17047
+ function formatIdentity(identity2, color = terminalColor()) {
17048
+ if (identity2.status === "not_connected") return `${color("! Not connected to Rill", 33)}
16821
17049
 
16822
- Sign in: ${cliCommand(identity.apiUrl, "login")}
17050
+ Sign in: ${cliCommand(identity2.apiUrl, "login")}
16823
17051
  `;
16824
- if (identity.status === "anonymous") return `${color("\u2713 Using Rill anonymously", 32)}
17052
+ if (identity2.status === "anonymous") return `${color("\u2713 Using Rill anonymously", 32)}
16825
17053
 
16826
- Connect your account: ${cliCommand(identity.apiUrl, "connect", "--email", "<your-email>")}
17054
+ Connect your account: ${cliCommand(identity2.apiUrl, "connect", "--email", "<your-email>")}
16827
17055
  `;
16828
17056
  return `${color("\u2713 Connected to Rill", 32)}
16829
- Account: ${identity.email ?? "Email unavailable"}
17057
+ Account: ${identity2.email ?? "Email unavailable"}
16830
17058
  `;
16831
17059
  }
16832
17060
  function formatDoctor(result, color = terminalColor()) {
@@ -16936,6 +17164,31 @@ async function withRunFeedback(session, start) {
16936
17164
 
16937
17165
  // src/doctor.ts
16938
17166
  import { spawn as spawn7 } from "child_process";
17167
+
17168
+ // src/node-runtime.ts
17169
+ var SUPPORTED_NODE_RANGE = "^22.23.4 || ^24.22.1 || >=26.12.1";
17170
+ var NODE_UPGRADE_RECOVERY = "Install Node.js 24.22.1 LTS or a newer patched release (22.23.4+, 24.22.1+, or 26.12.1+), then rerun the command. Update Chrome/Chromium separately for local recording.";
17171
+ function supportsNodeRuntime(version3) {
17172
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:\+[\w.-]+)?$/.exec(version3);
17173
+ if (!match) return false;
17174
+ const [major, minor, patch] = match.slice(1).map(Number);
17175
+ const minimum = major === 22 ? [23, 4] : major === 24 ? [22, 1] : major === 26 ? [12, 1] : null;
17176
+ if (!minimum) return major > 26;
17177
+ return minor > minimum[0] || minor === minimum[0] && patch >= minimum[1];
17178
+ }
17179
+ var UnsupportedNodeRuntimeError = class extends Error {
17180
+ code = "unsupported_node_version";
17181
+ recovery = NODE_UPGRADE_RECOVERY;
17182
+ constructor(version3) {
17183
+ super(`Node.js ${version3} does not meet Rill's patched runtime requirement for the eMudhra certificate compromise. Supported versions: ${SUPPORTED_NODE_RANGE}.`);
17184
+ this.name = "UnsupportedNodeRuntimeError";
17185
+ }
17186
+ };
17187
+ function assertNodeRuntime(version3 = process.version) {
17188
+ if (!supportsNodeRuntime(version3)) throw new UnsupportedNodeRuntimeError(version3);
17189
+ }
17190
+
17191
+ // src/doctor.ts
16939
17192
  var defaultDependencies = {
16940
17193
  platform: process.platform,
16941
17194
  encoding: "remote",
@@ -16954,10 +17207,12 @@ async function runDoctor(apiUrl, overrides = {}) {
16954
17207
  const checks = [];
16955
17208
  let feedbackEnabled = false;
16956
17209
  checks.push(recorderTransportCheck(dependencies.platform));
16957
- if (compareVersions(normalizeVersion(dependencies.nodeVersion), "22.0.0") >= 0) {
17210
+ if (supportsNodeRuntime(dependencies.nodeVersion)) {
16958
17211
  checks.push({ name: "node", status: "pass", message: `Node.js ${dependencies.nodeVersion} is supported.`, version: dependencies.nodeVersion });
16959
17212
  } else {
16960
- checks.push({ name: "node", status: "fail", message: `Node.js ${dependencies.nodeVersion} is unsupported.`, version: dependencies.nodeVersion, recovery: "Install Node.js 22 or newer, then reinstall the Rill CLI." });
17213
+ const error51 = new UnsupportedNodeRuntimeError(dependencies.nodeVersion);
17214
+ checks.push({ name: "node", status: "fail", message: error51.message, version: dependencies.nodeVersion, recovery: error51.recovery });
17215
+ return { schemaVersion: 1, status: "error", apiUrl, environment: { platform: dependencies.platform }, checks };
16961
17216
  }
16962
17217
  let health = null;
16963
17218
  try {
@@ -17024,8 +17279,8 @@ async function runDoctor(apiUrl, overrides = {}) {
17024
17279
  }
17025
17280
  async function executableCheck(name, path, args, run2, recovery) {
17026
17281
  try {
17027
- const version2 = firstLine(await run2(path, args));
17028
- return { name, status: "pass", message: `${name === "chrome" ? "Chrome/Chromium" : "ffmpeg"} is available.`, version: version2, path };
17282
+ const version3 = firstLine(await run2(path, args));
17283
+ return { name, status: "pass", message: `${name === "chrome" ? "Chrome/Chromium" : "ffmpeg"} is available.`, version: version3, path };
17029
17284
  } catch (error51) {
17030
17285
  return { name, status: "fail", message: `${name === "chrome" ? "Chrome/Chromium" : "ffmpeg"} is unavailable: ${errorMessage(error51)}`, path, recovery };
17031
17286
  }
@@ -17054,8 +17309,8 @@ ${Buffer.concat(stderr).toString("utf8")}`.trim();
17054
17309
  });
17055
17310
  });
17056
17311
  }
17057
- function normalizeVersion(version2) {
17058
- return version2.replace(/^v/, "").split("-")[0];
17312
+ function normalizeVersion(version3) {
17313
+ return version3.replace(/^v/, "").split("-")[0];
17059
17314
  }
17060
17315
  function compareVersions(left, right) {
17061
17316
  const leftParts = normalizeVersion(left).split(".").map(Number);
@@ -17205,9 +17460,9 @@ function watchEncoding(apiUrl, id) {
17205
17460
  var program = new Command();
17206
17461
  program.name("rill").description("Record, inspect, and share agent browser runs.").version(CLI_VERSION).option("--api-url <url>", "Rill control plane", controlPlaneUrl(process.env.RILL_API_URL)).option("--pretty", "Pretty-print JSON output", false).option("--json", "Output machine-readable JSON (including whoami)", false).option("--human", "Readable doctor, login, whoami and recording start/stop/status/retry output", false);
17207
17462
  program.configureOutput({ outputError: () => void 0 }).exitOverride();
17208
- function humanOutput(identity = false) {
17463
+ function humanOutput(identity2 = false) {
17209
17464
  const options = program.opts();
17210
- return !options.json && !options.pretty && (identity || options.human);
17465
+ return !options.json && !options.pretty && (identity2 || options.human);
17211
17466
  }
17212
17467
  function emitRecording(value, pretty) {
17213
17468
  if (humanOutput()) process.stdout.write(formatRecording(value, program.opts().apiUrl));
@@ -17250,10 +17505,10 @@ async function authorize(quiet = false) {
17250
17505
  }
17251
17506
  program.command("whoami").description("Show the current verified identity without starting sign-in.").addHelpText("after", "\nShows a short account summary by default. Use rill whoami --json for agents, or rill --pretty whoami for indented JSON.").action(async () => {
17252
17507
  const options = program.opts();
17253
- const identity = await whoami(options.apiUrl);
17254
- if (humanOutput(true)) process.stdout.write(formatIdentity(identity));
17255
- else emit(identity, options.pretty);
17256
- if (!identity.authenticated) process.exitCode = 1;
17508
+ const identity2 = await whoami(options.apiUrl);
17509
+ if (humanOutput(true)) process.stdout.write(formatIdentity(identity2));
17510
+ else emit(identity2, options.pretty);
17511
+ if (!identity2.authenticated) process.exitCode = 1;
17257
17512
  });
17258
17513
  var connect = program.command("connect").description("Connect this agent once, with renewable account access.").option("--email <email>", "Email of the Rill account approving access").option("--no-wait", "Return approval instructions immediately; poll with connect status").action(async (input) => {
17259
17514
  if (!input.email) throw new Error("validation_failed: Supply --email <email> to start a connection.");
@@ -17525,6 +17780,10 @@ drafts.command("purge").action(async () => {
17525
17780
  emit({ schemaVersion: 1, purged }, options.pretty);
17526
17781
  });
17527
17782
  program.hook("preAction", async (_command, actionCommand) => {
17783
+ if (!supportsNodeRuntime(process.version)) {
17784
+ if (commandPath(actionCommand).join(" ") === "doctor") return;
17785
+ assertNodeRuntime();
17786
+ }
17528
17787
  if (commandPath(actionCommand).join(" ") === "record start") assertRecorderTransport();
17529
17788
  const command = cliCommandCategory(commandPath(actionCommand));
17530
17789
  if (!command) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runalabs/rill-cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Record, inspect, and share agent-controlled browser runs with Rill.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -12,7 +12,7 @@
12
12
  "bugs": "https://github.com/RunaLabsDev/rill-web/issues",
13
13
  "type": "module",
14
14
  "engines": {
15
- "node": ">=22"
15
+ "node": "^22.23.4 || ^24.22.1 || >=26.12.1"
16
16
  },
17
17
  "bin": {
18
18
  "rill": "dist/cli.js"