@runalabs/rill-cli 0.1.20 → 0.1.21

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/README.md CHANGED
@@ -87,18 +87,33 @@ The default API is `https://userill.dev`. `RILL_API_URL` changes it; the global
87
87
  ## First recording
88
88
 
89
89
  ```sh
90
- rill record start --url https://example.com --title "Checkout verification"
91
- # Save recordingId and connect your browser tool to the returned cdpUrl.
92
- # Perform the flow in the existing recorded page.
90
+ rill browser connect
91
+ # Open the connection link in Chrome, then choose your app tab in Rill.
92
+ rill --human record start --title "Checkout verification"
93
+ # Perform the steps yourself in that tab; see the agent-operated example below.
93
94
  rill record stop <recording-id>
94
95
  rill inspect https://userill.dev/s/<share-secret>
95
96
  ```
96
97
 
97
- Start returns `recordingId`, `cdpUrl`, `status`, `bodyCaptureEnabled`, and `maxDurationSeconds`. Stop finalizes capture, uploads frames and diagnostics for remote encoding, and waits for playback. Confirm `status: "ready"` and a non-null `shareUrl` before returning the evidence link. Local capture finishing does not establish remote readiness.
98
+ Start returns `recordingId`, `browserAttachment`, `status`, `bodyCaptureEnabled`, and `maxDurationSeconds`. A managed or direct CDP recording also returns `cdpUrl`. Stop finalizes capture, uploads frames and diagnostics for remote encoding, and waits for playback. Confirm `status: "ready"` and a non-null `shareUrl` before returning the evidence link. Local capture finishing does not establish remote readiness.
98
99
 
99
100
  New browser recordings include a visible cursor and click pulses by default. In Rill Settings → Cursor appearance, workspace members can turn them off or choose an accent color. The CLI reads this preference when starting a recording; it is baked into the video and does not change existing recordings. Use an updated CLI and recorder daemon to capture the overlay.
100
101
 
101
- ### Connect Playwright
102
+ ### Record the browser you already use
103
+
104
+ This branch includes a private Chrome pilot; build the CLI from this checkout. The CLI packages its matching extension download. Install it once, run `rill browser connect`, open the returned link in Chrome and click **Connect Chrome**. Rill opens beside your page. Switch to your website and click **Connect this tab**. HTTP and HTTPS sites are supported, including localhost. Setup is remembered and multiple takes reuse the approved tab.
105
+
106
+ `record start` keeps its normal managed-browser behavior and does not require an extension. Choose `--browser existing` to record the tab connected through Rill for Chrome; that explicit mode fails with connection guidance when no tab is selected. Capture does not click, type, evaluate scripts, intercept navigation, or change the viewport. Sign-in state stays in Chrome; site credentials are never extracted or exported. Request bodies and headers are not captured. Console text is bounded and heuristically redacted; visible page content remains in the video.
107
+
108
+ For an agent-operated recording, first establish that its existing controller is on the selected target. Inspect `rill --json browser status` and match the returned Chrome tab/target ID with the controller's own selected tab. Then use `rill record start --browser existing --operator agent --controller-target <target-id>`. This matches target identity; it does not certify that an arbitrary agent can drive the browser. If the host cannot expose that identity or control the tab, explain the limitation and offer human operation instead. Rill supplies capture rather than a second controller.
109
+
110
+ Capture starts only after a frame has been saved. Stopping leaves the tab connected for the next take. A lost connection saves the captured part and marks interruption; upload retry reuses those bytes. New tabs/popups are outside capture and produce a coverage note. DevTools or tab closure can interrupt capture. Inactivity disconnects tab access after an hour; device setup remains saved. Use `rill browser disconnect` to end access. Chrome can still require its own permissions.
111
+
112
+ Choose a fresh browser explicitly with `rill record start --browser new --url <url>`. Local encoding is available on that managed/CDP path; the Chrome pilot uses remote encoding. Existing CLI limits, workspace access, recording lifecycle, recovery, and player still apply. Native extension/controller coexistence and provider playback must be verified separately from protocol tests.
113
+
114
+ ### Connect Playwright to an explicitly created browser
115
+
116
+ Use `rill record start --browser new --url <url>` only when the task calls for a fresh browser and your controller supports its connection.
102
117
 
103
118
  Start also returns `browserAttachment` with the CDP endpoint, protocol, Playwright method, and attachment instructions. Browser tools must explicitly support attaching to that endpoint; Rill does not automatically expose its browser to every tool.
104
119
 
@@ -254,7 +269,7 @@ Explicit `--json` or `--pretty` takes precedence over `--human`. Use `--pretty`
254
269
  rill whoami
255
270
  rill whoami --json
256
271
  rill --human doctor
257
- rill --human record start --url https://example.com --title "Smoke test" --headed
272
+ rill --human record start --browser new --url https://example.com --title "Smoke test" --headed
258
273
  rill --human record stop <recording-id>
259
274
  rill --human record status <recording-id>
260
275
  ```
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,9 +15605,9 @@ 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";
@@ -16394,18 +16425,34 @@ async function encodeFrames(directory, manifest, output, signal) {
16394
16425
  return withEncodingSlot(async () => {
16395
16426
  signal?.throwIfAborted();
16396
16427
  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)}`);
16428
+ if (output.endsWith(".mp4")) {
16429
+ for (let i = 0; i < manifest.frames.length; i++) {
16430
+ const next = manifest.frames[i + 1];
16431
+ const duration3 = next ? next.timestamp - manifest.frames[i].timestamp : manifest.durationSeconds - (manifest.frames[i].timestamp - manifest.frames[0].timestamp);
16432
+ lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
16433
+ }
16434
+ lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
16435
+ } else {
16436
+ const endTick = Math.max(0, Math.ceil(manifest.durationSeconds * 25) - 2);
16437
+ const frames = [];
16438
+ manifest.frames.forEach((frame, index) => {
16439
+ const tick = Math.min(endTick, Math.max(0, Math.round((frame.timestamp - manifest.frames[0].timestamp) * 25)));
16440
+ if (frames.at(-1)?.tick === tick) frames[frames.length - 1] = { index, tick };
16441
+ else frames.push({ index, tick });
16442
+ });
16443
+ if (frames.at(-1).tick < endTick) frames.push({ ...frames.at(-1), tick: endTick });
16444
+ frames.forEach((frame, index) => {
16445
+ lines.push(`file 'frame-${String(frame.index).padStart(6, "0")}.jpg'`);
16446
+ if (frames[index + 1]) lines.push(`duration ${((frames[index + 1].tick - frame.tick) / 25).toFixed(6)}`);
16447
+ });
16401
16448
  }
16402
- lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
16403
16449
  await writeFile3(join7(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
16404
16450
  const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
16405
16451
  if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
16406
16452
  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;
16453
+ const width = Math.ceil(manifest.width / 2) * 2, height = Math.ceil(manifest.height / 2) * 2;
16454
+ 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`;
16455
+ const filters = output.endsWith(".mp4") ? `${dimensions},fps=30,tpad=stop_mode=clone:stop_duration=1` : `${dimensions},fps=25`;
16409
16456
  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
16457
  await new Promise((resolve, reject) => {
16411
16458
  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 +16504,18 @@ var ScreencastRecorder = class {
16457
16504
  frameCounter = 0;
16458
16505
  frameBytes = 0;
16459
16506
  captureError;
16507
+ frameListener;
16508
+ async waitForFirstFrame(timeoutMs = 8e3) {
16509
+ const deadline = Date.now() + timeoutMs;
16510
+ while (!this.frames.length) {
16511
+ if (this.captureError) throw this.captureError;
16512
+ if (Date.now() >= deadline) throw new Error("browser_unavailable: No picture received. Keep the selected tab open and try again.");
16513
+ await new Promise((resolve) => setTimeout(resolve, 25));
16514
+ }
16515
+ }
16460
16516
  async start() {
16461
16517
  await mkdir5(this.directory, { recursive: true, mode: 448 });
16462
- this.session.on("Page.screencastFrame", (payload) => {
16518
+ this.frameListener = (payload) => {
16463
16519
  const frameEpochMs = (payload.metadata.timestamp ?? Date.now() / 1e3) * 1e3;
16464
16520
  this.firstFrameEpochMs = Math.min(this.firstFrameEpochMs ?? frameEpochMs, frameEpochMs);
16465
16521
  const bytes = Buffer.from(payload.data, "base64");
@@ -16480,14 +16536,17 @@ var ScreencastRecorder = class {
16480
16536
  void this.session.send("Page.screencastFrameAck", { sessionId: payload.sessionId }).catch(() => void 0);
16481
16537
  });
16482
16538
  this.pendingWrites.add(write);
16483
- });
16539
+ };
16540
+ this.session.on("Page.screencastFrame", this.frameListener);
16484
16541
  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 });
16542
+ const options = { format: "jpeg", quality: 82, maxWidth: this.width, maxHeight: this.height, everyNthFrame: 1, sendLastFrame: true };
16543
+ await this.session.send("Page.startScreencast", options);
16486
16544
  this.started = true;
16487
16545
  }
16488
16546
  async stop(outputPath, expectedDurationSeconds, captureOnly = false, signal) {
16489
16547
  if (this.started) await this.session.send("Page.stopScreencast").catch(() => void 0);
16490
16548
  await new Promise((resolve) => setTimeout(resolve, 120));
16549
+ if (this.frameListener) this.session.off("Page.screencastFrame", this.frameListener);
16491
16550
  await Promise.all(this.pendingWrites);
16492
16551
  if (this.captureError) throw this.captureError;
16493
16552
  if (!this.frames.length) throw new Error("No video frames were captured.");
@@ -16593,6 +16652,7 @@ var RecordingSession = class _RecordingSession {
16593
16652
  await options.beforeCapture?.(runtime.page);
16594
16653
  session.startedAt = /* @__PURE__ */ new Date();
16595
16654
  await screencast.start();
16655
+ await screencast.waitForFirstFrame();
16596
16656
  if (options.url) await session.page.goto(options.url, { waitUntil: "domcontentloaded" });
16597
16657
  } catch (error51) {
16598
16658
  await session.stop("failed").catch(() => void 0);
@@ -16845,21 +16905,412 @@ async function waitForCdp(cdpUrl, browserProcess) {
16845
16905
  throw new Error("Timed out waiting for Chrome DevTools Protocol.");
16846
16906
  }
16847
16907
 
16908
+ // ../chrome-extension/brand.ts
16909
+ var rillLogo = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSI4IiBmaWxsPSIjZjdmNmYxIiAvPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE2IDE2KSBzY2FsZSguOSkgdHJhbnNsYXRlKC0xNiAtMTYpIj48ZyB0cmFuc2Zvcm09InJvdGF0ZSgtMTAgMTYgMTYpIj48cmVjdCB4PSI2IiB5PSIxMSIgd2lkdGg9IjgiIGhlaWdodD0iMTciIHJ4PSI0IiBmaWxsPSIjMTcxNzE3IiAvPjxyZWN0IHg9IjE4LjQiIHk9IjUuNCIgd2lkdGg9IjgiIGhlaWdodD0iMTciIHJ4PSI0IiBmaWxsPSIjMTcxNzE3IiAvPjxyZWN0IHg9IjE3IiB5PSI0IiB3aWR0aD0iOCIgaGVpZ2h0PSIxNyIgcng9IjQiIGZpbGw9IiM3MTQ2ZmYiIHN0cm9rZT0iIzE3MTcxNyIgc3Ryb2tlLXdpZHRoPSIxLjgiIC8+PC9nPjwvZz4KPC9zdmc+Cg==";
16910
+ var rillFont = "data:font/woff2;base64,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";
16911
+
16912
+ // ../chrome-extension/connection-page.ts
16913
+ function renderChromeConnectionPage(downloadUrl, storeUrl, content) {
16914
+ const hosted = downloadUrl !== "/extension.zip";
16915
+ const installUrl = storeUrl && /^https:\/\/chromewebstore\.google\.com\/detail\/[a-z0-9-]+\/[a-p]{32}$/.test(storeUrl) ? storeUrl : downloadUrl;
16916
+ 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>
16917
+ @font-face{font-family:Geist;src:url('${rillFont}') format('woff2');font-weight:100 900;font-display:swap}:root{font-family:Geist,system-ui,sans-serif;-webkit-font-smoothing:antialiased;color:#171717;background:#f7f6f1;--purple:#7146ff;--muted:#6d6a66}*{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 #dedbd4}.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:#fff;padding:32px;border-radius:28px;box-shadow:0 1px 3px #00000008,0 12px 40px #00000006}.step{font-size:12px;color:#4c25cf;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:#4c25cf}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}}
16918
+ </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>`;
16919
+ return content?.body ? page.replace(/<main>[\s\S]*<\/main>/, () => `<main>${content.body}</main>`) : page;
16920
+ }
16921
+
16922
+ // ../recorder/src/browser-connection.ts
16923
+ import { EventEmitter } from "events";
16924
+ import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "crypto";
16925
+ import { createServer as createServer2 } from "http";
16926
+ import { WebSocketServer, WebSocket } from "ws";
16927
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir7 } from "fs/promises";
16928
+ import { dirname } from "path";
16929
+ var allowedMethods = /* @__PURE__ */ new Set(["Page.enable", "Page.getLayoutMetrics", "Page.startScreencast", "Page.stopScreencast", "Page.screencastFrameAck", "Runtime.enable", "Network.enable"]);
16930
+ var allowedEvents = /* @__PURE__ */ new Set(["Page.screencastFrame", "Page.frameNavigated", "Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Network.requestWillBeSent", "Network.responseReceived", "Network.loadingFailed", "Network.loadingFinished", "Rill.coverage"]);
16931
+ var BrowserConnection = class extends EventEmitter {
16932
+ constructor(options = {}) {
16933
+ super();
16934
+ this.options = options;
16935
+ }
16936
+ options;
16937
+ server;
16938
+ websocketServer;
16939
+ socket;
16940
+ token = randomBytes3(32).toString("hex");
16941
+ pending = /* @__PURE__ */ new Map();
16942
+ sequence = 0;
16943
+ address = "";
16944
+ tab;
16945
+ starting;
16946
+ lastRecordingActivity = Date.now();
16947
+ captureInUse = false;
16948
+ status() {
16949
+ return { connected: Boolean(this.tab && this.socket?.readyState === WebSocket.OPEN), tab: this.tab, captureOnly: true };
16950
+ }
16951
+ get connected() {
16952
+ return this.status().connected;
16953
+ }
16954
+ get keepsRecorderAlive() {
16955
+ return Boolean(this.server?.listening && Date.now() - this.lastRecordingActivity < 60 * 6e4);
16956
+ }
16957
+ claimCapture() {
16958
+ if (this.captureInUse) throw new Error("This tab is already recording. Stop it before starting another take.");
16959
+ this.captureInUse = true;
16960
+ return () => {
16961
+ this.captureInUse = false;
16962
+ };
16963
+ }
16964
+ async connect() {
16965
+ if (!this.server || this.starting) {
16966
+ this.starting ??= this.listen().catch((error51) => {
16967
+ this.server = void 0;
16968
+ throw error51;
16969
+ }).finally(() => {
16970
+ this.starting = void 0;
16971
+ });
16972
+ await this.starting;
16973
+ }
16974
+ return { ...this.status(), connectionUrl: `${this.address}/rill-connect#${this.token}` };
16975
+ }
16976
+ async listen() {
16977
+ let port = 0;
16978
+ if (this.options.statePath) {
16979
+ try {
16980
+ const saved = JSON.parse(await readFile7(this.options.statePath, "utf8"));
16981
+ if (/^[a-f0-9]{64}$/.test(saved.token) && Number.isInteger(saved.port) && saved.port > 0 && saved.port < 65536) {
16982
+ this.token = saved.token;
16983
+ port = saved.port;
16984
+ }
16985
+ } catch {
16986
+ }
16987
+ }
16988
+ this.server = createServer2((request3, response) => {
16989
+ if (this.address && request3.headers.host === new URL(this.address).host && request3.method === "GET" && request3.url === "/extension.zip" && this.options.extensionArchive) {
16990
+ void readFile7(this.options.extensionArchive).then((bytes) => {
16991
+ response.writeHead(200, { "Content-Type": "application/zip", "Content-Disposition": 'attachment; filename="rill-chrome-extension.zip"', "Cache-Control": "no-store" });
16992
+ response.end(bytes);
16993
+ }, () => {
16994
+ response.writeHead(404);
16995
+ response.end("The download is not available. Ask your agent to rebuild this Rill preview.");
16996
+ });
16997
+ return;
16998
+ }
16999
+ if (!this.address || request3.headers.host !== new URL(this.address).host || request3.url !== "/rill-connect" || request3.method !== "GET") {
17000
+ response.writeHead(404);
17001
+ response.end();
17002
+ return;
17003
+ }
17004
+ 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" });
17005
+ response.end(connectionPage);
17006
+ });
17007
+ this.websocketServer = new WebSocketServer({ noServer: true, maxPayload: 6 * 1024 * 1024, perMessageDeflate: false });
17008
+ this.server.on("upgrade", (request3, socket, head) => {
17009
+ const supplied = request3.headers["sec-websocket-protocol"]?.split(",").map((value) => value.trim());
17010
+ const candidate = supplied?.[1] ?? "";
17011
+ const authorized = /^[a-f0-9]{64}$/.test(candidate) && timingSafeEqual2(Buffer.from(candidate), Buffer.from(this.token));
17012
+ 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) {
17013
+ socket.end("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
17014
+ return;
17015
+ }
17016
+ this.websocketServer.handleUpgrade(request3, socket, head, (peer) => this.attach(peer));
17017
+ });
17018
+ await new Promise((resolve, reject) => {
17019
+ this.server.once("error", reject);
17020
+ this.server.listen(port, "127.0.0.1", resolve);
17021
+ });
17022
+ const address = this.server.address();
17023
+ if (!address || typeof address === "string") throw new Error("Could not connect Chrome. Try again.");
17024
+ this.address = `http://127.0.0.1:${address.port}`;
17025
+ if (this.options.statePath) {
17026
+ await mkdir7(dirname(this.options.statePath), { recursive: true, mode: 448 });
17027
+ await writeFile6(this.options.statePath, JSON.stringify({ token: this.token, port: address.port }), { mode: 384 });
17028
+ }
17029
+ }
17030
+ attach(peer) {
17031
+ this.socket = peer;
17032
+ const helloTimer = setTimeout(() => {
17033
+ if (!this.tab) peer.close(1008, "Choose a tab");
17034
+ }, 1e4);
17035
+ peer.on("error", () => peer.close());
17036
+ peer.on("message", (bytes) => {
17037
+ try {
17038
+ const message = JSON.parse(bytes.toString());
17039
+ if (message.type === "ping") {
17040
+ peer.send('{"type":"pong"}');
17041
+ return;
17042
+ }
17043
+ if (message.type === "tab") {
17044
+ 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");
17045
+ const next = { tabId: String(message.tabId), targetId: message.targetId, title: redactDiagnosticText(String(message.title ?? "").replace(/[\u0000-\u001f\u007f]/g, ""), 200), url: sanitizeUrl(message.url) };
17046
+ if (this.tab && this.tab.targetId !== next.targetId) throw new Error("Disconnect before choosing another tab");
17047
+ this.tab = next;
17048
+ clearTimeout(helloTimer);
17049
+ this.emit("connected", this.status());
17050
+ return;
17051
+ }
17052
+ if (message.type === "event" && this.tab && allowedEvents.has(message.method)) {
17053
+ this.emit(message.method, message.params);
17054
+ return;
17055
+ }
17056
+ if (message.type === "reply") {
17057
+ const pending2 = this.pending.get(message.id);
17058
+ if (!pending2) return;
17059
+ this.pending.delete(message.id);
17060
+ clearTimeout(pending2.timer);
17061
+ if (message.error) pending2.reject(new Error("Chrome could not finish recording. Keep the selected tab open and try again."));
17062
+ else pending2.resolve(message.result);
17063
+ }
17064
+ } catch {
17065
+ peer.close(1008, "Could not connect");
17066
+ }
17067
+ });
17068
+ peer.on("close", () => {
17069
+ clearTimeout(helloTimer);
17070
+ if (this.socket !== peer) return;
17071
+ this.socket = void 0;
17072
+ this.tab = void 0;
17073
+ for (const pending2 of this.pending.values()) {
17074
+ clearTimeout(pending2.timer);
17075
+ pending2.reject(new Error("browser_unavailable: Chrome disconnected. Your captured recording is being saved."));
17076
+ }
17077
+ this.pending.clear();
17078
+ this.emit("disconnected");
17079
+ });
17080
+ }
17081
+ send(method, params = {}) {
17082
+ if (method === "Page.startScreencast" || method === "Page.stopScreencast") this.lastRecordingActivity = Date.now();
17083
+ if (!allowedMethods.has(method)) return Promise.reject(new Error("This connection only records the selected tab."));
17084
+ if (!this.connected) return Promise.reject(new Error("browser_unavailable: Choose a tab in Rill for Chrome first."));
17085
+ const id = ++this.sequence;
17086
+ return new Promise((resolve, reject) => {
17087
+ const timer = setTimeout(() => {
17088
+ this.pending.delete(id);
17089
+ reject(new Error("browser_unavailable: Chrome took too long to respond. Try reconnecting your tab."));
17090
+ }, 1e4);
17091
+ this.pending.set(id, { resolve, reject, timer });
17092
+ this.socket.send(JSON.stringify({ id, method, params }), (error51) => {
17093
+ if (error51) {
17094
+ clearTimeout(timer);
17095
+ this.pending.delete(id);
17096
+ reject(error51);
17097
+ }
17098
+ });
17099
+ });
17100
+ }
17101
+ disconnect() {
17102
+ this.socket?.close(1e3, "Disconnected");
17103
+ }
17104
+ expireIdle() {
17105
+ if (Date.now() - this.lastRecordingActivity > 60 * 6e4) this.disconnect();
17106
+ }
17107
+ async close() {
17108
+ this.socket?.terminate();
17109
+ await new Promise((resolve) => this.websocketServer ? this.websocketServer.close(() => resolve()) : resolve());
17110
+ await new Promise((resolve) => this.server ? this.server.close(() => resolve()) : resolve());
17111
+ }
17112
+ };
17113
+ var connectionPage = renderChromeConnectionPage("/extension.zip", "");
17114
+
17115
+ // ../recorder/src/extension-session.ts
17116
+ import { mkdir as mkdir8, writeFile as writeFile7 } from "fs/promises";
17117
+ import { join as join10 } from "path";
17118
+ var ExtensionRecordingSession = class _ExtensionRecordingSession {
17119
+ constructor(options, connection) {
17120
+ this.options = options;
17121
+ this.connection = connection;
17122
+ this.targetId = connection.status().tab.targetId;
17123
+ }
17124
+ options;
17125
+ connection;
17126
+ startedAt = /* @__PURE__ */ new Date();
17127
+ screencast;
17128
+ console = [];
17129
+ network = /* @__PURE__ */ new Map();
17130
+ timeline = [];
17131
+ listeners = [];
17132
+ interrupted = false;
17133
+ truncated = false;
17134
+ stopPromise;
17135
+ targetId;
17136
+ releaseCapture;
17137
+ static async start(options, connection) {
17138
+ if (!connection.connected) throw new Error("browser_unavailable: Open Rill in Chrome and choose a tab to record.");
17139
+ 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.");
17140
+ if (options.encoding !== "remote") throw new Error("validation_failed: Connected Chrome currently uses the standard recording upload.");
17141
+ 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.");
17142
+ const session = new _ExtensionRecordingSession(options, connection);
17143
+ session.releaseCapture = connection.claimCapture();
17144
+ session.screencast = new ScreencastRecorder(connection, join10(options.outputDirectory, "frames"), options.width ?? 1280, options.height ?? 720);
17145
+ try {
17146
+ await mkdir8(options.outputDirectory, { recursive: true, mode: 448 });
17147
+ await writeFile7(join10(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
17148
+ session.attachDiagnostics();
17149
+ await connection.send("Runtime.enable");
17150
+ await connection.send("Network.enable");
17151
+ await session.screencast.start();
17152
+ await session.screencast.waitForFirstFrame();
17153
+ return session;
17154
+ } catch (error51) {
17155
+ await session.screencast.stop(join10(options.outputDirectory, "recording.webm"), 0.1, true).catch(() => void 0);
17156
+ session.detach();
17157
+ session.releaseCapture?.();
17158
+ throw error51;
17159
+ }
17160
+ }
17161
+ status() {
17162
+ const elapsedSeconds = Math.max(0, (Date.now() - this.startedAt.getTime()) / 1e3);
17163
+ const maxDurationSeconds = this.options.maxDurationSeconds ?? 600;
17164
+ return {
17165
+ recordingId: this.options.recordingId,
17166
+ cdpUrl: "",
17167
+ tabId: this.targetId,
17168
+ browser: "existing",
17169
+ startedAt: this.startedAt.toISOString(),
17170
+ elapsedSeconds,
17171
+ maxDurationSeconds,
17172
+ remainingSeconds: this.stopPromise ? 0 : Math.max(0, maxDurationSeconds - elapsedSeconds),
17173
+ stopsAt: new Date(this.startedAt.getTime() + maxDurationSeconds * 1e3).toISOString(),
17174
+ status: this.stopPromise ? "finalizing" : "recording",
17175
+ interrupted: this.interrupted,
17176
+ captureOnly: true,
17177
+ controllerVerified: false,
17178
+ ...this.interrupted ? { message: "Chrome disconnected. The captured part of your recording is being saved." } : {}
17179
+ };
17180
+ }
17181
+ stop(reason = "agent_request") {
17182
+ return this.stopPromise ??= this.finalize(reason);
17183
+ }
17184
+ on(method, listener) {
17185
+ this.connection.on(method, listener);
17186
+ this.listeners.push([method, listener]);
17187
+ }
17188
+ detach() {
17189
+ for (const [method, listener] of this.listeners) this.connection.off(method, listener);
17190
+ this.listeners = [];
17191
+ }
17192
+ offset() {
17193
+ return Math.max(0, Date.now() - this.startedAt.getTime());
17194
+ }
17195
+ event(type, label) {
17196
+ if (this.timeline.length >= 1e4) {
17197
+ this.truncated = true;
17198
+ return;
17199
+ }
17200
+ this.timeline.push({ id: `time_${this.timeline.length + 1}`, offsetMs: this.offset(), type, label });
17201
+ }
17202
+ attachDiagnostics() {
17203
+ this.on("Rill.coverage", () => {
17204
+ this.truncated = true;
17205
+ this.event("lifecycle", "Another tab opened. It is outside this recording.");
17206
+ });
17207
+ this.on("disconnected", () => {
17208
+ this.interrupted = true;
17209
+ this.event("lifecycle", "Chrome disconnected. Recording stopped.");
17210
+ });
17211
+ this.on("Page.frameNavigated", (value) => {
17212
+ if (!value.frame?.parentId) this.event("navigation", sanitizeUrl(String(value.frame?.url ?? "")));
17213
+ });
17214
+ this.on("Runtime.consoleAPICalled", (value) => {
17215
+ if (this.console.length >= 1e4) {
17216
+ this.truncated = true;
17217
+ return;
17218
+ }
17219
+ const severity = value.type === "error" || value.type === "assert" ? "error" : value.type === "warning" || value.type === "warn" ? "warning" : "info";
17220
+ 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);
17221
+ this.console.push({ id: `con_${this.console.length + 1}`, offsetMs: this.offset(), severity, text, stack: [] });
17222
+ if (severity !== "info") this.event("console", text.slice(0, 160));
17223
+ });
17224
+ this.on("Runtime.exceptionThrown", (value) => {
17225
+ if (this.console.length >= 1e4) {
17226
+ this.truncated = true;
17227
+ return;
17228
+ }
17229
+ const text = redactDiagnosticText(String(value.exceptionDetails?.exception?.description ?? value.exceptionDetails?.text ?? "Page error"), 2e3);
17230
+ this.console.push({ id: `con_${this.console.length + 1}`, offsetMs: this.offset(), severity: "error", text, stack: [] });
17231
+ this.event("exception", text.slice(0, 160));
17232
+ });
17233
+ this.on("Network.requestWillBeSent", (value) => {
17234
+ if (this.network.size >= 5e3) {
17235
+ this.truncated = true;
17236
+ return;
17237
+ }
17238
+ 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").slice(0, 40) });
17239
+ });
17240
+ this.on("Network.responseReceived", (value) => {
17241
+ const event = this.network.get(value.requestId);
17242
+ if (event && Number.isInteger(value.response?.status)) event.status = value.response.status;
17243
+ });
17244
+ this.on("Network.loadingFinished", (value) => {
17245
+ const event = this.network.get(value.requestId);
17246
+ if (event) {
17247
+ event.completedOffsetMs = this.offset();
17248
+ event.durationMs = Math.max(0, event.completedOffsetMs - event.offsetMs);
17249
+ }
17250
+ });
17251
+ this.on("Network.loadingFailed", (value) => {
17252
+ const event = this.network.get(value.requestId);
17253
+ if (event) event.failureReason = redactDiagnosticText(String(value.errorText ?? "Request failed"), 200);
17254
+ });
17255
+ }
17256
+ async finalize(reason) {
17257
+ const durationSeconds = Math.min(this.options.maxDurationSeconds ?? 600, Math.max(0.1, this.offset() / 1e3));
17258
+ const videoPath = join10(this.options.outputDirectory, "recording.webm");
17259
+ const diagnosticsPath = join10(this.options.outputDirectory, "diagnostics-v1.ndjson.gz");
17260
+ const network = [...this.network.values()];
17261
+ try {
17262
+ await this.screencast.stop(videoPath, durationSeconds, true);
17263
+ const bundle = {
17264
+ schemaVersion: 1,
17265
+ capturePolicyVersion: "safe-default-v1",
17266
+ recordingId: this.options.recordingId,
17267
+ startedAt: this.startedAt.toISOString(),
17268
+ durationMs: Math.round(durationSeconds * 1e3),
17269
+ videoStartOffsetMs: (this.screencast.videoStartedAtMs ?? this.startedAt.getTime()) - this.startedAt.getTime(),
17270
+ bodyCaptureEnabled: false,
17271
+ bodyCaptureRedacted: false,
17272
+ truncated: this.truncated || this.interrupted,
17273
+ console: this.console,
17274
+ network,
17275
+ timeline: this.timeline
17276
+ };
17277
+ await writeBundle(bundle, diagnosticsPath);
17278
+ } finally {
17279
+ this.detach();
17280
+ this.releaseCapture?.();
17281
+ }
17282
+ return {
17283
+ recordingId: this.options.recordingId,
17284
+ videoPath,
17285
+ frameDirectory: join10(this.options.outputDirectory, "frames"),
17286
+ diagnosticsPath,
17287
+ durationSeconds,
17288
+ stoppedReason: this.interrupted ? "browser_closed" : reason,
17289
+ 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 }
17290
+ };
17291
+ }
17292
+ };
17293
+
16848
17294
  // ../recorder/src/daemon.ts
16849
17295
  var sessions = /* @__PURE__ */ new Map();
16850
17296
  var lastActivity = Date.now();
17297
+ 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
17298
  async function runDaemon() {
16852
17299
  if (process.platform === "win32" && process.env.RILL_SOCKET) throw new Error("RILL_SOCKET overrides are not supported by authenticated Windows IPC.");
16853
17300
  const identity2 = process.platform === "win32" ? await windowsIpcIdentity() : void 0;
16854
17301
  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 });
17302
+ await mkdir9(identity2 ? dirname2(daemonStatePath()) : dirname2(socketPath), { recursive: true, mode: 448 });
17303
+ await mkdir9(recordingTempRoot(), { recursive: true, mode: 448 });
16857
17304
  await restoreRecoverableSessions();
16858
17305
  await purgeExpiredCaptures();
17306
+ if (await stat4(join11(dirname2(daemonStatePath()), "chrome-connection.json")).catch(() => null)) await browserConnection.connect();
17307
+ browserConnection.on("disconnected", () => {
17308
+ for (const entry of sessions.values()) if (entry.state === "recording" && entry.session instanceof ExtensionRecordingSession) void finalizeEntry(entry, "browser_closed");
17309
+ });
16859
17310
  if (!identity2) await unlink(socketPath).catch((error51) => {
16860
17311
  if (error51.code !== "ENOENT") throw error51;
16861
17312
  });
16862
- const server = createServer2((request3, response) => {
17313
+ const server = createServer3((request3, response) => {
16863
17314
  if (identity2 && !acceptsWindowsIpcToken(identity2.token, request3.headers.authorization)) {
16864
17315
  request3.resume();
16865
17316
  json2(response, 401, { error: { code: "authentication_required", message: "Recorder authentication required." } });
@@ -16879,13 +17330,15 @@ async function runDaemon() {
16879
17330
  await purgeExpiredCaptures();
16880
17331
  const hasActiveSession = [...sessions.values()].some((entry) => entry.state === "recording" || entry.state === "finalizing");
16881
17332
  const hasRetainedCapture = await hasRetainedRemoteCaptures(recordingTempRoot());
16882
- if (!hasActiveSession && !hasRetainedCapture && Date.now() - lastActivity > 5 * 6e4) server.close(() => process.exit(0));
17333
+ if (!hasActiveSession) browserConnection.expireIdle();
17334
+ if (!hasActiveSession && !hasRetainedCapture && !browserConnection.keepsRecorderAlive && Date.now() - lastActivity > 5 * 6e4) server.close(() => process.exit(0));
16883
17335
  })().catch(() => void 0);
16884
17336
  }, 3e4);
16885
17337
  idleTimer.unref();
16886
17338
  const close = () => {
16887
17339
  clearInterval(idleTimer);
16888
17340
  server.close();
17341
+ void browserConnection.close();
16889
17342
  if (!identity2) void unlink(socketPath).catch(() => void 0);
16890
17343
  };
16891
17344
  process.once("SIGTERM", close);
@@ -16894,14 +17347,27 @@ async function runDaemon() {
16894
17347
  async function route(request3, response) {
16895
17348
  lastActivity = Date.now();
16896
17349
  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"] });
17350
+ if (request3.method === "GET" && url2.pathname === "/health") return json2(response, 200, { ok: true, pid: process.pid, capabilities: ["tab-selection-v1", "chrome-connection-v1"] });
17351
+ if (request3.method === "POST" && url2.pathname === "/browser/connect") {
17352
+ try {
17353
+ return json2(response, 200, await browserConnection.connect());
17354
+ } catch {
17355
+ return json2(response, 500, { error: { code: "browser_unavailable", message: "Could not connect Chrome. Try again." } });
17356
+ }
17357
+ }
17358
+ if (request3.method === "GET" && url2.pathname === "/browser/status") return json2(response, 200, browserConnection.status());
17359
+ if (request3.method === "POST" && url2.pathname === "/browser/disconnect") {
17360
+ browserConnection.disconnect();
17361
+ return json2(response, 200, { connected: false });
17362
+ }
16898
17363
  if (request3.method === "POST" && url2.pathname === "/sessions") {
16899
17364
  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
17365
  try {
16901
17366
  const input = await readJson(request3);
16902
17367
  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 });
17368
+ const outputDirectory = join11(recordingTempRoot(), input.recordingId);
17369
+ 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.");
17370
+ const session = input.browser === "existing" ? await ExtensionRecordingSession.start({ ...input, outputDirectory }, browserConnection) : await RecordingSession.start({ ...input, outputDirectory });
16905
17371
  const status = session.status();
16906
17372
  const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, tabId: status.tabId, startedAt: status.startedAt }, state: "recording" };
16907
17373
  sessions.set(input.recordingId, entry);
@@ -16951,7 +17417,7 @@ function serializeEntry(entry) {
16951
17417
  }
16952
17418
  async function restoreRecoverableSessions() {
16953
17419
  try {
16954
- const value = JSON.parse(await readFile7(daemonStatePath(), "utf8"));
17420
+ const value = JSON.parse(await readFile8(daemonStatePath(), "utf8"));
16955
17421
  for (const [id, entry] of Object.entries(value)) {
16956
17422
  if (entry.status !== "ready" && entry.status !== "failed") continue;
16957
17423
  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 +17428,7 @@ async function restoreRecoverableSessions() {
16962
17428
  async function persistState() {
16963
17429
  const state = Object.fromEntries([...sessions.entries()].map(([id, entry]) => [id, serializeEntry(entry)]));
16964
17430
  const temporary = daemonStatePath() + "." + crypto.randomUUID() + ".tmp";
16965
- await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
17431
+ await writeFile8(temporary, `${JSON.stringify(state, null, 2)}
16966
17432
  `, { mode: 384 });
16967
17433
  await rename2(temporary, daemonStatePath());
16968
17434
  }
@@ -17085,8 +17551,11 @@ function formatRecording(value, apiUrl, color = terminalColor()) {
17085
17551
  const duration3 = value.durationSeconds ?? value.duration_seconds ?? artifact.durationSeconds;
17086
17552
  if (typeof duration3 === "number" && Number.isFinite(duration3)) lines.push(`Duration: ${Math.floor(duration3 / 60)}m ${Math.floor(duration3 % 60)}s`);
17087
17553
  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}`);
17554
+ if (value.browser === "existing") lines.push("Recording your selected Chrome tab.");
17555
+ else {
17556
+ if (typeof value.cdpUrl === "string" && value.cdpUrl) lines.push(`Browser connection: ${value.cdpUrl}`);
17557
+ if (typeof value.tabId === "string") lines.push(`Recorded tab: ${value.tabId}`);
17558
+ }
17090
17559
  if (typeof value.shareUrl === "string" && value.shareUrl) lines.push("", `Share: ${value.shareUrl}`);
17091
17560
  else if (status === "ready") lines.push("", "Ready. No share link returned; open this recording in your Rill account.");
17092
17561
  const message = value.message ?? value.error;
@@ -17450,7 +17919,7 @@ function controlPlaneUrl(environmentUrl) {
17450
17919
  async function removeLocalCapture(id) {
17451
17920
  await ensureDaemon(process.argv[1]);
17452
17921
  const local = await recordingStatus(id).catch(() => null);
17453
- if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
17922
+ if (local?.result?.frameDirectory) await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
17454
17923
  }
17455
17924
  function watchEncoding(apiUrl, id) {
17456
17925
  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 +17929,32 @@ function watchEncoding(apiUrl, id) {
17460
17929
  var program = new Command();
17461
17930
  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
17931
  program.configureOutput({ outputError: () => void 0 }).exitOverride();
17932
+ var browserCommands = program.command("browser").description("Connect the Chrome tab you already use.");
17933
+ browserCommands.command("connect").action(async () => {
17934
+ await ensureDaemon(process.argv[1], false, true);
17935
+ const result = await connectBrowser();
17936
+ if (program.opts().json || program.opts().pretty) emit(result, program.opts().pretty);
17937
+ else process.stdout.write(result.connected ? `Chrome is connected. You can start another recording.
17938
+ ` : `Connect Chrome
17939
+
17940
+ Open this link in Chrome, then choose your app tab in Rill:
17941
+ ${result.connectionUrl}
17942
+ `);
17943
+ });
17944
+ browserCommands.command("status").action(async () => {
17945
+ await ensureDaemon(process.argv[1], false, true);
17946
+ const result = await browserConnectionStatus();
17947
+ if (program.opts().json || program.opts().pretty) emit(result, program.opts().pretty);
17948
+ else process.stdout.write(result.connected ? `Chrome is connected.
17949
+ Tab: ${result.tab?.title}
17950
+ ` : "Chrome is not connected. Run rill browser connect to get started.\n");
17951
+ });
17952
+ browserCommands.command("disconnect").action(async () => {
17953
+ await ensureDaemon(process.argv[1], false, true);
17954
+ const result = await disconnectBrowser();
17955
+ if (program.opts().json || program.opts().pretty) emit(result, program.opts().pretty);
17956
+ else process.stdout.write("Chrome disconnected. Your tab stays open.\n");
17957
+ });
17463
17958
  function humanOutput(identity2 = false) {
17464
17959
  const options = program.opts();
17465
17960
  return !options.json && !options.pretty && (identity2 || options.human);
@@ -17560,10 +18055,14 @@ record2.command("tabs").requiredOption("--cdp-url <url>", "Existing Chromium CDP
17560
18055
  const options = program.opts();
17561
18056
  emit({ schemaVersion: 1, tabs: await listBrowserTabs(commandOptions.cdpUrl) }, options.pretty);
17562
18057
  });
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) => {
18058
+ 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
18059
  const options = program.opts();
17565
18060
  assertRecorderTransport();
17566
18061
  validateTabSelection(commandOptions);
18062
+ const connectedBrowser = await prepareBrowserRecording(commandOptions, async () => {
18063
+ await ensureDaemon(process.argv[1], false, true);
18064
+ return browserConnectionStatus();
18065
+ });
17567
18066
  await ensureAnonymousCredential(options.apiUrl);
17568
18067
  const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
17569
18068
  const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
@@ -17572,7 +18071,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
17572
18071
  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
18072
  const width = Number(commandOptions.width);
17574
18073
  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 };
18074
+ 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
18075
  const remote = await createRecordingWithRecovery({
17577
18076
  createRemote: () => client.createRecording(metadata),
17578
18077
  localStatus: recordingStatus,
@@ -17585,8 +18084,8 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
17585
18084
  rememberRecording(remote.recordingId);
17586
18085
  const local = await startRecordingWithCompensation(remote, {
17587
18086
  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 });
18087
+ await ensureDaemon(process.argv[1], Boolean(commandOptions.cdpUrl), Boolean(connectedBrowser));
18088
+ 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
18089
  },
17591
18090
  cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
17592
18091
  }, options.apiUrl);
@@ -17595,9 +18094,9 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
17595
18094
  os: process.platform,
17596
18095
  arch: process.arch,
17597
18096
  headed: Boolean(commandOptions.headed),
17598
- attached: Boolean(commandOptions.cdpUrl)
18097
+ attached: Boolean(commandOptions.cdpUrl || connectedBrowser)
17599
18098
  }).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) };
18099
+ 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
18100
  });
17602
18101
  emitRecording(result, options.pretty);
17603
18102
  });
@@ -17622,7 +18121,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
17622
18121
  const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
17623
18122
  const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
17624
18123
  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 });
18124
+ if (result2.status === "ready") await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
17626
18125
  else watchEncoding(options.apiUrl, recordingId);
17627
18126
  return;
17628
18127
  }
@@ -17644,7 +18143,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
17644
18143
  ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
17645
18144
  };
17646
18145
  emitRecording(result, options.pretty);
17647
- if (remote) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
18146
+ if (remote) await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
17648
18147
  });
17649
18148
  record2.command("retry").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
17650
18149
  const options = program.opts();
@@ -17676,7 +18175,7 @@ program.command("encoding-watch", { hidden: true }).argument("<recording-id>").a
17676
18175
  if (result.status === "ready") {
17677
18176
  await ensureDaemon(process.argv[1]);
17678
18177
  const local = await recordingStatus(id).catch(() => null);
17679
- if (local?.result?.frameDirectory) await rm7(dirname2(local.result.videoPath), { recursive: true, force: true });
18178
+ if (local?.result?.frameDirectory) await rm7(dirname3(local.result.videoPath), { recursive: true, force: true });
17680
18179
  }
17681
18180
  });
17682
18181
  var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
@@ -17760,17 +18259,17 @@ comments.command("edit").argument("<share-url>").requiredOption("--comment <id>"
17760
18259
  var drafts = program.command("drafts");
17761
18260
  drafts.command("list").action(async () => {
17762
18261
  const options = program.opts();
17763
- const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
18262
+ const root = process.env.RILL_TEMP ?? join12(tmpdir3(), "rill-recordings");
17764
18263
  const names = await readdir2(root).catch(() => []);
17765
18264
  emit({ schemaVersion: 1, drafts: names }, options.pretty);
17766
18265
  });
17767
18266
  drafts.command("purge").action(async () => {
17768
18267
  const options = program.opts();
17769
- const root = process.env.RILL_TEMP ?? join11(tmpdir3(), "rill-recordings");
18268
+ const root = process.env.RILL_TEMP ?? join12(tmpdir3(), "rill-recordings");
17770
18269
  const names = await readdir2(root).catch(() => []);
17771
18270
  let purged = 0;
17772
18271
  for (const name of names) {
17773
- const path = join11(root, name);
18272
+ const path = join12(root, name);
17774
18273
  const metadata = await stat5(path);
17775
18274
  if (Date.now() - metadata.mtimeMs > 24 * 60 * 60 * 1e3) {
17776
18275
  await rm7(path, { recursive: true, force: true });
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runalabs/rill-cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Record, inspect, and share agent-controlled browser runs with Rill.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "access": "public"
29
29
  },
30
30
  "scripts": {
31
- "build": "tsup",
31
+ "build": "tsup && node scripts/package-extension.mjs",
32
32
  "prepack": "npm run build",
33
33
  "test": "vitest run",
34
34
  "typecheck": "tsc --noEmit"
@@ -36,7 +36,8 @@
36
36
  "dependencies": {
37
37
  "commander": "^14.0.0",
38
38
  "playwright-core": "^1.55.0",
39
- "tus-js-client": "^4.3.1"
39
+ "tus-js-client": "^4.3.1",
40
+ "ws": "8.21.3"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@rill/contracts": "*",