@runalabs/rill-cli 0.1.20 → 0.1.22

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.
package/dist/cli.js CHANGED
@@ -15160,12 +15160,14 @@ config(en_default());
15160
15160
  // ../contracts/src/encoding.ts
15161
15161
  var ENCODING_PART_BYTES = 4 * 1024 * 1024;
15162
15162
  var ENCODING_MAX_BYTES = 1024 * 1024 * 1024;
15163
+ var ENCODING_MAX_SECONDS = 600;
15164
+ var ENCODING_MAX_FRAMES = 36e3;
15163
15165
  var frameManifestSchema = external_exports.object({
15164
15166
  version: external_exports.literal(1),
15165
- durationSeconds: external_exports.number().positive().max(600),
15167
+ durationSeconds: external_exports.number().positive().max(ENCODING_MAX_SECONDS),
15166
15168
  width: external_exports.number().int().min(320).max(3840),
15167
15169
  height: external_exports.number().int().min(240).max(2160),
15168
- frames: external_exports.array(external_exports.object({ timestamp: external_exports.number().nonnegative().finite(), size: external_exports.number().int().positive().max(ENCODING_PART_BYTES) }).strict()).min(1).max(36e3)
15170
+ frames: external_exports.array(external_exports.object({ timestamp: external_exports.number().nonnegative().finite(), size: external_exports.number().int().positive().max(ENCODING_PART_BYTES) }).strict()).min(1).max(ENCODING_MAX_FRAMES)
15169
15171
  }).strict().superRefine((m, ctx) => {
15170
15172
  if (m.frames.reduce((n, f) => n + f.size, 0) > ENCODING_MAX_BYTES) ctx.addIssue({ code: "custom", message: "Frame upload exceeds the trial byte limit." });
15171
15173
  for (let i = 1; i < m.frames.length; i++) if (m.frames[i].timestamp < m.frames[i - 1].timestamp) ctx.addIssue({ code: "custom", message: "Frame timestamps must be ordered." });
@@ -15267,7 +15269,7 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
15267
15269
  import { spawn as spawn8 } from "child_process";
15268
15270
  import { hostname as hostname4, tmpdir as tmpdir3 } from "os";
15269
15271
  import { Command, CommanderError } from "commander";
15270
- import { basename, dirname as dirname2, join as join11 } from "path";
15272
+ import { basename, dirname as dirname3, join as join12 } from "path";
15271
15273
  import { readdir as readdir2, rm as rm7, stat as stat5 } from "fs/promises";
15272
15274
 
15273
15275
  // ../recorder/src/windows-ipc.ts
@@ -15376,9 +15378,17 @@ import { spawn as spawn4 } from "child_process";
15376
15378
  // ../recorder/src/browser-tabs.ts
15377
15379
  import { chromium } from "playwright-core";
15378
15380
 
15381
+ // ../contracts/src/redaction.js
15382
+ var tokenValuePattern = /\b(?:sk|pk|ghp|github_pat|xox[baprs]|eyJ)[-_a-zA-Z0-9.]{16,}\b/g;
15383
+ function redactText(value, maxLength = 8e3) {
15384
+ return value.replace(tokenValuePattern, "[REDACTED]").slice(0, maxLength);
15385
+ }
15386
+ function redactDiagnosticText(value, maxLength = 2e3) {
15387
+ return redactText(value.replace(/(bearer\s+|["']?(?:password|secret|token|authorization|cookie|api[_-]?key)["']?\s*[:=]\s*["']?)[^\s"',;]+/gi, "$1[REDACTED]"), maxLength);
15388
+ }
15389
+
15379
15390
  // ../recorder/src/redaction.ts
15380
15391
  var secretKeyPattern = /(?:pass(?:word)?|secret|token|api[_-]?key|authorization|cookie|session|access[_-]?token|refresh[_-]?token|client[_-]?secret)/i;
15381
- var tokenValuePattern = /\b(?:sk|pk|ghp|github_pat|xox[baprs]|eyJ)[-_a-zA-Z0-9.]{16,}\b/g;
15382
15392
  function sanitizeUrl(input) {
15383
15393
  try {
15384
15394
  const url2 = new URL(input);
@@ -15391,9 +15401,6 @@ function sanitizeUrl(input) {
15391
15401
  return "[invalid-url]";
15392
15402
  }
15393
15403
  }
15394
- function redactText(value, maxLength = 8e3) {
15395
- return value.replace(tokenValuePattern, "[REDACTED]").slice(0, maxLength);
15396
- }
15397
15404
  function redactBody(value, contentType, maxBytes = 65536) {
15398
15405
  if (Buffer.byteLength(value) > maxBytes) return { body: void 0, omittedReason: "body_too_large" };
15399
15406
  if (!isAllowedBodyType(contentType)) return { body: void 0, omittedReason: "unsupported_content_type" };
@@ -15472,8 +15479,9 @@ async function connectRecordingTab(cdpUrl, outputDirectory, tabId) {
15472
15479
  }
15473
15480
 
15474
15481
  // ../recorder/src/client.ts
15475
- async function ensureDaemon(cliEntryPath, requireTabSelection = false) {
15482
+ async function ensureDaemon(cliEntryPath, requireTabSelection = false, requireChromeConnection = false) {
15476
15483
  const check2 = (health2) => {
15484
+ if (requireChromeConnection && !health2.capabilities?.includes("chrome-connection-v1")) throw new Error("browser_unavailable: Finish your current recordings, then restart Rill to connect Chrome.");
15477
15485
  if (requireTabSelection && !health2.capabilities?.includes("tab-selection-v1")) throw new Error("browser_unavailable: The running Rill recorder predates tab selection. Stop active recordings, then restart the recorder service before attaching.");
15478
15486
  };
15479
15487
  let health;
@@ -15505,6 +15513,15 @@ async function ensureDaemon(cliEntryPath, requireTabSelection = false) {
15505
15513
  }
15506
15514
  throw new Error("The local recorder service did not start.");
15507
15515
  }
15516
+ function connectBrowser() {
15517
+ return callDaemon("POST", "/browser/connect", {});
15518
+ }
15519
+ function browserConnectionStatus() {
15520
+ return callDaemon("GET", "/browser/status");
15521
+ }
15522
+ function disconnectBrowser() {
15523
+ return callDaemon("POST", "/browser/disconnect", {});
15524
+ }
15508
15525
  function startRecording(input) {
15509
15526
  return callDaemon("POST", "/sessions", input);
15510
15527
  }
@@ -15543,6 +15560,20 @@ async function callDaemon(method, path, body) {
15543
15560
  });
15544
15561
  }
15545
15562
 
15563
+ // src/browser-recording.ts
15564
+ async function prepareBrowserRecording(options, getConnection) {
15565
+ if (!["existing", "new"].includes(options.browser ?? "new")) throw new Error("validation_failed: Choose an existing tab or a new browser.");
15566
+ if (!["human", "agent"].includes(options.operator ?? "human")) throw new Error("validation_failed: Choose who will perform the steps: human or agent.");
15567
+ if (options.cdpUrl || (options.browser ?? "new") === "new") return void 0;
15568
+ const connection = await getConnection();
15569
+ if (!connection.connected || !connection.tab) throw Object.assign(new Error("browser_unavailable: Connect Chrome and choose the tab you want to record."), { recovery: "Run rill browser connect, then choose your tab in Rill for Chrome." });
15570
+ if (options.url || options.tabId) throw new Error("validation_failed: Open the page you want to record, then choose it in Rill for Chrome.");
15571
+ if (options.encoding && options.encoding !== "remote") throw new Error("validation_failed: Connected Chrome uses the standard recording upload.");
15572
+ if (options.operator === "agent" && !options.controllerTarget) throw new Error("browser_unavailable: Confirm your agent can control the selected tab before recording. Use its target ID with --controller-target, or use --operator human to perform the steps yourself.");
15573
+ if (options.controllerTarget && options.controllerTarget !== connection.tab.targetId) throw new Error("browser_unavailable: Your agent is using a different tab. Select the same tab before recording.");
15574
+ return connection;
15575
+ }
15576
+
15546
15577
  // ../recorder/src/retention.ts
15547
15578
  import { readdir, readFile as readFile5, rm as rm4 } from "fs/promises";
15548
15579
  import { join as join6 } from "path";
@@ -15574,30 +15605,55 @@ async function hasRetainedRemoteCaptures(root) {
15574
15605
  }
15575
15606
 
15576
15607
  // ../recorder/src/daemon.ts
15577
- import { createServer as createServer2 } from "http";
15578
- import { mkdir as mkdir7, readFile as readFile7, unlink, writeFile as writeFile6, rename as rename2, stat as stat4, chmod } from "fs/promises";
15579
- import { dirname, join as join10 } from "path";
15608
+ import { createServer as createServer3 } from "http";
15609
+ import { mkdir as mkdir9, readFile as readFile8, unlink, writeFile as writeFile8, rename as rename2, stat as stat4, chmod } from "fs/promises";
15610
+ import { dirname as dirname2, join as join11 } from "path";
15580
15611
 
15581
15612
  // ../recorder/src/click-capture.ts
15582
15613
  import { randomUUID as randomUUID2 } from "crypto";
15614
+
15615
+ // ../contracts/src/click-capture.js
15616
+ function installClickCapture(binding, key2) {
15617
+ if (window[key2]) return;
15618
+ const listener = (event) => {
15619
+ if (!event.isTrusted) return;
15620
+ const element2 = event.composedPath().find((value) => value instanceof Element);
15621
+ if (!element2 || element2.closest("[data-rill-cursor]")) return;
15622
+ const control = element2.closest('button, a, input, select, textarea, [role="button"], [role="link"]') || element2;
15623
+ const tag = control.tagName.toLowerCase();
15624
+ const editable = Boolean(element2.closest('[contenteditable]:not([contenteditable="false"])'));
15625
+ const containsFields = Boolean(control.querySelector('input, select, textarea, [contenteditable]:not([contenteditable="false"])'));
15626
+ const labelled = !editable && control.getAttribute("aria-label");
15627
+ const text = !editable && !containsFields && ["button", "a"].includes(tag) ? control.textContent : "";
15628
+ const label = (labelled || text || (tag === "input" ? `input (${control.getAttribute("type") || "text"})` : tag)).replace(/\s+/g, " ").trim().slice(0, 80) || "Element";
15629
+ try {
15630
+ window[binding](JSON.stringify({ x: event.clientX, y: event.clientY, label }));
15631
+ } catch {
15632
+ }
15633
+ };
15634
+ document.addEventListener("click", listener, { capture: true });
15635
+ window[key2] = () => {
15636
+ document.removeEventListener("click", listener, { capture: true });
15637
+ delete window[key2];
15638
+ };
15639
+ }
15640
+ function clickCaptureSource(binding, key2) {
15641
+ return `(${installClickCapture.toString()})(${JSON.stringify(binding)},${JSON.stringify(key2)})`;
15642
+ }
15643
+ function clickCaptureCleanupSource(binding, key2) {
15644
+ return `(() => { if (typeof window[${JSON.stringify(key2)}] === 'function') window[${JSON.stringify(key2)}](); delete window[${JSON.stringify(binding)}]; })()`;
15645
+ }
15646
+
15647
+ // ../recorder/src/click-capture.ts
15583
15648
  async function captureClicks(page, session, onClick) {
15584
15649
  const binding = `__rillClick_${randomUUID2().replaceAll("-", "")}`;
15585
- const source = `(() => {
15586
- const key = ${JSON.stringify(binding)};
15587
- if (window[key + '_listener']) return;
15588
- const listener = event => {
15589
- const target = event.target;
15590
- const label = target?.innerText?.trim().slice(0, 80) || target?.getAttribute?.('aria-label') || target?.tagName || 'Element';
15591
- try { window[key](JSON.stringify({ x: event.clientX, y: event.clientY, label })); } catch {}
15592
- };
15593
- window[key + '_listener'] = listener;
15594
- document.addEventListener('click', listener, { capture: true });
15595
- })();`;
15650
+ const key2 = `${binding}_listener`;
15651
+ const source = clickCaptureSource(binding, key2);
15596
15652
  const receive = (event) => {
15597
15653
  if (event.name !== binding) return;
15598
15654
  try {
15599
15655
  const value = JSON.parse(event.payload);
15600
- if (typeof value.x === "number" && Number.isFinite(value.x) && typeof value.y === "number" && Number.isFinite(value.y) && typeof value.label === "string") onClick({ x: value.x, y: value.y, label: value.label.slice(0, 80) });
15656
+ if (typeof value.x === "number" && Number.isFinite(value.x) && typeof value.y === "number" && Number.isFinite(value.y) && typeof value.label === "string") onClick({ x: value.x, y: value.y, label: redactDiagnosticText(value.label.replace(/[\u0000-\u001f\u007f]/g, ""), 80) });
15601
15657
  } catch {
15602
15658
  }
15603
15659
  };
@@ -15605,13 +15661,7 @@ async function captureClicks(page, session, onClick) {
15605
15661
  session.on("Runtime.bindingCalled", receive);
15606
15662
  const cleanup = async () => {
15607
15663
  if (identifier) await session.send("Page.removeScriptToEvaluateOnNewDocument", { identifier }).catch(() => void 0);
15608
- await Promise.all(page.frames().map((frame) => frame.evaluate((key2) => {
15609
- const target2 = window;
15610
- const listener = target2[key2 + "_listener"];
15611
- if (typeof listener === "function") document.removeEventListener("click", listener, { capture: true });
15612
- delete target2[key2 + "_listener"];
15613
- delete target2[key2];
15614
- }, binding).catch(() => void 0)));
15664
+ await Promise.all(page.frames().map((frame) => frame.evaluate(clickCaptureCleanupSource(binding, key2)).catch(() => void 0)));
15615
15665
  await session.send("Runtime.removeBinding", { name: binding }).catch(() => void 0);
15616
15666
  session.off("Runtime.bindingCalled", receive);
15617
15667
  };
@@ -15813,7 +15863,10 @@ var siteAccountSchema = external_exports.object({
15813
15863
  // ../contracts/src/recording.ts
15814
15864
  var cursorAppearanceSchema = external_exports.object({
15815
15865
  enabled: external_exports.boolean().default(true),
15816
- color: external_exports.string().regex(/^#[0-9a-fA-F]{6}$/).default("#6366f1")
15866
+ color: external_exports.string().regex(/^#[0-9a-fA-F]{6}$/).default("#6366f1"),
15867
+ shape: external_exports.enum(["collaborator", "arrow", "dot"]).default("collaborator"),
15868
+ size: external_exports.number().min(0.75).max(2).default(1),
15869
+ clickHighlights: external_exports.boolean().default(true)
15817
15870
  });
15818
15871
  var recordingStatusSchema = external_exports.enum([
15819
15872
  "draft",
@@ -15855,7 +15908,7 @@ var agentOutcomeSchema = external_exports.object({
15855
15908
  evidence: external_exports.string().max(1e3)
15856
15909
  }).strict();
15857
15910
 
15858
- // ../recorder/src/collaborator-pointer.ts
15911
+ // ../contracts/src/cursor-overlay.js
15859
15912
  var collaboratorPointerSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="17.3" height="17.3" viewBox="-0.75 -0.75 17.3 17.3">
15860
15913
  <!-- Hotspot is (0, 0) in user units: 0.75px right and 0.75px down from the SVG's top-left corner. -->
15861
15914
  <defs>
@@ -15877,8 +15930,6 @@ var collaboratorPointerStyles = `
15877
15930
  .rill-cursor-visual.label-above .rill-cursor-label { top:auto; bottom:6px; }
15878
15931
  @media (prefers-reduced-motion:reduce) { .rill-cursor-shape svg { transition:none; } }
15879
15932
  `;
15880
-
15881
- // ../recorder/src/scroll-capsule.ts
15882
15933
  var scrollCapsuleSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="20.5" height="42.5" viewBox="0 0 20.5 42.5">
15883
15934
  <!-- Capsule body is 18 x 40 at (1.25, 1.25); the white edge sits 1.25px outside it. Shown pointing down. -->
15884
15935
  <defs>
@@ -15906,11 +15957,10 @@ var scrollCapsuleStyles = `
15906
15957
  .scroll-indicator[data-direction="unknown"] #chevrons { visibility:hidden; }
15907
15958
  @media (prefers-reduced-motion:reduce) { .scroll-indicator,.scroll-indicator.is-visible { transform:none; transition:none; } }
15908
15959
  `;
15909
-
15910
- // ../recorder/src/cursor-overlay.ts
15911
15960
  function installCursorOverlay(options) {
15912
15961
  const scope = window;
15913
- if (scope[options.key]) return;
15962
+ if (scope[options.key])
15963
+ return;
15914
15964
  let host;
15915
15965
  let scrollTimer;
15916
15966
  let scrollExitTimer;
@@ -15920,13 +15970,15 @@ function installCursorOverlay(options) {
15920
15970
  controller.abort();
15921
15971
  window.clearTimeout(scrollTimer);
15922
15972
  window.clearTimeout(scrollExitTimer);
15923
- for (const animation of animations) animation.cancel();
15973
+ for (const animation of animations)
15974
+ animation.cancel();
15924
15975
  host?.remove();
15925
15976
  delete scope[options.key];
15926
15977
  };
15927
15978
  scope[options.key] = cleanup;
15928
15979
  const mount = () => {
15929
- if (controller.signal.aborted || host || !document.documentElement) return;
15980
+ if (controller.signal.aborted || host || !document.documentElement)
15981
+ return;
15930
15982
  host = document.createElement("div");
15931
15983
  host.setAttribute("data-rill-cursor", "");
15932
15984
  host.setAttribute("aria-hidden", "true");
@@ -15936,6 +15988,7 @@ function installCursorOverlay(options) {
15936
15988
  style.textContent = `
15937
15989
  :host, * { pointer-events: none !important; }
15938
15990
  ${options.pointerStyles}
15991
+ .rill-cursor-content { position:absolute; transform:scale(${options.size ?? 1}); transform-origin:0 0; }
15939
15992
  .pointer { display:none; --rill-cursor-accent:${options.color}; }
15940
15993
  ${options.scrollStyles}
15941
15994
  .scroll-indicator { --rill-cursor-accent:${options.color}; }
@@ -15945,7 +15998,7 @@ function installCursorOverlay(options) {
15945
15998
  `;
15946
15999
  const pointer = document.createElement("div");
15947
16000
  pointer.className = "pointer rill-cursor-visual";
15948
- pointer.innerHTML = options.pointerMarkup;
16001
+ pointer.innerHTML = '<span class="rill-cursor-content">' + options.pointerMarkup + "</span>";
15949
16002
  const scrollIndicator = document.createElement("div");
15950
16003
  scrollIndicator.className = "scroll-indicator";
15951
16004
  scrollIndicator.innerHTML = options.scrollMarkup + '<span class="rill-cursor-label scroll-label">Scrolling</span>';
@@ -15957,7 +16010,8 @@ function installCursorOverlay(options) {
15957
16010
  let scrollDirection;
15958
16011
  let drift;
15959
16012
  const stopDrift = () => {
15960
- if (!drift) return;
16013
+ if (!drift)
16014
+ return;
15961
16015
  animations.delete(drift);
15962
16016
  drift.cancel();
15963
16017
  drift = void 0;
@@ -15968,7 +16022,8 @@ function installCursorOverlay(options) {
15968
16022
  scrollIndicator.dataset.direction = direction;
15969
16023
  chevrons.setAttribute("transform", direction === "up" ? "rotate(180 10.25 21.25)" : "");
15970
16024
  scrollIndicator.classList.add("is-visible");
15971
- if (reducedMotion.matches || direction === "unknown") stopDrift();
16025
+ if (reducedMotion.matches || direction === "unknown")
16026
+ stopDrift();
15972
16027
  else if (direction !== scrollDirection || !drift) {
15973
16028
  stopDrift();
15974
16029
  const distance = direction === "up" || direction === "left" ? -3 : 3;
@@ -15984,25 +16039,31 @@ function installCursorOverlay(options) {
15984
16039
  scrollDirection = direction;
15985
16040
  scrollTimer = window.setTimeout(() => {
15986
16041
  scrollIndicator.classList.remove("is-visible");
15987
- if (reducedMotion.matches) stopDrift();
15988
- else scrollExitTimer = window.setTimeout(stopDrift, 180);
16042
+ if (reducedMotion.matches)
16043
+ stopDrift();
16044
+ else
16045
+ scrollExitTimer = window.setTimeout(stopDrift, 180);
15989
16046
  }, 550);
15990
16047
  };
15991
16048
  reducedMotion.addEventListener("change", () => {
15992
- if (reducedMotion.matches) stopDrift();
16049
+ if (reducedMotion.matches)
16050
+ stopDrift();
15993
16051
  }, { signal: controller.signal });
15994
16052
  const positions = /* @__PURE__ */ new WeakMap();
15995
16053
  positions.set(document, { x: window.scrollX, y: window.scrollY });
15996
16054
  document.addEventListener("scroll", (event) => {
15997
- if (!event.isTrusted || document.hidden) return;
16055
+ if (!event.isTrusted || document.hidden)
16056
+ return;
15998
16057
  const target2 = event.target;
15999
- if (target2 !== document && !(target2 instanceof Element)) return;
16058
+ if (target2 !== document && !(target2 instanceof Element))
16059
+ return;
16000
16060
  const position = target2 === document ? { x: window.scrollX, y: window.scrollY } : { x: target2.scrollLeft, y: target2.scrollTop };
16001
16061
  const previous = positions.get(target2);
16002
16062
  positions.set(target2, position);
16003
16063
  const dx = previous ? position.x - previous.x : 0;
16004
16064
  const dy = previous ? position.y - previous.y : 0;
16005
- if (previous && !dx && !dy) return;
16065
+ if (previous && !dx && !dy)
16066
+ return;
16006
16067
  const direction = !previous ? "unknown" : Math.abs(dy) >= Math.abs(dx) ? dy > 0 ? "down" : "up" : dx > 0 ? "right" : "left";
16007
16068
  showScroll(direction);
16008
16069
  }, listenerOptions);
@@ -16011,20 +16072,22 @@ function installCursorOverlay(options) {
16011
16072
  pointer.classList.remove("is-pressed");
16012
16073
  };
16013
16074
  const move = (event) => {
16014
- if (event.pointerType === "touch") return false;
16075
+ if (event.pointerType === "touch")
16076
+ return false;
16015
16077
  if (event.target instanceof HTMLIFrameElement || event.target instanceof HTMLFrameElement) {
16016
16078
  hide();
16017
16079
  return false;
16018
16080
  }
16019
16081
  pointer.style.display = "block";
16020
16082
  pointer.style.transform = `translate(${event.clientX}px, ${event.clientY}px)`;
16021
- pointer.classList.toggle("label-left", event.clientX > window.innerWidth - 90);
16022
- pointer.classList.toggle("label-above", event.clientY > window.innerHeight - 34);
16083
+ pointer.classList.toggle("label-left", event.clientX > window.innerWidth - 90 * (options.size ?? 1));
16084
+ pointer.classList.toggle("label-above", event.clientY > window.innerHeight - 34 * (options.size ?? 1));
16023
16085
  pointer.classList.toggle("is-pressed", event.buttons !== 0);
16024
16086
  return true;
16025
16087
  };
16026
16088
  const release = (event) => {
16027
- if (event.pointerType !== "touch") pointer.classList.toggle("is-pressed", event.type !== "pointercancel" && event.buttons !== 0);
16089
+ if (event.pointerType !== "touch")
16090
+ pointer.classList.toggle("is-pressed", event.type !== "pointercancel" && event.buttons !== 0);
16028
16091
  };
16029
16092
  document.addEventListener("pointermove", move, listenerOptions);
16030
16093
  document.addEventListener("pointerover", move, listenerOptions);
@@ -16040,7 +16103,8 @@ function installCursorOverlay(options) {
16040
16103
  });
16041
16104
  };
16042
16105
  document.addEventListener("pointerdown", (event) => {
16043
- if (!move(event)) return;
16106
+ if (!move(event) || options.clickHighlights === false)
16107
+ return;
16044
16108
  const dot = document.createElement("div");
16045
16109
  dot.className = "click-dot";
16046
16110
  dot.style.left = `${event.clientX}px`;
@@ -16054,7 +16118,8 @@ function installCursorOverlay(options) {
16054
16118
  ring.className = "click-ring";
16055
16119
  ring.style.left = `${event.clientX}px`;
16056
16120
  ring.style.top = `${event.clientY}px`;
16057
- if (event.button === 2) ring.style.borderStyle = "dashed";
16121
+ if (event.button === 2)
16122
+ ring.style.borderStyle = "dashed";
16058
16123
  if (reducedMotion.matches) {
16059
16124
  ring.style.width = "26px";
16060
16125
  ring.style.height = "26px";
@@ -16066,22 +16131,49 @@ function installCursorOverlay(options) {
16066
16131
  ], reducedMotion.matches ? 120 : 320, "cubic-bezier(.2,.7,.2,1)");
16067
16132
  }, listenerOptions);
16068
16133
  document.addEventListener("pointerout", (event) => {
16069
- if (!event.relatedTarget) hide();
16134
+ if (!event.relatedTarget)
16135
+ hide();
16070
16136
  }, listenerOptions);
16071
16137
  window.addEventListener("blur", hide, { signal: controller.signal });
16072
16138
  document.addEventListener("visibilitychange", () => {
16073
- if (document.hidden) hide();
16139
+ if (document.hidden)
16140
+ hide();
16074
16141
  }, { signal: controller.signal });
16075
16142
  };
16076
- if (document.documentElement) mount();
16077
- else document.addEventListener("DOMContentLoaded", mount, { once: true, signal: controller.signal });
16143
+ if (document.documentElement)
16144
+ mount();
16145
+ else
16146
+ document.addEventListener("DOMContentLoaded", mount, { once: true, signal: controller.signal });
16147
+ }
16148
+ var arrowSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="17.3" height="17.3" viewBox="-0.75 -0.75 17.3 17.3"><path d="M0 0L0 15L4.2 11.2L7.1 15.5L10 14.3L7.2 9.8L13 9Z" fill="var(--rill-cursor-accent)" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>';
16149
+ var dotSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="-9 -9 18 18"><circle r="6" fill="var(--rill-cursor-accent)" stroke="#fff" stroke-width="1.5"/></svg>';
16150
+ var cursorPointerStyles = collaboratorPointerStyles + "\n.rill-cursor-dot { left:-9px; top:-9px; width:18px; height:18px; } .rill-cursor-dot svg { width:18px; height:18px; transform-origin:9px 9px; }";
16151
+ function cursorPointerMarkup(shape = "collaborator") {
16152
+ return shape === "arrow" ? '<span class="rill-cursor-shape">' + arrowSvg + "</span>" : shape === "dot" ? '<span class="rill-cursor-shape rill-cursor-dot">' + dotSvg + "</span>" : collaboratorPointerMarkup;
16153
+ }
16154
+ function normalizeCursorAppearance(input = {}) {
16155
+ input = input && typeof input === "object" ? input : {};
16156
+ return {
16157
+ enabled: input.enabled !== false,
16158
+ color: typeof input.color === "string" && /^#[0-9a-fA-F]{6}$/.test(input.color) ? input.color : "#6366f1",
16159
+ shape: ["collaborator", "arrow", "dot"].includes(input.shape) ? input.shape : "collaborator",
16160
+ size: typeof input.size === "number" && Number.isFinite(input.size) && input.size >= 0.75 && input.size <= 2 ? input.size : 1,
16161
+ clickHighlights: input.clickHighlights !== false
16162
+ };
16078
16163
  }
16164
+ function cursorOverlaySource(input, key2) {
16165
+ const appearance = normalizeCursorAppearance(input);
16166
+ if (!appearance.enabled) return "void 0";
16167
+ return "(" + installCursorOverlay.toString() + ")(" + JSON.stringify({ ...appearance, key: key2, pointerMarkup: cursorPointerMarkup(appearance.shape), pointerStyles: cursorPointerStyles, scrollMarkup: scrollCapsuleSvg, scrollStyles: scrollCapsuleStyles }) + ")";
16168
+ }
16169
+
16170
+ // ../recorder/src/cursor-overlay.ts
16079
16171
  async function attachCursorOverlay(page, cdp, input) {
16080
16172
  const appearance = cursorAppearanceSchema.parse(input ?? {});
16081
16173
  if (!appearance.enabled) return async () => {
16082
16174
  };
16083
16175
  const key2 = `__rillCursor_${crypto.randomUUID().replaceAll("-", "")}`;
16084
- const source = `(${installCursorOverlay.toString()})(${JSON.stringify({ ...appearance, key: key2, pointerMarkup: collaboratorPointerMarkup, pointerStyles: collaboratorPointerStyles, scrollMarkup: scrollCapsuleSvg, scrollStyles: scrollCapsuleStyles })})`;
16176
+ const source = cursorOverlaySource(appearance, key2);
16085
16177
  await cdp.send("Page.enable");
16086
16178
  const { identifier } = await cdp.send("Page.addScriptToEvaluateOnNewDocument", { source });
16087
16179
  let stopped = false;
@@ -16394,18 +16486,34 @@ async function encodeFrames(directory, manifest, output, signal) {
16394
16486
  return withEncodingSlot(async () => {
16395
16487
  signal?.throwIfAborted();
16396
16488
  const lines = ["ffconcat version 1.0"];
16397
- for (let i = 0; i < manifest.frames.length; i++) {
16398
- const next = manifest.frames[i + 1];
16399
- const duration3 = next ? next.timestamp - manifest.frames[i].timestamp : manifest.durationSeconds - (manifest.frames[i].timestamp - manifest.frames[0].timestamp);
16400
- lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
16489
+ if (output.endsWith(".mp4")) {
16490
+ for (let i = 0; i < manifest.frames.length; i++) {
16491
+ const next = manifest.frames[i + 1];
16492
+ const duration3 = next ? next.timestamp - manifest.frames[i].timestamp : manifest.durationSeconds - (manifest.frames[i].timestamp - manifest.frames[0].timestamp);
16493
+ lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
16494
+ }
16495
+ lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
16496
+ } else {
16497
+ const endTick = Math.max(0, Math.ceil(manifest.durationSeconds * 25) - 2);
16498
+ const frames = [];
16499
+ manifest.frames.forEach((frame, index) => {
16500
+ const tick = Math.min(endTick, Math.max(0, Math.round((frame.timestamp - manifest.frames[0].timestamp) * 25)));
16501
+ if (frames.at(-1)?.tick === tick) frames[frames.length - 1] = { index, tick };
16502
+ else frames.push({ index, tick });
16503
+ });
16504
+ if (frames.at(-1).tick < endTick) frames.push({ ...frames.at(-1), tick: endTick });
16505
+ frames.forEach((frame, index) => {
16506
+ lines.push(`file 'frame-${String(frame.index).padStart(6, "0")}.jpg'`);
16507
+ if (frames[index + 1]) lines.push(`duration ${((frames[index + 1].tick - frame.tick) / 25).toFixed(6)}`);
16508
+ });
16401
16509
  }
16402
- lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
16403
16510
  await writeFile3(join7(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
16404
16511
  const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
16405
16512
  if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
16406
16513
  signal?.throwIfAborted();
16407
- const dimensions = `scale=${manifest.width}:${manifest.height}:force_original_aspect_ratio=decrease,pad=${manifest.width}:${manifest.height}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
16408
- const filters = output.endsWith(".mp4") ? `${dimensions},fps=30,tpad=stop_mode=clone:stop_duration=1` : dimensions;
16514
+ const width = Math.ceil(manifest.width / 2) * 2, height = Math.ceil(manifest.height / 2) * 2;
16515
+ const dimensions = `scale=${width}:${height}:force_original_aspect_ratio=decrease:force_divisible_by=2,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
16516
+ const filters = output.endsWith(".mp4") ? `${dimensions},fps=30,tpad=stop_mode=clone:stop_duration=1` : `${dimensions},fps=25`;
16409
16517
  const codec2 = output.endsWith(".mp4") ? ["-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-movflags", "+faststart", "-fps_mode", "cfr", "-t", String(Math.min(60, manifest.durationSeconds))] : ["-c:v", "libvpx-vp9", "-deadline", "good", "-cpu-used", "4", "-b:v", "0", "-crf", "32"];
16410
16518
  await new Promise((resolve, reject) => {
16411
16519
  const child = spawn5(process.env.RILL_FFMPEG ?? "ffmpeg", ["-hide_banner", "-loglevel", "error", "-protocol_whitelist", "file", "-f", "concat", "-safe", "1", "-i", "frames.ffconcat", "-vsync", "vfr", "-pix_fmt", "yuv420p", "-vf", filters, ...codec2, "-threads", String(threads), "-y", output], { cwd: directory, windowsHide: true, stdio: ["ignore", "ignore", "pipe"] });
@@ -16457,9 +16565,18 @@ var ScreencastRecorder = class {
16457
16565
  frameCounter = 0;
16458
16566
  frameBytes = 0;
16459
16567
  captureError;
16568
+ frameListener;
16569
+ async waitForFirstFrame(timeoutMs = 8e3) {
16570
+ const deadline = Date.now() + timeoutMs;
16571
+ while (!this.frames.length) {
16572
+ if (this.captureError) throw this.captureError;
16573
+ if (Date.now() >= deadline) throw new Error("browser_unavailable: No picture received. Keep the selected tab open and try again.");
16574
+ await new Promise((resolve) => setTimeout(resolve, 25));
16575
+ }
16576
+ }
16460
16577
  async start() {
16461
16578
  await mkdir5(this.directory, { recursive: true, mode: 448 });
16462
- this.session.on("Page.screencastFrame", (payload) => {
16579
+ this.frameListener = (payload) => {
16463
16580
  const frameEpochMs = (payload.metadata.timestamp ?? Date.now() / 1e3) * 1e3;
16464
16581
  this.firstFrameEpochMs = Math.min(this.firstFrameEpochMs ?? frameEpochMs, frameEpochMs);
16465
16582
  const bytes = Buffer.from(payload.data, "base64");
@@ -16480,14 +16597,17 @@ var ScreencastRecorder = class {
16480
16597
  void this.session.send("Page.screencastFrameAck", { sessionId: payload.sessionId }).catch(() => void 0);
16481
16598
  });
16482
16599
  this.pendingWrites.add(write);
16483
- });
16600
+ };
16601
+ this.session.on("Page.screencastFrame", this.frameListener);
16484
16602
  await this.session.send("Page.enable");
16485
- await this.session.send("Page.startScreencast", { format: "jpeg", quality: 82, maxWidth: this.width, maxHeight: this.height, everyNthFrame: 1 });
16603
+ const options = { format: "jpeg", quality: 82, maxWidth: this.width, maxHeight: this.height, everyNthFrame: 1, sendLastFrame: true };
16604
+ await this.session.send("Page.startScreencast", options);
16486
16605
  this.started = true;
16487
16606
  }
16488
16607
  async stop(outputPath, expectedDurationSeconds, captureOnly = false, signal) {
16489
16608
  if (this.started) await this.session.send("Page.stopScreencast").catch(() => void 0);
16490
16609
  await new Promise((resolve) => setTimeout(resolve, 120));
16610
+ if (this.frameListener) this.session.off("Page.screencastFrame", this.frameListener);
16491
16611
  await Promise.all(this.pendingWrites);
16492
16612
  if (this.captureError) throw this.captureError;
16493
16613
  if (!this.frames.length) throw new Error("No video frames were captured.");
@@ -16593,6 +16713,7 @@ var RecordingSession = class _RecordingSession {
16593
16713
  await options.beforeCapture?.(runtime.page);
16594
16714
  session.startedAt = /* @__PURE__ */ new Date();
16595
16715
  await screencast.start();
16716
+ await screencast.waitForFirstFrame();
16596
16717
  if (options.url) await session.page.goto(options.url, { waitUntil: "domcontentloaded" });
16597
16718
  } catch (error51) {
16598
16719
  await session.stop("failed").catch(() => void 0);
@@ -16845,21 +16966,476 @@ async function waitForCdp(cdpUrl, browserProcess) {
16845
16966
  throw new Error("Timed out waiting for Chrome DevTools Protocol.");
16846
16967
  }
16847
16968
 
16969
+ // ../chrome-extension/brand.ts
16970
+ var rillLogo = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSI4IiBmaWxsPSIjZjdmNmYxIiAvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE2IDE2KSBzY2FsZSguOSkgdHJhbnNsYXRlKC0xNiAtMTYpIj48ZyB0cmFuc2Zvcm09InJvdGF0ZSgtMTAgMTYgMTYpIj48cmVjdCB4PSI2IiB5PSIxMSIgd2lkdGg9IjgiIGhlaWdodD0iMTciIHJ4PSI0IiBmaWxsPSIjMTcxNzE3IiAvPjxyZWN0IHg9IjE4LjQiIHk9IjUuNCIgd2lkdGg9IjgiIGhlaWdodD0iMTciIHJ4PSI0IiBmaWxsPSIjMTcxNzE3IiAvPjxyZWN0IHg9IjE3IiB5PSI0IiB3aWR0aD0iOCIgaGVpZ2h0PSIxNyIgcng9IjQiIGZpbGw9IiM3MTQ2ZmYiIHN0cm9rZT0iIzE3MTcxNyIgc3Ryb2tlLXdpZHRoPSIxLjgiIC8+PC9nPjwvZz4KPC9zdmc+Cg==";
16971
+ var rillFont = "data:font/woff2;base64,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";
16972
+
16973
+ // ../../shared/brand.ts
16974
+ var brand = {
16975
+ purple: "#7146ff",
16976
+ purpleDark: "#4c25cf",
16977
+ purplePale: "#eee9ff",
16978
+ ink: "#171717",
16979
+ paper: "#f7f6f1",
16980
+ paperRaised: "#fbfaf6",
16981
+ panel: "#efede7",
16982
+ muted: "#6d6a66",
16983
+ faint: "#a8a49d",
16984
+ rule: "#b7b3ab",
16985
+ ruleSoft: "#dedbd4",
16986
+ danger: "#c23434",
16987
+ sans: "Geist, 'Geist Variable', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif"
16988
+ };
16989
+ function brandCssVariables() {
16990
+ return `--purple:${brand.purple};--purple-dark:${brand.purpleDark};--purple-pale:${brand.purplePale};--tint:${brand.purplePale};--ink:${brand.ink};--paper:${brand.paper};--paper-raised:${brand.paperRaised};--panel:${brand.panel};--muted:${brand.muted};--faint:${brand.faint};--rule:${brand.rule};--rule-soft:${brand.ruleSoft};--danger:${brand.danger};--sans:${brand.sans}`;
16991
+ }
16992
+
16993
+ // ../chrome-extension/connection-page.ts
16994
+ function renderChromeConnectionPage(downloadUrl, storeUrl, content) {
16995
+ const hosted = downloadUrl !== "/extension.zip";
16996
+ const installUrl = storeUrl && /^https:\/\/chromewebstore\.google\.com\/detail\/[a-z0-9-]+\/[a-p]{32}$/.test(storeUrl) ? storeUrl : downloadUrl;
16997
+ const page = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${content?.title ?? (hosted ? "Record with your chat" : "Record with your agent")} \xB7 Rill</title><style>
16998
+ @font-face{font-family:Geist;src:url('${rillFont}') format('woff2');font-weight:100 900;font-display:swap}:root{${brandCssVariables()};font-family:var(--sans);-webkit-font-smoothing:antialiased;color:var(--ink);background:var(--paper)}*{box-sizing:border-box}body{margin:0;line-height:1.6}header{display:flex;align-items:center;justify-content:space-between;padding:24px clamp(24px,6vw,80px);border-bottom:1px solid var(--rule-soft)}.brand{display:flex;align-items:center;gap:10px;font-size:22px;font-weight:650;letter-spacing:-.6px}.brand img{width:32px;height:32px}.context{font-size:12px;color:var(--muted)}main{width:min(520px,calc(100% - 40px));margin:8vh auto 5vh;background:var(--paper-raised);padding:32px;border-radius:28px;box-shadow:0 1px 3px #00000008,0 12px 40px #00000006}.step{font-size:12px;color:var(--purple-dark);margin:0 0 16px}h1{font-weight:550;font-size:34px;line-height:1.14;letter-spacing:-1.4px;text-wrap:balance;margin:0 0 18px}p{color:var(--muted);text-wrap:pretty;font-size:15px;margin:0 0 22px}button,.button{display:flex;align-items:center;justify-content:center;font:550 15px Geist,system-ui,sans-serif;min-height:48px;width:100%;padding:13px 18px;border:0;border-radius:12px;color:white;background:var(--purple);cursor:pointer;text-decoration:none;transition:scale .16s cubic-bezier(.2,0,0,1),background-color .16s}button:hover,.button:hover{background:var(--purple-dark)}button:active,.button:active{scale:.96}button:disabled{opacity:.5;cursor:default}a:focus-visible,button:focus-visible{outline:3px solid var(--purple);outline-offset:4px}#status{font-size:13px;margin:16px 0 0;min-height:21px}.privacy{margin-top:28px;padding-top:22px;border-top:1px solid #eee9e1;font-size:12px;line-height:1.7}footer{text-align:center;color:var(--muted);font-size:12px;padding:0 24px 32px}[hidden]{display:none!important}@media(max-width:540px){main{margin-top:5vh;padding:24px}h1{font-size:30px}header{padding:20px 24px}}@media(prefers-reduced-motion:reduce){button,.button{transition:none}button:active,.button:active{scale:1}}
16999
+ </style></head><body><header><div class="brand"><img src="${rillLogo}" alt="">Rill</div><span class="context">Chrome recorder</span></header><main><p class="step" id="step">One-time setup</p><h1 id="heading">Record right where you work.</h1><p id="intro">${hosted && !content?.direct ? "Connect Rill to Chrome. We\u2019ll guide you through recording your page, then bring the video back to your chat." : "Connect Rill to Chrome, then choose the page you want to record."}</p><button id="connect-chrome" hidden disabled>Connect Chrome</button><a id="install" class="button" href="${installUrl}">Get Rill for Chrome</a><p id="status" role="status" aria-live="polite">Add Rill for Chrome, then reload this page to continue.</p><p class="privacy">You choose what to record. Rill saves only that tab, including anything visible in it, with browser errors and request details. ${hosted ? "Your video starts private in your Rill workspace." : "Rill creates a recording link. Anyone with that link can watch until you turn off sharing."}</p></main><footer>Your page. Your steps. One clear recording.</footer></body></html>`;
17000
+ const rendered = content?.body ? page.replace(/<main>[\s\S]*<\/main>/, () => `<main>${content.body}</main>`) : page;
17001
+ return hosted ? rendered.replace("</main>", '<section id="recording-handoff" class="privacy ph-no-capture" aria-label="What to record" data-ph-no-capture hidden></section></main><script defer src="/connect/chrome/handoff.js"></script>') : rendered;
17002
+ }
17003
+
17004
+ // ../recorder/src/browser-connection.ts
17005
+ import { EventEmitter } from "events";
17006
+ import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
17007
+ import { createServer as createServer2 } from "http";
17008
+ import { WebSocketServer, WebSocket } from "ws";
17009
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7 } from "fs/promises";
17010
+ import { dirname } from "path";
17011
+ var allowedMethods = /* @__PURE__ */ new Set(["Page.enable", "Page.getLayoutMetrics", "Page.startScreencast", "Page.stopScreencast", "Page.screencastFrameAck", "Runtime.enable", "Network.enable", "Rill.startInteractionCapture", "Rill.stopInteractionCapture"]);
17012
+ var allowedEvents = /* @__PURE__ */ new Set(["Page.screencastFrame", "Page.frameNavigated", "Page.navigatedWithinDocument", "Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Network.requestWillBeSent", "Network.responseReceived", "Network.loadingFailed", "Network.loadingFinished", "Rill.coverage", "Rill.interaction"]);
17013
+ var BrowserConnection = class extends EventEmitter {
17014
+ constructor(options = {}) {
17015
+ super();
17016
+ this.options = options;
17017
+ }
17018
+ options;
17019
+ server;
17020
+ websocketServer;
17021
+ socket;
17022
+ token = randomBytes3(32).toString("hex");
17023
+ pending = /* @__PURE__ */ new Map();
17024
+ sequence = 0;
17025
+ address = "";
17026
+ tab;
17027
+ starting;
17028
+ lastRecordingActivity = Date.now();
17029
+ captureInUse = false;
17030
+ status() {
17031
+ return { connected: Boolean(this.tab && this.socket?.readyState === WebSocket.OPEN), tab: this.tab, captureOnly: true };
17032
+ }
17033
+ get connected() {
17034
+ return this.status().connected;
17035
+ }
17036
+ get keepsRecorderAlive() {
17037
+ return Boolean(this.server?.listening && Date.now() - this.lastRecordingActivity < 60 * 6e4);
17038
+ }
17039
+ claimCapture() {
17040
+ if (this.captureInUse) throw new Error("This tab is already recording. Stop it before starting another take.");
17041
+ this.captureInUse = true;
17042
+ return () => {
17043
+ this.captureInUse = false;
17044
+ };
17045
+ }
17046
+ async connect() {
17047
+ if (!this.server || this.starting) {
17048
+ this.starting ??= this.listen().catch((error51) => {
17049
+ this.server = void 0;
17050
+ throw error51;
17051
+ }).finally(() => {
17052
+ this.starting = void 0;
17053
+ });
17054
+ await this.starting;
17055
+ }
17056
+ return { ...this.status(), connectionUrl: `${this.address}/rill-connect#${this.token}` };
17057
+ }
17058
+ async listen() {
17059
+ let port = 0;
17060
+ if (this.options.statePath) {
17061
+ try {
17062
+ const saved = JSON.parse(await readFile7(this.options.statePath, "utf8"));
17063
+ if (/^[a-f0-9]{64}$/.test(saved.token) && Number.isInteger(saved.port) && saved.port > 0 && saved.port < 65536) {
17064
+ this.token = saved.token;
17065
+ port = saved.port;
17066
+ }
17067
+ } catch {
17068
+ }
17069
+ }
17070
+ this.server = createServer2((request3, response) => {
17071
+ if (this.address && request3.headers.host === new URL(this.address).host && request3.method === "GET" && request3.url === "/extension.zip" && this.options.extensionArchive) {
17072
+ void readFile7(this.options.extensionArchive).then((bytes) => {
17073
+ response.writeHead(200, { "Content-Type": "application/zip", "Content-Disposition": 'attachment; filename="rill-chrome-extension.zip"', "Cache-Control": "no-store" });
17074
+ response.end(bytes);
17075
+ }, () => {
17076
+ response.writeHead(404);
17077
+ response.end("The download is not available. Ask your agent to rebuild this Rill preview.");
17078
+ });
17079
+ return;
17080
+ }
17081
+ if (!this.address || request3.headers.host !== new URL(this.address).host || request3.url !== "/rill-connect" || request3.method !== "GET") {
17082
+ response.writeHead(404);
17083
+ response.end();
17084
+ return;
17085
+ }
17086
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer", "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:; frame-ancestors 'none'; base-uri 'none'", "X-Content-Type-Options": "nosniff" });
17087
+ response.end(connectionPage);
17088
+ });
17089
+ this.websocketServer = new WebSocketServer({ noServer: true, maxPayload: 6 * 1024 * 1024, perMessageDeflate: false });
17090
+ this.server.on("upgrade", (request3, socket, head) => {
17091
+ const supplied = request3.headers["sec-websocket-protocol"]?.split(",").map((value) => value.trim());
17092
+ const candidate = supplied?.[1] ?? "";
17093
+ const authorized = /^[a-f0-9]{64}$/.test(candidate) && timingSafeEqual2(Buffer.from(candidate), Buffer.from(this.token));
17094
+ if (!this.address || request3.headers.host !== new URL(this.address).host || request3.url !== "/capture" || !/^chrome-extension:\/\/[a-p]{32}$/.test(request3.headers.origin ?? "") || supplied?.[0] !== "rill-capture" || !authorized || this.socket) {
17095
+ socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
17096
+ return;
17097
+ }
17098
+ this.websocketServer.handleUpgrade(request3, socket, head, (peer) => this.attach(peer));
17099
+ });
17100
+ await new Promise((resolve, reject) => {
17101
+ this.server.once("error", reject);
17102
+ this.server.listen(port, "127.0.0.1", resolve);
17103
+ });
17104
+ const address = this.server.address();
17105
+ if (!address || typeof address === "string") throw new Error("Could not connect Chrome. Try again.");
17106
+ this.address = `http://127.0.0.1:${address.port}`;
17107
+ if (this.options.statePath) {
17108
+ await mkdir7(dirname(this.options.statePath), { recursive: true, mode: 448 });
17109
+ await writeFile6(this.options.statePath, JSON.stringify({ token: this.token, port: address.port }), { mode: 384 });
17110
+ }
17111
+ }
17112
+ attach(peer) {
17113
+ this.socket = peer;
17114
+ const helloTimer = setTimeout(() => {
17115
+ if (!this.tab) peer.close(1008, "Choose a tab");
17116
+ }, 1e4);
17117
+ peer.on("error", () => peer.close());
17118
+ peer.on("message", (bytes) => {
17119
+ try {
17120
+ const message = JSON.parse(bytes.toString());
17121
+ if (message.type === "ping") {
17122
+ peer.send('{"type":"pong"}');
17123
+ return;
17124
+ }
17125
+ if (message.type === "tab") {
17126
+ if (!Number.isInteger(message.tabId) || message.tabId < 0 || !/^[a-zA-Z0-9_-]{1,128}$/.test(message.targetId) || !/^https?:\/\//.test(message.url)) throw new Error("Invalid tab");
17127
+ const next = {
17128
+ tabId: String(message.tabId),
17129
+ targetId: message.targetId,
17130
+ title: redactDiagnosticText(String(message.title ?? "").replace(/[\u0000-\u001f\u007f]/g, ""), 200),
17131
+ url: sanitizeUrl(message.url),
17132
+ ...message.captureCapabilities?.interactionCapture === true ? { captureCapabilities: { interactionCapture: true } } : {}
17133
+ };
17134
+ if (this.tab && this.tab.targetId !== next.targetId) throw new Error("Disconnect before choosing another tab");
17135
+ this.tab = next;
17136
+ clearTimeout(helloTimer);
17137
+ this.emit("connected", this.status());
17138
+ return;
17139
+ }
17140
+ if (message.type === "event" && this.tab && allowedEvents.has(message.method)) {
17141
+ this.emit(message.method, message.params);
17142
+ return;
17143
+ }
17144
+ if (message.type === "reply") {
17145
+ const pending2 = this.pending.get(message.id);
17146
+ if (!pending2) return;
17147
+ this.pending.delete(message.id);
17148
+ clearTimeout(pending2.timer);
17149
+ if (message.error) pending2.reject(new Error("Chrome could not finish recording. Keep the selected tab open and try again."));
17150
+ else pending2.resolve(message.result);
17151
+ }
17152
+ } catch {
17153
+ peer.close(1008, "Could not connect");
17154
+ }
17155
+ });
17156
+ peer.on("close", () => {
17157
+ clearTimeout(helloTimer);
17158
+ if (this.socket !== peer) return;
17159
+ this.socket = void 0;
17160
+ this.tab = void 0;
17161
+ for (const pending2 of this.pending.values()) {
17162
+ clearTimeout(pending2.timer);
17163
+ pending2.reject(new Error("browser_unavailable: Chrome disconnected. Your captured recording is being saved."));
17164
+ }
17165
+ this.pending.clear();
17166
+ this.emit("disconnected");
17167
+ });
17168
+ }
17169
+ send(method, params = {}) {
17170
+ if (method === "Page.startScreencast" || method === "Page.stopScreencast") this.lastRecordingActivity = Date.now();
17171
+ if (!allowedMethods.has(method)) return Promise.reject(new Error("This connection only records the selected tab."));
17172
+ if (!this.connected) return Promise.reject(new Error("browser_unavailable: Choose a tab in Rill for Chrome first."));
17173
+ const id = ++this.sequence;
17174
+ return new Promise((resolve, reject) => {
17175
+ const timer = setTimeout(() => {
17176
+ this.pending.delete(id);
17177
+ reject(new Error("browser_unavailable: Chrome took too long to respond. Try reconnecting your tab."));
17178
+ }, 1e4);
17179
+ this.pending.set(id, { resolve, reject, timer });
17180
+ this.socket.send(JSON.stringify({ id, method, params }), (error51) => {
17181
+ if (error51) {
17182
+ clearTimeout(timer);
17183
+ this.pending.delete(id);
17184
+ reject(error51);
17185
+ }
17186
+ });
17187
+ });
17188
+ }
17189
+ disconnect() {
17190
+ this.socket?.close(1e3, "Disconnected");
17191
+ }
17192
+ expireIdle() {
17193
+ if (Date.now() - this.lastRecordingActivity > 60 * 6e4) this.disconnect();
17194
+ }
17195
+ async close() {
17196
+ this.socket?.terminate();
17197
+ await new Promise((resolve) => this.websocketServer ? this.websocketServer.close(() => resolve()) : resolve());
17198
+ await new Promise((resolve) => this.server ? this.server.close(() => resolve()) : resolve());
17199
+ }
17200
+ };
17201
+ var connectionPage = renderChromeConnectionPage("/extension.zip", "https://chromewebstore.google.com/detail/rill-for-chrome/ehpjlnjimoljofijlkkdiefpcmiimdbp");
17202
+
17203
+ // ../recorder/src/extension-session.ts
17204
+ import { mkdir as mkdir8, writeFile as writeFile7 } from "fs/promises";
17205
+ import { join as join10 } from "path";
17206
+ var ExtensionRecordingSession = class _ExtensionRecordingSession {
17207
+ constructor(options, connection) {
17208
+ this.options = options;
17209
+ this.connection = connection;
17210
+ this.targetId = connection.status().tab.targetId;
17211
+ }
17212
+ options;
17213
+ connection;
17214
+ startedAt = /* @__PURE__ */ new Date();
17215
+ screencast;
17216
+ console = [];
17217
+ network = /* @__PURE__ */ new Map();
17218
+ timeline = [];
17219
+ listeners = [];
17220
+ interrupted = false;
17221
+ truncated = false;
17222
+ stopPromise;
17223
+ targetId;
17224
+ releaseCapture;
17225
+ interactionCapture = false;
17226
+ legacyExtension = false;
17227
+ static async start(options, connection) {
17228
+ if (!connection.connected) throw new Error("browser_unavailable: Open Rill in Chrome and choose a tab to record.");
17229
+ if (options.url || options.cdpUrl || options.tabId) throw new Error("validation_failed: Choose the tab in Rill for Chrome. Recording keeps the page as it is.");
17230
+ if (options.encoding !== "remote") throw new Error("validation_failed: Connected Chrome currently uses the standard recording upload.");
17231
+ if (options.controllerTarget && options.controllerTarget !== connection.status().tab.targetId) throw new Error("browser_unavailable: Your agent is using a different tab. Select the same tab before recording.");
17232
+ const session = new _ExtensionRecordingSession(options, connection);
17233
+ session.releaseCapture = connection.claimCapture();
17234
+ session.screencast = new ScreencastRecorder(connection, join10(options.outputDirectory, "frames"), options.width ?? 1280, options.height ?? 720);
17235
+ try {
17236
+ await mkdir8(options.outputDirectory, { recursive: true, mode: 448 });
17237
+ await writeFile7(join10(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
17238
+ session.attachDiagnostics();
17239
+ await connection.send("Runtime.enable");
17240
+ await connection.send("Network.enable");
17241
+ if (connection.status().tab?.captureCapabilities?.interactionCapture) {
17242
+ const cursorAppearance = cursorAppearanceSchema.parse(options.cursorAppearance ?? {});
17243
+ session.interactionCapture = true;
17244
+ await connection.send("Rill.startInteractionCapture", { cursorAppearance: { ...cursorAppearance, enabled: !options.hideCursor && cursorAppearance.enabled } });
17245
+ } else {
17246
+ session.legacyExtension = true;
17247
+ session.truncated = true;
17248
+ session.event("lifecycle", "To include clicks and your chosen pointer, update Rill for Chrome before your next recording.");
17249
+ }
17250
+ await session.screencast.start();
17251
+ await session.screencast.waitForFirstFrame();
17252
+ return session;
17253
+ } catch (error51) {
17254
+ await session.stopInteractions();
17255
+ await session.screencast.stop(join10(options.outputDirectory, "recording.webm"), 0.1, true).catch(() => void 0);
17256
+ session.detach();
17257
+ session.releaseCapture?.();
17258
+ throw error51;
17259
+ }
17260
+ }
17261
+ status() {
17262
+ const elapsedSeconds = Math.max(0, (Date.now() - this.startedAt.getTime()) / 1e3);
17263
+ const maxDurationSeconds = this.options.maxDurationSeconds ?? 600;
17264
+ return {
17265
+ recordingId: this.options.recordingId,
17266
+ cdpUrl: "",
17267
+ tabId: this.targetId,
17268
+ browser: "existing",
17269
+ startedAt: this.startedAt.toISOString(),
17270
+ elapsedSeconds,
17271
+ maxDurationSeconds,
17272
+ remainingSeconds: this.stopPromise ? 0 : Math.max(0, maxDurationSeconds - elapsedSeconds),
17273
+ stopsAt: new Date(this.startedAt.getTime() + maxDurationSeconds * 1e3).toISOString(),
17274
+ status: this.stopPromise ? "finalizing" : "recording",
17275
+ interrupted: this.interrupted,
17276
+ captureOnly: true,
17277
+ controllerVerified: false,
17278
+ ...this.interrupted ? { message: "Chrome disconnected. The recorded part is being saved." } : this.legacyExtension ? { message: "Chrome is recording. To include clicks and your chosen pointer, update Rill for Chrome before your next recording." } : {}
17279
+ };
17280
+ }
17281
+ stop(reason = "agent_request") {
17282
+ return this.stopPromise ??= this.finalize(reason);
17283
+ }
17284
+ on(method, listener) {
17285
+ this.connection.on(method, listener);
17286
+ this.listeners.push([method, listener]);
17287
+ }
17288
+ detach() {
17289
+ for (const [method, listener] of this.listeners) this.connection.off(method, listener);
17290
+ this.listeners = [];
17291
+ }
17292
+ offset() {
17293
+ return Math.max(0, Date.now() - this.startedAt.getTime());
17294
+ }
17295
+ event(type, label, severity, offsetMs = this.offset()) {
17296
+ if (this.timeline.length >= 1e4) {
17297
+ this.truncated = true;
17298
+ return;
17299
+ }
17300
+ this.timeline.push({ id: `time_${this.timeline.length + 1}`, offsetMs, type, label, ...severity ? { severity } : {} });
17301
+ }
17302
+ async stopInteractions() {
17303
+ if (!this.interactionCapture) return;
17304
+ await this.connection.send("Rill.stopInteractionCapture").catch(() => {
17305
+ this.truncated = true;
17306
+ });
17307
+ this.interactionCapture = false;
17308
+ }
17309
+ attachDiagnostics() {
17310
+ this.on("Rill.coverage", (value) => {
17311
+ this.truncated = true;
17312
+ this.event("lifecycle", value?.reason === "interaction_capture" ? "Some clicks or pointer highlights may be missing." : "Another tab opened. Only your original tab is being recorded.");
17313
+ });
17314
+ this.on("disconnected", () => {
17315
+ this.interrupted = true;
17316
+ this.event("lifecycle", "Chrome disconnected. Recording stopped.");
17317
+ });
17318
+ this.on("Page.frameNavigated", (value) => {
17319
+ if (!value.frame?.parentId) this.event("navigation", sanitizeUrl(String(value.frame?.url ?? "")));
17320
+ });
17321
+ this.on("Page.navigatedWithinDocument", (value) => {
17322
+ if (!value.parentId) this.event("navigation", sanitizeUrl(String(value.url ?? "")));
17323
+ });
17324
+ this.on("Rill.interaction", (value) => {
17325
+ if (!this.interactionCapture || value?.type !== "click" || !Number.isFinite(value.x) || !Number.isFinite(value.y) || typeof value.label !== "string") return;
17326
+ const label = redactDiagnosticText(value.label.replace(/[\u0000-\u001f\u007f]/g, ""), 80);
17327
+ this.event("click", `${label || "Element"} at ${Math.round(value.x)},${Math.round(value.y)}`);
17328
+ });
17329
+ this.on("Runtime.consoleAPICalled", (value) => {
17330
+ if (this.console.length >= 1e4) {
17331
+ this.truncated = true;
17332
+ return;
17333
+ }
17334
+ const severity = value.type === "error" || value.type === "assert" ? "error" : value.type === "warning" || value.type === "warn" ? "warning" : "info";
17335
+ const text = redactDiagnosticText((value.args ?? []).slice(0, 20).map((arg) => typeof arg.value === "string" ? arg.value : typeof arg.value === "number" || typeof arg.value === "boolean" ? String(arg.value) : "[object]").join(" "), 2e3);
17336
+ this.console.push({ id: `con_${this.console.length + 1}`, offsetMs: this.offset(), severity, text, stack: [] });
17337
+ if (severity !== "info") this.event("console", text.slice(0, 160), severity);
17338
+ });
17339
+ this.on("Runtime.exceptionThrown", (value) => {
17340
+ if (this.console.length >= 1e4) {
17341
+ this.truncated = true;
17342
+ return;
17343
+ }
17344
+ const text = redactDiagnosticText(String(value.exceptionDetails?.exception?.description ?? value.exceptionDetails?.text ?? "Page error"), 2e3);
17345
+ this.console.push({ id: `con_${this.console.length + 1}`, offsetMs: this.offset(), severity: "error", text, stack: [] });
17346
+ this.event("exception", text.slice(0, 160), "error");
17347
+ });
17348
+ this.on("Network.requestWillBeSent", (value) => {
17349
+ if (this.network.size >= 5e3) {
17350
+ this.truncated = true;
17351
+ return;
17352
+ }
17353
+ this.network.set(value.requestId, { id: `net_${this.network.size + 1}`, offsetMs: this.offset(), method: String(value.request?.method ?? "GET").slice(0, 20), url: sanitizeUrl(String(value.request?.url ?? "")), resourceType: String(value.type ?? "other").toLowerCase().slice(0, 40) });
17354
+ });
17355
+ this.on("Network.responseReceived", (value) => {
17356
+ const event = this.network.get(value.requestId);
17357
+ if (event && Number.isInteger(value.response?.status)) {
17358
+ event.status = value.response.status;
17359
+ if (event.status >= 400) this.event("network", `${event.method} ${event.url} \u2192 ${event.status}`, "error", event.offsetMs);
17360
+ }
17361
+ });
17362
+ this.on("Network.loadingFinished", (value) => {
17363
+ const event = this.network.get(value.requestId);
17364
+ if (event) {
17365
+ event.completedOffsetMs = this.offset();
17366
+ event.durationMs = Math.max(0, event.completedOffsetMs - event.offsetMs);
17367
+ }
17368
+ });
17369
+ this.on("Network.loadingFailed", (value) => {
17370
+ const event = this.network.get(value.requestId);
17371
+ if (event) {
17372
+ event.failureReason = redactDiagnosticText(String(value.errorText ?? "Request failed"), 200);
17373
+ event.completedOffsetMs = this.offset();
17374
+ event.durationMs = Math.max(0, event.completedOffsetMs - event.offsetMs);
17375
+ this.event("network", `${event.method} ${event.url} \u2192 ${event.failureReason}`, "error", event.offsetMs);
17376
+ }
17377
+ });
17378
+ }
17379
+ async finalize(reason) {
17380
+ await this.stopInteractions();
17381
+ const durationSeconds = Math.min(this.options.maxDurationSeconds ?? 600, Math.max(0.1, this.offset() / 1e3));
17382
+ const videoPath = join10(this.options.outputDirectory, "recording.webm");
17383
+ const diagnosticsPath = join10(this.options.outputDirectory, "diagnostics-v1.ndjson.gz");
17384
+ const network = [...this.network.values()];
17385
+ try {
17386
+ await this.screencast.stop(videoPath, durationSeconds, true);
17387
+ const bundle = {
17388
+ schemaVersion: 1,
17389
+ capturePolicyVersion: "safe-default-v1",
17390
+ recordingId: this.options.recordingId,
17391
+ startedAt: this.startedAt.toISOString(),
17392
+ durationMs: Math.round(durationSeconds * 1e3),
17393
+ videoStartOffsetMs: (this.screencast.videoStartedAtMs ?? this.startedAt.getTime()) - this.startedAt.getTime(),
17394
+ bodyCaptureEnabled: false,
17395
+ bodyCaptureRedacted: false,
17396
+ truncated: this.truncated || this.interrupted,
17397
+ console: this.console,
17398
+ network,
17399
+ timeline: this.timeline.toSorted((a, b) => a.offsetMs - b.offsetMs)
17400
+ };
17401
+ await writeBundle(bundle, diagnosticsPath);
17402
+ } finally {
17403
+ await this.stopInteractions();
17404
+ this.detach();
17405
+ this.releaseCapture?.();
17406
+ }
17407
+ return {
17408
+ recordingId: this.options.recordingId,
17409
+ videoPath,
17410
+ frameDirectory: join10(this.options.outputDirectory, "frames"),
17411
+ diagnosticsPath,
17412
+ durationSeconds,
17413
+ stoppedReason: this.interrupted ? "browser_closed" : reason,
17414
+ diagnostics: { consoleErrors: this.console.filter((event) => event.severity === "error").length, failedRequests: network.filter((event) => event.failureReason).length, httpErrors: network.filter((event) => (event.status ?? 0) >= 400).length, truncated: this.truncated || this.interrupted, bodyCaptureEnabled: false }
17415
+ };
17416
+ }
17417
+ };
17418
+
16848
17419
  // ../recorder/src/daemon.ts
16849
17420
  var sessions = /* @__PURE__ */ new Map();
16850
17421
  var lastActivity = Date.now();
17422
+ var browserConnection = new BrowserConnection({ statePath: join11(dirname2(daemonStatePath()), "chrome-connection.json"), extensionArchive: process.argv[1] ? join11(dirname2(process.argv[1]), "rill-chrome-extension.zip") : void 0 });
16851
17423
  async function runDaemon() {
16852
17424
  if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC.");
16853
17425
  const identity2 = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
16854
17426
  const socketPath = identity2?.pipe ?? daemonSocketPath();
16855
- await mkdir7(identity2 ? dirname(daemonStatePath()) : dirname(socketPath), { recursive: true, mode: 448 });
16856
- await mkdir7(recordingTempRoot(), { recursive: true, mode: 448 });
17427
+ await mkdir9(identity2 ? dirname2(daemonStatePath()) : dirname2(socketPath), { recursive: true, mode: 448 });
17428
+ await mkdir9(recordingTempRoot(), { recursive: true, mode: 448 });
16857
17429
  await restoreRecoverableSessions();
16858
17430
  await purgeExpiredCaptures();
17431
+ if (await stat4(join11(dirname2(daemonStatePath()), "chrome-connection.json")).catch(() => null)) await browserConnection.connect();
17432
+ browserConnection.on("disconnected", () => {
17433
+ for (const entry of sessions.values()) if (entry.state === "recording" && entry.session instanceof ExtensionRecordingSession) void finalizeEntry(entry, "browser_closed");
17434
+ });
16859
17435
  if (!identity2) await unlink(socketPath).catch((error51) => {
16860
17436
  if (error51.code !== "ENOENT") throw error51;
16861
17437
  });
16862
- const server = createServer2((request3, response) => {
17438
+ const server = createServer3((request3, response) => {
16863
17439
  if (identity2 && !acceptsWindowsIpcToken(identity2.token, request3.headers.authorization)) {
16864
17440
  request3.resume();
16865
17441
  json2(response, 401, { error: { code: "authentication_required", message: "Recorder authentication required." } });
@@ -16879,13 +17455,15 @@ async function runDaemon() {
16879
17455
  await purgeExpiredCaptures();
16880
17456
  const hasActiveSession = [...sessions.values()].some((entry) => entry.state === "recording" || entry.state === "finalizing");
16881
17457
  const hasRetainedCapture = await hasRetainedRemoteCaptures(recordingTempRoot());
16882
- if (!hasActiveSession && !hasRetainedCapture && Date.now() - lastActivity > 5 * 6e4) server.close(() => process.exit(0));
17458
+ if (!hasActiveSession) browserConnection.expireIdle();
17459
+ if (!hasActiveSession && !hasRetainedCapture && !browserConnection.keepsRecorderAlive && Date.now() - lastActivity > 5 * 6e4) server.close(() => process.exit(0));
16883
17460
  })().catch(() => void 0);
16884
17461
  }, 3e4);
16885
17462
  idleTimer.unref();
16886
17463
  const close = () => {
16887
17464
  clearInterval(idleTimer);
16888
17465
  server.close();
17466
+ void browserConnection.close();
16889
17467
  if (!identity2) void unlink(socketPath).catch(() => void 0);
16890
17468
  };
16891
17469
  process.once("SIGTERM", close);
@@ -16894,14 +17472,27 @@ async function runDaemon() {
16894
17472
  async function route(request3, response) {
16895
17473
  lastActivity = Date.now();
16896
17474
  const url2 = new URL(request3.url ?? "/", "http://daemon.local");
16897
- if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid, capabilities: ["tab-selection-v1"] });
17475
+ if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid, capabilities: ["tab-selection-v1", "chrome-connection-v1"] });
17476
+ if (request3.method === "POST" && url2.pathname === "/browser/connect") {
17477
+ try {
17478
+ return json2(response, 200, await browserConnection.connect());
17479
+ } catch {
17480
+ return json2(response, 500, { error: { code: "browser_unavailable", message: "Could not connect Chrome. Try again." } });
17481
+ }
17482
+ }
17483
+ if (request3.method === "GET" && url2.pathname === "/browser/status") return json2(response, 200, browserConnection.status());
17484
+ if (request3.method === "POST" && url2.pathname === "/browser/disconnect") {
17485
+ browserConnection.disconnect();
17486
+ return json2(response, 200, { connected: false });
17487
+ }
16898
17488
  if (request3.method === "POST" && url2.pathname === "/sessions") {
16899
17489
  if ([...sessions.values()].filter((entry) => entry.state === "recording").length >= 2) return json2(response, 429, { error: { code: "quota_exceeded", message: "This recorder already has two active sessions." } });
16900
17490
  try {
16901
17491
  const input = await readJson(request3);
16902
17492
  if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
16903
- const outputDirectory = join10(recordingTempRoot(), input.recordingId);
16904
- const session = await RecordingSession.start({ ...input, outputDirectory });
17493
+ const outputDirectory = join11(recordingTempRoot(), input.recordingId);
17494
+ if (input.browser === "existing" && [...sessions.values()].some((entry2) => entry2.state !== "ready" && entry2.state !== "failed" && entry2.session instanceof ExtensionRecordingSession)) throw new Error("This tab is already recording. Stop it before starting another take.");
17495
+ const session = input.browser === "existing" ? await ExtensionRecordingSession.start({ ...input, outputDirectory }, browserConnection) : await RecordingSession.start({ ...input, outputDirectory });
16905
17496
  const status = session.status();
16906
17497
  const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, tabId: status.tabId, startedAt: status.startedAt }, state: "recording" };
16907
17498
  sessions.set(input.recordingId, entry);
@@ -16951,7 +17542,7 @@ function serializeEntry(entry) {
16951
17542
  }
16952
17543
  async function restoreRecoverableSessions() {
16953
17544
  try {
16954
- const value = JSON.parse(await readFile7(daemonStatePath(), "utf8"));
17545
+ const value = JSON.parse(await readFile8(daemonStatePath(), "utf8"));
16955
17546
  for (const [id, entry] of Object.entries(value)) {
16956
17547
  if (entry.status !== "ready" && entry.status !== "failed") continue;
16957
17548
  sessions.set(id, { base: { recordingId: entry.recordingId, cdpUrl: entry.cdpUrl, tabId: entry.tabId, startedAt: entry.startedAt }, state: entry.status, result: entry.result, error: entry.error });
@@ -16962,7 +17553,7 @@ async function restoreRecoverableSessions() {
16962
17553
  async function persistState() {
16963
17554
  const state = Object.fromEntries([...sessions.entries()].map(([id, entry]) => [id, serializeEntry(entry)]));
16964
17555
  const temporary = daemonStatePath() + "." + crypto.randomUUID() + ".tmp";
16965
- await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
17556
+ await writeFile8(temporary, `${JSON.stringify(state, null, 2)}
16966
17557
  `, { mode: 384 });
16967
17558
  await rename2(temporary, daemonStatePath());
16968
17559
  }
@@ -17085,8 +17676,11 @@ function formatRecording(value, apiUrl, color = terminalColor()) {
17085
17676
  const duration3 = value.durationSeconds ?? value.duration_seconds ?? artifact.durationSeconds;
17086
17677
  if (typeof duration3 === "number" && Number.isFinite(duration3)) lines.push(`Duration: ${Math.floor(duration3 / 60)}m ${Math.floor(duration3 % 60)}s`);
17087
17678
  if (typeof value.remainingSeconds === "number") lines.push(`Time remaining: ${Math.max(0, Math.ceil(value.remainingSeconds))}s`);
17088
- if (typeof value.cdpUrl === "string") lines.push(`Browser connection: ${value.cdpUrl}`);
17089
- if (typeof value.tabId === "string") lines.push(`Recorded tab: ${value.tabId}`);
17679
+ if (value.browser === "existing") lines.push("Recording your selected Chrome tab.");
17680
+ else {
17681
+ if (typeof value.cdpUrl === "string" && value.cdpUrl) lines.push(`Browser connection: ${value.cdpUrl}`);
17682
+ if (typeof value.tabId === "string") lines.push(`Recorded tab: ${value.tabId}`);
17683
+ }
17090
17684
  if (typeof value.shareUrl === "string" && value.shareUrl) lines.push("", `Share: ${value.shareUrl}`);
17091
17685
  else if (status === "ready") lines.push("", "Ready. No share link returned; open this recording in your Rill account.");
17092
17686
  const message = value.message ?? value.error;
@@ -17450,7 +18044,7 @@ function controlPlaneUrl(environmentUrl) {
17450
18044
  async function removeLocalCapture(id) {
17451
18045
  await ensureDaemon(process.argv[1]);
17452
18046
  const local = await recordingStatus(id).catch(() => null);
17453
- if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
18047
+ if (local?.result?.frameDirectory) await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
17454
18048
  }
17455
18049
  function watchEncoding(apiUrl, id) {
17456
18050
  const child = spawn8(process.execPath, [process.argv[1], "--api-url", apiUrl, "encoding-watch", id], { detached: true, stdio: "ignore", env: { ...process.env }, windowsHide: true });
@@ -17460,6 +18054,32 @@ function watchEncoding(apiUrl, id) {
17460
18054
  var program = new Command();
17461
18055
  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);
17462
18056
  program.configureOutput({ outputError: () => void 0 }).exitOverride();
18057
+ var browserCommands = program.command("browser").description("Connect the Chrome tab you already use.");
18058
+ browserCommands.command("connect").action(async () => {
18059
+ await ensureDaemon(process.argv[1], false, true);
18060
+ const result = await connectBrowser();
18061
+ if (program.opts().json || program.opts().pretty) emit(result, program.opts().pretty);
18062
+ else process.stdout.write(result.connected ? `Chrome is connected. You can start another recording.
18063
+ ` : `Connect Chrome
18064
+
18065
+ Open this link in Chrome, then choose your app tab in Rill:
18066
+ ${result.connectionUrl}
18067
+ `);
18068
+ });
18069
+ browserCommands.command("status").action(async () => {
18070
+ await ensureDaemon(process.argv[1], false, true);
18071
+ const result = await browserConnectionStatus();
18072
+ if (program.opts().json || program.opts().pretty) emit(result, program.opts().pretty);
18073
+ else process.stdout.write(result.connected ? `Chrome is connected.
18074
+ Tab: ${result.tab?.title}
18075
+ ` : "Chrome is not connected. Run rill browser connect to get started.\n");
18076
+ });
18077
+ browserCommands.command("disconnect").action(async () => {
18078
+ await ensureDaemon(process.argv[1], false, true);
18079
+ const result = await disconnectBrowser();
18080
+ if (program.opts().json || program.opts().pretty) emit(result, program.opts().pretty);
18081
+ else process.stdout.write("Chrome disconnected. Your tab stays open.\n");
18082
+ });
17463
18083
  function humanOutput(identity2 = false) {
17464
18084
  const options = program.opts();
17465
18085
  return !options.json && !options.pretty && (identity2 || options.human);
@@ -17560,10 +18180,14 @@ record2.command("tabs").requiredOption("--cdp-url <url>", "Existing Chromium CDP
17560
18180
  const options = program.opts();
17561
18181
  emit({ schemaVersion: 1, tabs: await listBrowserTabs(commandOptions.cdpUrl) }, options.pretty);
17562
18182
  });
17563
- record2.command("start").option("--encoding <mode>", "remote or local", "remote").option("--url <url>").option("--title <title>").option("--description <description>", "").option("--headed").option("--cdp-url <url>").option("--tab-id <id>", "Record this existing tab from record tabs").option("--width <pixels>", "", "1280").option("--height <pixels>", "", "720").option("--max-duration <seconds>", "", "600").action(async (commandOptions) => {
18183
+ record2.command("start").option("--browser <choice>", "existing or new", "new").option("--operator <who>", "human or agent", "human").option("--controller-target <id>", "Target selected by your agent controller").option("--encoding <mode>", "remote or local", "remote").option("--url <url>").option("--title <title>").option("--description <description>", "").option("--headed").option("--cdp-url <url>").option("--tab-id <id>", "Record this existing tab from record tabs").option("--width <pixels>", "", "1280").option("--height <pixels>", "", "720").option("--max-duration <seconds>", "", "600").action(async (commandOptions) => {
17564
18184
  const options = program.opts();
17565
18185
  assertRecorderTransport();
17566
18186
  validateTabSelection(commandOptions);
18187
+ const connectedBrowser = await prepareBrowserRecording(commandOptions, async () => {
18188
+ await ensureDaemon(process.argv[1], false, true);
18189
+ return browserConnectionStatus();
18190
+ });
17567
18191
  await ensureAnonymousCredential(options.apiUrl);
17568
18192
  const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
17569
18193
  const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
@@ -17572,7 +18196,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
17572
18196
  if (!Number.isFinite(Number(commandOptions.maxDuration)) || Number(commandOptions.maxDuration) < 1 || Number(commandOptions.maxDuration) > 600) throw new Error("Recording duration must be between 1 and 600 seconds.");
17573
18197
  const width = Number(commandOptions.width);
17574
18198
  const height = Number(commandOptions.height);
17575
- const metadata = { requestId: crypto.randomUUID(), title: commandOptions.title ?? `Browser recording \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}`, description: commandOptions.description, sourceUrl: commandOptions.url ?? null, width, height };
18199
+ const metadata = { requestId: crypto.randomUUID(), title: commandOptions.title ?? `Browser recording \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}`, description: commandOptions.description, sourceUrl: commandOptions.url ?? connectedBrowser?.tab?.url ?? null, width, height, ...connectedBrowser ? { recordingSource: "chrome_cli" } : {} };
17576
18200
  const remote = await createRecordingWithRecovery({
17577
18201
  createRemote: () => client.createRecording(metadata),
17578
18202
  localStatus: recordingStatus,
@@ -17585,8 +18209,8 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
17585
18209
  rememberRecording(remote.recordingId);
17586
18210
  const local = await startRecordingWithCompensation(remote, {
17587
18211
  startLocal: async () => {
17588
- await ensureDaemon(process.argv[1], Boolean(commandOptions.cdpUrl));
17589
- return startRecording({ encoding: commandOptions.encoding, recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, tabId: commandOptions.tabId, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600), captureNetworkBodies: remote.bodyCaptureEnabled, cursorAppearance: remote.cursorAppearance });
18212
+ await ensureDaemon(process.argv[1], Boolean(commandOptions.cdpUrl), Boolean(connectedBrowser));
18213
+ return startRecording({ ...connectedBrowser ? { browser: "existing", controllerTarget: commandOptions.controllerTarget } : {}, encoding: commandOptions.encoding, recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, tabId: commandOptions.tabId, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600), captureNetworkBodies: remote.bodyCaptureEnabled, cursorAppearance: remote.cursorAppearance });
17590
18214
  },
17591
18215
  cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
17592
18216
  }, options.apiUrl);
@@ -17595,9 +18219,9 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
17595
18219
  os: process.platform,
17596
18220
  arch: process.arch,
17597
18221
  headed: Boolean(commandOptions.headed),
17598
- attached: Boolean(commandOptions.cdpUrl)
18222
+ attached: Boolean(commandOptions.cdpUrl || connectedBrowser)
17599
18223
  }).catch(() => void 0);
17600
- return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, ...local.tabId ? { tabId: local.tabId } : {}, browserAttachment: browserAttachment(local.cdpUrl, local.tabId), startedAt: local.startedAt, stopsAt: local.stopsAt, elapsedSeconds: local.elapsedSeconds, remainingSeconds: local.remainingSeconds, bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
18224
+ return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, ...connectedBrowser ? { browser: "existing", browserAttachment: { protocol: "extension", tab: connectedBrowser.tab, captureOnly: true, controllerVerified: false, instructions: "Use your existing controller on this exact target, or perform the steps yourself. Rill records the selected tab and does not provide browser control. Popups and other tabs are outside this recording." } } : { cdpUrl: local.cdpUrl, browserAttachment: browserAttachment(local.cdpUrl, local.tabId) }, ...local.tabId ? { tabId: local.tabId } : {}, startedAt: local.startedAt, stopsAt: local.stopsAt, elapsedSeconds: local.elapsedSeconds, remainingSeconds: local.remainingSeconds, bodyCaptureEnabled: connectedBrowser ? false : remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
17601
18225
  });
17602
18226
  emitRecording(result, options.pretty);
17603
18227
  });
@@ -17622,7 +18246,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
17622
18246
  const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
17623
18247
  const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
17624
18248
  emitRecording({ schemaVersion: 1, ...result2, recordingId, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
17625
- if (result2.status === "ready") await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
18249
+ if (result2.status === "ready") await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
17626
18250
  else watchEncoding(options.apiUrl, recordingId);
17627
18251
  return;
17628
18252
  }
@@ -17644,7 +18268,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
17644
18268
  ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
17645
18269
  };
17646
18270
  emitRecording(result, options.pretty);
17647
- if (remote) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
18271
+ if (remote) await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
17648
18272
  });
17649
18273
  record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
17650
18274
  const options = program.opts();
@@ -17676,7 +18300,7 @@ program.command("encoding-watch", { hidden: true }).argument("<recording-id>").a
17676
18300
  if (result.status === "ready") {
17677
18301
  await ensureDaemon(process.argv[1]);
17678
18302
  const local = await recordingStatus(id).catch(() => null);
17679
- if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
18303
+ if (local?.result?.frameDirectory) await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
17680
18304
  }
17681
18305
  });
17682
18306
  var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
@@ -17760,17 +18384,17 @@ comments.command("edit").argument("<share-url>").requiredOption("--comment <id>"
17760
18384
  var drafts = program.command("drafts");
17761
18385
  drafts.command("list").action(async () => {
17762
18386
  const options = program.opts();
17763
- const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
18387
+ const root = process.env.RILL_TEMP ?? join12(tmpdir3(), "rill-recordings");
17764
18388
  const names = await readdir2(root).catch(() => []);
17765
18389
  emit({ schemaVersion: 1, drafts: names }, options.pretty);
17766
18390
  });
17767
18391
  drafts.command("purge").action(async () => {
17768
18392
  const options = program.opts();
17769
- const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
18393
+ const root = process.env.RILL_TEMP ?? join12(tmpdir3(), "rill-recordings");
17770
18394
  const names = await readdir2(root).catch(() => []);
17771
18395
  let purged = 0;
17772
18396
  for (const name of names) {
17773
- const path = join11(root, name);
18397
+ const path = join12(root, name);
17774
18398
  const metadata = await stat5(path);
17775
18399
  if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
17776
18400
  await rm7(path, { recursive: true, force: true });