@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 +21 -6
- package/dist/cli.js +544 -45
- package/dist/rill-chrome-extension.zip +0 -0
- package/package.json +4 -3
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
|
|
91
|
-
#
|
|
92
|
-
|
|
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`, `
|
|
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
|
-
###
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
15578
|
-
import { mkdir as
|
|
15579
|
-
import { dirname, join as
|
|
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
|
-
|
|
16398
|
-
|
|
16399
|
-
|
|
16400
|
-
|
|
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
|
|
16408
|
-
const
|
|
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.
|
|
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
|
-
|
|
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
|
|
16856
|
-
await
|
|
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 =
|
|
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
|
|
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 =
|
|
16904
|
-
|
|
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
|
|
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
|
|
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 (
|
|
17089
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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 ??
|
|
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 ??
|
|
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 =
|
|
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.
|
|
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": "*",
|