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