@runalabs/rill-cli 0.1.19 → 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 +355 -97
  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."),
@@ -15994,11 +15995,11 @@ function installCursorOverlay(options) {
15994
15995
  positions.set(document, { x: window.scrollX, y: window.scrollY });
15995
15996
  document.addEventListener("scroll", (event) => {
15996
15997
  if (!event.isTrusted || document.hidden) return;
15997
- const target = event.target;
15998
- if (target !== document && !(target instanceof Element)) return;
15999
- const position = target === document ? { x: window.scrollX, y: window.scrollY } : { x: target.scrollLeft, y: target.scrollTop };
16000
- const previous = positions.get(target);
16001
- 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);
16002
16003
  const dx = previous ? position.x - previous.x : 0;
16003
16004
  const dy = previous ? position.y - previous.y : 0;
16004
16005
  if (previous && !dx && !dy) return;
@@ -16029,13 +16030,13 @@ function installCursorOverlay(options) {
16029
16030
  document.addEventListener("pointerover", move, listenerOptions);
16030
16031
  document.addEventListener("pointerup", release, listenerOptions);
16031
16032
  document.addEventListener("pointercancel", release, listenerOptions);
16032
- const animateFeedback = (element, frames, duration3, easing) => {
16033
- root.insertBefore(element, pointer);
16034
- 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 });
16035
16036
  animations.add(animation);
16036
16037
  void animation.finished.catch(() => void 0).finally(() => {
16037
16038
  animations.delete(animation);
16038
- element.remove();
16039
+ element2.remove();
16039
16040
  });
16040
16041
  };
16041
16042
  document.addEventListener("pointerdown", (event) => {
@@ -16126,8 +16127,10 @@ async function writeBundle(bundle, outputPath) {
16126
16127
  const output = createWriteStream(outputPath, { mode: 384 });
16127
16128
  const gzip = createGzip({ level: 9 });
16128
16129
  gzip.pipe(output);
16129
- const { console: consoleEvents, network, timeline, ...manifest } = bundle;
16130
+ const { console: consoleEvents, network, timeline, actions, ...manifest } = bundle;
16130
16131
  gzip.write(`${JSON.stringify({ kind: "manifest", ...manifest })}
16132
+ `);
16133
+ for (const event of actions ?? []) gzip.write(`${JSON.stringify(event)}
16131
16134
  `);
16132
16135
  for (const event of consoleEvents) gzip.write(`${JSON.stringify({ kind: "console", ...event })}
16133
16136
  `);
@@ -16139,6 +16142,203 @@ async function writeBundle(bundle, outputPath) {
16139
16142
  await once(output, "close");
16140
16143
  }
16141
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
+
16142
16342
  // ../recorder/src/environment.ts
16143
16343
  import { existsSync } from "fs";
16144
16344
  import { win32 as win323 } from "path";
@@ -16250,12 +16450,18 @@ var ScreencastRecorder = class {
16250
16450
  frames = [];
16251
16451
  pendingWrites = /* @__PURE__ */ new Set();
16252
16452
  started = false;
16453
+ firstFrameEpochMs;
16454
+ get videoStartedAtMs() {
16455
+ return this.firstFrameEpochMs;
16456
+ }
16253
16457
  frameCounter = 0;
16254
16458
  frameBytes = 0;
16255
16459
  captureError;
16256
16460
  async start() {
16257
16461
  await mkdir5(this.directory, { recursive: true, mode: 448 });
16258
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);
16259
16465
  const bytes = Buffer.from(payload.data, "base64");
16260
16466
  if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
16261
16467
  this.captureError = new Error("Capture exceeds the remote encoding frame or byte limit.");
@@ -16266,7 +16472,7 @@ var ScreencastRecorder = class {
16266
16472
  const path = join8(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
16267
16473
  this.frameCounter += 1;
16268
16474
  const write = writeFile4(path, bytes, { mode: 384 }).then(() => {
16269
- this.frames.push({ path, timestamp: payload.metadata.timestamp ?? Date.now() / 1e3 });
16475
+ this.frames.push({ path, timestamp: frameEpochMs / 1e3 });
16270
16476
  }).catch((error51) => {
16271
16477
  this.captureError = error51 instanceof Error ? error51 : new Error(String(error51));
16272
16478
  }).finally(() => {
@@ -16320,6 +16526,17 @@ var RecordingSession = class _RecordingSession {
16320
16526
  consoleEvents = [];
16321
16527
  networkEvents = [];
16322
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
+ }
16323
16540
  requestStart = /* @__PURE__ */ new Map();
16324
16541
  requestEvents = /* @__PURE__ */ new Map();
16325
16542
  stopPromise;
@@ -16341,6 +16558,7 @@ var RecordingSession = class _RecordingSession {
16341
16558
  this.videoFormat = options.videoFormat ?? "webm";
16342
16559
  this.captureSignal = options.signal;
16343
16560
  this.recordingId = options.recordingId;
16561
+ this.browserActions = new BrowserActionCapture(options.recordingId);
16344
16562
  this.cdpUrl = runtime.cdpUrl;
16345
16563
  this.tabId = runtime.tabId;
16346
16564
  this.browser = runtime.browser;
@@ -16410,6 +16628,10 @@ var RecordingSession = class _RecordingSession {
16410
16628
  }
16411
16629
  async finalize(reason) {
16412
16630
  if (this.maxTimer) clearTimeout(this.maxTimer);
16631
+ await this.removeClickCapture?.().catch(() => {
16632
+ this.truncated = true;
16633
+ });
16634
+ this.removeClickCapture = void 0;
16413
16635
  const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
16414
16636
  const outputDirectory = this.outputDirectory;
16415
16637
  const videoPath = join9(outputDirectory, `recording.${this.videoFormat}`);
@@ -16417,22 +16639,25 @@ var RecordingSession = class _RecordingSession {
16417
16639
  const bundle = {
16418
16640
  schemaVersion: 1,
16419
16641
  capturePolicyVersion: "safe-default-v1",
16642
+ ...this.screencast.videoStartedAtMs == null ? {} : { videoStartOffsetMs: this.screencast.videoStartedAtMs - this.startedAt.getTime() },
16420
16643
  recordingId: this.recordingId,
16421
16644
  startedAt: this.startedAt.toISOString(),
16422
16645
  durationMs: Math.round(durationSeconds * 1e3),
16423
16646
  bodyCaptureEnabled: this.captureNetworkBodies,
16424
16647
  bodyCaptureRedacted: this.captureNetworkBodies,
16425
- truncated: this.truncated,
16648
+ truncated: this.truncated || this.browserActions.truncated,
16649
+ actionsTruncated: this.browserActions.truncated,
16426
16650
  console: this.consoleEvents,
16427
16651
  network: this.networkEvents,
16428
16652
  timeline: this.timelineEvents.toSorted((left, right) => left.offsetMs - right.offsetMs)
16429
16653
  };
16430
16654
  try {
16431
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;
16432
16658
  await writeBundle(safeBundle, diagnosticsPath);
16433
16659
  await this.screencast.stop(videoPath, durationSeconds, this.encoding === "remote", this.captureSignal);
16434
16660
  } finally {
16435
- await this.removeClickCapture?.().catch(() => void 0);
16436
16661
  await this.removeCursorOverlay?.().catch(() => void 0);
16437
16662
  await this.browser.close().catch(() => void 0);
16438
16663
  if (this.browserProcess) {
@@ -16443,7 +16668,7 @@ var RecordingSession = class _RecordingSession {
16443
16668
  const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
16444
16669
  const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
16445
16670
  const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
16446
- 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 } };
16447
16672
  }
16448
16673
  captureNetworkBodies = false;
16449
16674
  captureSignal;
@@ -16498,7 +16723,7 @@ var RecordingSession = class _RecordingSession {
16498
16723
  }
16499
16724
  const started = offset();
16500
16725
  this.requestStart.set(request3, started);
16501
- 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()) };
16502
16727
  if (captureNetworkBodies && request3.postData()) {
16503
16728
  const redacted = redactBody(request3.postData(), request3.headers()["content-type"]);
16504
16729
  event.requestBody = redacted.body;
@@ -16513,6 +16738,7 @@ var RecordingSession = class _RecordingSession {
16513
16738
  const event = this.requestEvents.get(request3);
16514
16739
  if (!event) return;
16515
16740
  event.failureReason = request3.failure()?.errorText ?? "request_failed";
16741
+ event.completedOffsetMs = offset();
16516
16742
  event.durationMs = Math.max(0, offset() - (this.requestStart.get(request3) ?? offset()));
16517
16743
  pushTimeline({ offsetMs: event.offsetMs, type: "network", label: `${event.method} ${event.url} \u2192 ${event.failureReason}`, severity: "error" });
16518
16744
  });
@@ -16525,6 +16751,7 @@ var RecordingSession = class _RecordingSession {
16525
16751
  const event = this.requestEvents.get(response.request());
16526
16752
  if (!event) return;
16527
16753
  event.status = response.status();
16754
+ event.completedOffsetMs = Math.max(0, Date.now() - this.startedAt.getTime());
16528
16755
  event.mimeType = response.headers()["content-type"];
16529
16756
  event.durationMs = Math.max(0, Date.now() - this.startedAt.getTime() - event.offsetMs);
16530
16757
  event.transferSize = Number(response.headers()["content-length"] ?? 0) || void 0;
@@ -16573,10 +16800,10 @@ async function launchManaged(options) {
16573
16800
  try {
16574
16801
  await waitForCdp(cdpUrl, browserProcess);
16575
16802
  const browser = await chromium2.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
16576
- const context = options.blockServiceWorkers ? await browser.newContext({ serviceWorkers: "block" }) : browser.contexts()[0];
16577
- 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();
16578
16805
  await page.setViewportSize({ width: options.width, height: options.height });
16579
- return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
16806
+ return { cdpUrl, browser, context: context2, page, browserProcess, profileDirectory };
16580
16807
  } catch (error51) {
16581
16808
  await terminateBrowser(browserProcess);
16582
16809
  await rm5(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
@@ -16623,17 +16850,17 @@ var sessions = /* @__PURE__ */ new Map();
16623
16850
  var lastActivity = Date.now();
16624
16851
  async function runDaemon() {
16625
16852
  if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC.");
16626
- const identity = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
16627
- const socketPath = identity?.pipe ?? daemonSocketPath();
16628
- 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 });
16629
16856
  await mkdir7(recordingTempRoot(), { recursive: true, mode: 448 });
16630
16857
  await restoreRecoverableSessions();
16631
16858
  await purgeExpiredCaptures();
16632
- if (!identity) await unlink(socketPath).catch((error51) => {
16859
+ if (!identity2) await unlink(socketPath).catch((error51) => {
16633
16860
  if (error51.code !== "ENOENT") throw error51;
16634
16861
  });
16635
16862
  const server = createServer2((request3, response) => {
16636
- if (identity && !acceptsWindowsIpcToken(identity.token, request3.headers.authorization)) {
16863
+ if (identity2 && !acceptsWindowsIpcToken(identity2.token, request3.headers.authorization)) {
16637
16864
  request3.resume();
16638
16865
  json2(response, 401, { error: { code: "authentication_required", message: "Recorder authentication required." } });
16639
16866
  return;
@@ -16643,7 +16870,7 @@ async function runDaemon() {
16643
16870
  await new Promise((resolve, reject) => {
16644
16871
  server.once("error", reject);
16645
16872
  server.listen(socketPath, () => {
16646
- if (identity) resolve();
16873
+ if (identity2) resolve();
16647
16874
  else void chmod(socketPath, 384).then(resolve, reject);
16648
16875
  });
16649
16876
  });
@@ -16659,7 +16886,7 @@ async function runDaemon() {
16659
16886
  const close = () => {
16660
16887
  clearInterval(idleTimer);
16661
16888
  server.close();
16662
- if (!identity) void unlink(socketPath).catch(() => void 0);
16889
+ if (!identity2) void unlink(socketPath).catch(() => void 0);
16663
16890
  };
16664
16891
  process.once("SIGTERM", close);
16665
16892
  process.once("SIGINT", close);
@@ -16803,7 +17030,7 @@ function emit(value, pretty = false) {
16803
17030
  function failureValue(error51) {
16804
17031
  const message = error51 instanceof Error ? error51.message : String(error51);
16805
17032
  const [candidate] = message.split(":", 1);
16806
- 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"]);
16807
17034
  const structured = error51 && typeof error51 === "object" ? error51 : {};
16808
17035
  const code = structured.code && knownCodes.has(structured.code) ? structured.code : knownCodes.has(candidate) ? candidate : "internal_error";
16809
17036
  return { error: { code, message, ...structured.recovery ? { recovery: structured.recovery } : {}, ...structured.details ? { details: structured.details } : {} } };
@@ -16817,17 +17044,17 @@ function fail(error51) {
16817
17044
  function terminalColor(enabled = Boolean(process.stdout.isTTY) && !("NO_COLOR" in process.env) && process.env.TERM !== "dumb") {
16818
17045
  return (text, code) => enabled ? `\x1B[${code}m${text}\x1B[0m` : text;
16819
17046
  }
16820
- function formatIdentity(identity, color = terminalColor()) {
16821
- 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)}
16822
17049
 
16823
- Sign in: ${cliCommand(identity.apiUrl, "login")}
17050
+ Sign in: ${cliCommand(identity2.apiUrl, "login")}
16824
17051
  `;
16825
- if (identity.status === "anonymous") return `${color("\u2713 Using Rill anonymously", 32)}
17052
+ if (identity2.status === "anonymous") return `${color("\u2713 Using Rill anonymously", 32)}
16826
17053
 
16827
- Connect your account: ${cliCommand(identity.apiUrl, "connect", "--email", "<your-email>")}
17054
+ Connect your account: ${cliCommand(identity2.apiUrl, "connect", "--email", "<your-email>")}
16828
17055
  `;
16829
17056
  return `${color("\u2713 Connected to Rill", 32)}
16830
- Account: ${identity.email ?? "Email unavailable"}
17057
+ Account: ${identity2.email ?? "Email unavailable"}
16831
17058
  `;
16832
17059
  }
16833
17060
  function formatDoctor(result, color = terminalColor()) {
@@ -16937,6 +17164,31 @@ async function withRunFeedback(session, start) {
16937
17164
 
16938
17165
  // src/doctor.ts
16939
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
16940
17192
  var defaultDependencies = {
16941
17193
  platform: process.platform,
16942
17194
  encoding: "remote",
@@ -16955,10 +17207,12 @@ async function runDoctor(apiUrl, overrides = {}) {
16955
17207
  const checks = [];
16956
17208
  let feedbackEnabled = false;
16957
17209
  checks.push(recorderTransportCheck(dependencies.platform));
16958
- if (compareVersions(normalizeVersion(dependencies.nodeVersion), "22.0.0") >= 0) {
17210
+ if (supportsNodeRuntime(dependencies.nodeVersion)) {
16959
17211
  checks.push({ name: "node", status: "pass", message: `Node.js ${dependencies.nodeVersion} is supported.`, version: dependencies.nodeVersion });
16960
17212
  } else {
16961
- 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 };
16962
17216
  }
16963
17217
  let health = null;
16964
17218
  try {
@@ -17025,8 +17279,8 @@ async function runDoctor(apiUrl, overrides = {}) {
17025
17279
  }
17026
17280
  async function executableCheck(name, path, args, run2, recovery) {
17027
17281
  try {
17028
- const version2 = firstLine(await run2(path, args));
17029
- 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 };
17030
17284
  } catch (error51) {
17031
17285
  return { name, status: "fail", message: `${name === "chrome" ? "Chrome/Chromium" : "ffmpeg"} is unavailable: ${errorMessage(error51)}`, path, recovery };
17032
17286
  }
@@ -17055,8 +17309,8 @@ ${Buffer.concat(stderr).toString("utf8")}`.trim();
17055
17309
  });
17056
17310
  });
17057
17311
  }
17058
- function normalizeVersion(version2) {
17059
- return version2.replace(/^v/, "").split("-")[0];
17312
+ function normalizeVersion(version3) {
17313
+ return version3.replace(/^v/, "").split("-")[0];
17060
17314
  }
17061
17315
  function compareVersions(left, right) {
17062
17316
  const leftParts = normalizeVersion(left).split(".").map(Number);
@@ -17206,9 +17460,9 @@ function watchEncoding(apiUrl, id) {
17206
17460
  var program = new Command();
17207
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);
17208
17462
  program.configureOutput({ outputError: () => void 0 }).exitOverride();
17209
- function humanOutput(identity = false) {
17463
+ function humanOutput(identity2 = false) {
17210
17464
  const options = program.opts();
17211
- return !options.json && !options.pretty && (identity || options.human);
17465
+ return !options.json && !options.pretty && (identity2 || options.human);
17212
17466
  }
17213
17467
  function emitRecording(value, pretty) {
17214
17468
  if (humanOutput()) process.stdout.write(formatRecording(value, program.opts().apiUrl));
@@ -17251,10 +17505,10 @@ async function authorize(quiet = false) {
17251
17505
  }
17252
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 () => {
17253
17507
  const options = program.opts();
17254
- const identity = await whoami(options.apiUrl);
17255
- if (humanOutput(true)) process.stdout.write(formatIdentity(identity));
17256
- else emit(identity, options.pretty);
17257
- 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;
17258
17512
  });
17259
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) => {
17260
17514
  if (!input.email) throw new Error("validation_failed: Supply --email <email> to start a connection.");
@@ -17526,6 +17780,10 @@ drafts.command("purge").action(async () => {
17526
17780
  emit({ schemaVersion: 1, purged }, options.pretty);
17527
17781
  });
17528
17782
  program.hook("preAction", async (_command, actionCommand) => {
17783
+ if (!supportsNodeRuntime(process.version)) {
17784
+ if (commandPath(actionCommand).join(" ") === "doctor") return;
17785
+ assertNodeRuntime();
17786
+ }
17529
17787
  if (commandPath(actionCommand).join(" ") === "record start") assertRecorderTransport();
17530
17788
  const command = cliCommandCategory(commandPath(actionCommand));
17531
17789
  if (!command) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runalabs/rill-cli",
3
- "version": "0.1.19",
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"