@runalabs/rill-cli 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +17 -0
  2. package/dist/cli.js +97 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,6 +6,23 @@ Rill starts a local Chromium recorder. Your browser tool drives the returned CDP
6
6
 
7
7
  [Getting started](https://userill.dev/docs) · [Website CLI reference](https://userill.dev/docs/cli) · [Agent instructions](https://userill.dev/docs/cli#agents)
8
8
 
9
+ ## Cloud agent runs
10
+
11
+ Invited workspaces can supply a URL and prompt. Cloud runs need Node.js 22+ and a Rill login; Chrome and ffmpeg run on Rill's server.
12
+
13
+ ```sh
14
+ rill record https://example.com -p "Open pricing and describe the plans."
15
+ rill record https://example.com/dashboard -p "Describe the dashboard." --account staging
16
+ rill run status <run-id>
17
+ rill run cancel <run-id>
18
+ ```
19
+
20
+ The first run opens Rill authorization if needed. Save optional website test accounts in Rill Settings using an account name, exact HTTPS origin, username, password, and optional login URL. Login appears in the private video, with masked password inputs; login diagnostics are omitted. MFA, CAPTCHA, external SSO, and unsupported login forms return a blocker.
21
+
22
+ Cloud recordings are private by default. Closing the terminal disconnects without cancelling. Use `--no-wait` to return after submission, and `--id <same-run-id>` with identical arguments to retry an uncertain submission. Cloud runs use run credits: Free includes 3 per calendar month and Pro includes 20. Buy additional credits from Billing.
23
+
24
+ The following local-recording instructions remain available through `record start` and `record stop`.
25
+
9
26
  ## Install and verify
10
27
 
11
28
  Browser recording runs on macOS and Linux. Install Node.js 22+, Google Chrome or Chromium, and ffmpeg on `PATH`.
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/cli.ts
9
+ import { spawn as spawn6 } from "child_process";
9
10
  import { Command } from "commander";
10
11
  import { hostname as hostname3 } from "os";
11
12
  import { basename, dirname as dirname2 } from "path";
@@ -14631,6 +14632,11 @@ var recordingSchema = external_exports.object({
14631
14632
  truncated: external_exports.boolean()
14632
14633
  })
14633
14634
  });
14635
+ var agentOutcomeSchema = external_exports.object({
14636
+ outcome: external_exports.enum(["walkthrough", "passed", "failed", "inconclusive"]),
14637
+ summary: external_exports.string().min(1).max(1e3),
14638
+ evidence: external_exports.string().max(1e3)
14639
+ }).strict();
14634
14640
 
14635
14641
  // ../recorder/src/cursor-overlay.ts
14636
14642
  function installCursorOverlay(options) {
@@ -14917,6 +14923,12 @@ var RecordingSession = class _RecordingSession {
14917
14923
  stopPromise;
14918
14924
  maxTimer;
14919
14925
  removeCursorOverlay;
14926
+ diagnosticsPaused = false;
14927
+ secretValues = [];
14928
+ setDiagnosticPrivacy(paused, secrets) {
14929
+ this.diagnosticsPaused = paused;
14930
+ if (secrets) this.secretValues = secrets.filter(Boolean);
14931
+ }
14920
14932
  bodyBytes = 0;
14921
14933
  truncated = false;
14922
14934
  constructor(options, runtime) {
@@ -14934,7 +14946,7 @@ var RecordingSession = class _RecordingSession {
14934
14946
  const width = options.width ?? 1280;
14935
14947
  const height = options.height ?? 720;
14936
14948
  await mkdir2(options.outputDirectory, { recursive: true, mode: 448 });
14937
- const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory });
14949
+ const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer });
14938
14950
  const cdpSession = await runtime.context.newCDPSession(runtime.page);
14939
14951
  const screencast = new ScreencastRecorder(cdpSession, join3(options.outputDirectory, "frames"), width, height);
14940
14952
  const session = new _RecordingSession(options, { ...runtime, screencast });
@@ -14952,6 +14964,11 @@ var RecordingSession = class _RecordingSession {
14952
14964
  }, (options.maxDurationSeconds ?? 600) * 1e3);
14953
14965
  return session;
14954
14966
  }
14967
+ // In-process agents share this connection so exposed diagnostic bindings are
14968
+ // not dispatched through a second Playwright client.
14969
+ get recordedPage() {
14970
+ return this.page;
14971
+ }
14955
14972
  status() {
14956
14973
  return { recordingId: this.recordingId, cdpUrl: this.cdpUrl, startedAt: this.startedAt.toISOString(), status: this.stopPromise ? "finalizing" : "recording" };
14957
14974
  }
@@ -14979,13 +14996,14 @@ var RecordingSession = class _RecordingSession {
14979
14996
  timeline: this.timelineEvents.toSorted((left, right) => left.offsetMs - right.offsetMs)
14980
14997
  };
14981
14998
  try {
14982
- await writeBundle(bundle, diagnosticsPath);
14999
+ const safeBundle = JSON.parse(this.secretValues.reduce((text, secret) => text.split(JSON.stringify(secret).slice(1, -1)).join("[redacted]"), JSON.stringify(bundle)));
15000
+ await writeBundle(safeBundle, diagnosticsPath);
14983
15001
  await this.screencast.stop(videoPath, durationSeconds);
14984
15002
  } finally {
14985
15003
  await this.removeCursorOverlay?.();
14986
15004
  if (this.browserProcess) {
14987
15005
  await this.browser.close().catch(() => void 0);
14988
- this.browserProcess.kill("SIGTERM");
15006
+ await terminateBrowser(this.browserProcess);
14989
15007
  if (this.profileDirectory) await rm(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
14990
15008
  }
14991
15009
  }
@@ -15000,6 +15018,7 @@ var RecordingSession = class _RecordingSession {
15000
15018
  this.captureNetworkBodies = captureNetworkBodies;
15001
15019
  const offset = () => Date.now() - this.startedAt.getTime();
15002
15020
  const pushTimeline = (event) => {
15021
+ if (this.diagnosticsPaused) return;
15003
15022
  if (this.timelineEvents.length >= 1e4) {
15004
15023
  this.truncated = true;
15005
15024
  return;
@@ -15018,6 +15037,7 @@ var RecordingSession = class _RecordingSession {
15018
15037
  if (frame === this.page.mainFrame()) pushTimeline({ offsetMs: offset(), type: "navigation", label: sanitizeUrl(frame.url()) });
15019
15038
  });
15020
15039
  this.page.on("console", async (message) => {
15040
+ if (this.diagnosticsPaused) return;
15021
15041
  if (this.consoleEvents.length >= 1e4) {
15022
15042
  this.truncated = true;
15023
15043
  return;
@@ -15036,11 +15056,13 @@ var RecordingSession = class _RecordingSession {
15036
15056
  if (severity === "error" || severity === "warning") pushTimeline({ offsetMs: event.offsetMs, type: "console", label: event.text.slice(0, 160), severity });
15037
15057
  });
15038
15058
  this.page.on("pageerror", (error51) => {
15059
+ if (this.diagnosticsPaused) return;
15039
15060
  const event = { id: `con_${this.consoleEvents.length + 1}`, offsetMs: offset(), severity: "error", text: redactText(error51.message), stack: redactText(error51.stack ?? "").split("\n").slice(0, 20) };
15040
15061
  this.consoleEvents.push(event);
15041
15062
  pushTimeline({ offsetMs: event.offsetMs, type: "exception", label: event.text.slice(0, 160), severity: "error" });
15042
15063
  });
15043
15064
  this.page.on("request", (request2) => {
15065
+ if (this.diagnosticsPaused) return;
15044
15066
  if (this.networkEvents.length >= 5e3) {
15045
15067
  this.truncated = true;
15046
15068
  return;
@@ -15112,17 +15134,25 @@ async function launchManaged(options) {
15112
15134
  "--disable-component-update",
15113
15135
  "--disable-sync",
15114
15136
  "--metrics-recording-only",
15137
+ ...options.proxyServer ? [`--proxy-server=${options.proxyServer}`, "--proxy-bypass-list=<-loopback>", "--disable-quic", "--force-webrtc-ip-handling-policy=disable_non_proxied_udp"] : [],
15115
15138
  ...options.headed ? [] : ["--headless=new", "--hide-scrollbars"],
15116
15139
  "about:blank"
15117
15140
  ];
15118
- const browserProcess = spawn3(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false });
15141
+ const env = options.proxyServer ? Object.fromEntries(Object.entries(process.env).filter(([key]) => ["PATH", "HOME", "TMPDIR", "DISPLAY", "LANG", "LD_LIBRARY_PATH", "XDG_RUNTIME_DIR"].includes(key))) : process.env;
15142
+ const browserProcess = spawn3(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, env });
15119
15143
  const cdpUrl = `http://127.0.0.1:${port}`;
15120
- await waitForCdp(cdpUrl, browserProcess);
15121
- const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
15122
- const context = browser.contexts()[0];
15123
- const page = context.pages()[0] ?? await context.newPage();
15124
- await page.setViewportSize({ width: options.width, height: options.height });
15125
- return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
15144
+ try {
15145
+ await waitForCdp(cdpUrl, browserProcess);
15146
+ const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: options.outputDirectory, isLocal: true });
15147
+ const context = browser.contexts()[0];
15148
+ const page = context.pages()[0] ?? await context.newPage();
15149
+ await page.setViewportSize({ width: options.width, height: options.height });
15150
+ return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
15151
+ } catch (error51) {
15152
+ await terminateBrowser(browserProcess);
15153
+ await rm(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
15154
+ throw error51;
15155
+ }
15126
15156
  }
15127
15157
  async function connectAttached(cdpUrl, outputDirectory) {
15128
15158
  const browser = await chromium.connectOverCDP(cdpUrl, { artifactsDir: outputDirectory, isLocal: true, noDefaults: true });
@@ -15140,6 +15170,17 @@ async function getAvailablePort() {
15140
15170
  await new Promise((resolve) => server.close(() => resolve()));
15141
15171
  return port;
15142
15172
  }
15173
+ async function terminateBrowser(child) {
15174
+ if (child.exitCode !== null || child.signalCode !== null) return;
15175
+ await new Promise((resolve) => {
15176
+ const timeout = setTimeout(() => child.kill("SIGKILL"), 3e3);
15177
+ child.once("exit", () => {
15178
+ clearTimeout(timeout);
15179
+ resolve();
15180
+ });
15181
+ child.kill("SIGTERM");
15182
+ });
15183
+ }
15143
15184
  async function waitForCdp(cdpUrl, browserProcess) {
15144
15185
  const deadline = Date.now() + 15e3;
15145
15186
  while (Date.now() < deadline) {
@@ -15281,6 +15322,15 @@ var ControlPlaneClient = class {
15281
15322
  this.apiUrl = apiUrl;
15282
15323
  this.token = token;
15283
15324
  }
15325
+ createCloudRun(input) {
15326
+ return this.request("/api/cloud/runs", { method: "POST", body: JSON.stringify(input) });
15327
+ }
15328
+ cloudRun(id) {
15329
+ return this.request(`/api/cloud/runs/${encodeURIComponent(id)}`);
15330
+ }
15331
+ cancelCloudRun(id) {
15332
+ return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
15333
+ }
15284
15334
  health() {
15285
15335
  return this.request("/api/health", {}, false);
15286
15336
  }
@@ -15698,12 +15748,16 @@ program.command("doctor").description("Validate Rill prerequisites before record
15698
15748
  emit(result, options.pretty);
15699
15749
  if (result.status === "error") process.exitCode = 1;
15700
15750
  });
15701
- program.command("login").description("Authorize this agent using a human-approved device code.").action(async () => {
15751
+ async function authorize() {
15702
15752
  const options = program.opts();
15703
15753
  const client = new ControlPlaneClient(options.apiUrl, null);
15704
15754
  const authorization = await client.createDeviceAuthorization({ deviceName: `${hostname3()} \xB7 ${process.platform}` });
15705
15755
  process.stderr.write(`Open ${authorization.verificationUri} and enter ${authorization.userCode}
15706
15756
  `);
15757
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
15758
+ const child = spawn6(opener, [authorization.verificationUri], { stdio: "ignore", detached: true });
15759
+ child.on("error", () => void 0);
15760
+ child.unref();
15707
15761
  const deadline = Date.now() + authorization.expiresIn * 1e3;
15708
15762
  while (Date.now() < deadline) {
15709
15763
  await new Promise((resolve) => setTimeout(resolve, authorization.interval * 1e3));
@@ -15712,11 +15766,41 @@ program.command("login").description("Authorize this agent using a human-approve
15712
15766
  if (result.status !== "approved" || !result.token) throw new Error(`Device authorization ${result.status}.`);
15713
15767
  await saveCredential(options.apiUrl, result.token);
15714
15768
  emit({ schemaVersion: 1, status: "authenticated", expiresAt: result.expiresAt, apiUrl: options.apiUrl }, options.pretty);
15715
- return;
15769
+ return result.token;
15716
15770
  }
15717
15771
  throw new Error("Device authorization expired.");
15772
+ }
15773
+ program.command("login").description("Authorize Rill once.").action(async () => {
15774
+ await authorize();
15775
+ });
15776
+ var record2 = program.command("record").description("Run a cloud browser from a URL and prompt, or manage a local recording.").argument("[url]").option("-p, --prompt <prompt>").option("--expectation <text>").option("--account <name>", "Saved website test account").option("--id <uuid>", "Retry a submission with the same run ID").option("--no-wait").action(async (url2, commandOptions) => {
15777
+ if (!url2 || !commandOptions.prompt) throw new Error("Use rill record <url> -p <prompt>.");
15778
+ const options = program.opts();
15779
+ const token = await loadCredential(options.apiUrl) ?? await authorize();
15780
+ const client = new ControlPlaneClient(options.apiUrl, token);
15781
+ const id = commandOptions.id ?? crypto.randomUUID();
15782
+ process.stderr.write(`Run ID: ${id}
15783
+ `);
15784
+ let run2 = await client.createCloudRun({ id, url: url2, prompt: commandOptions.prompt, expectation: commandOptions.expectation, account: commandOptions.account });
15785
+ let last = "";
15786
+ while (commandOptions.wait !== false && !["completed", "failed", "cancelled"].includes(run2.status)) {
15787
+ if (last !== run2.status) {
15788
+ last = run2.status;
15789
+ process.stderr.write(`${last}
15790
+ `);
15791
+ }
15792
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
15793
+ run2 = await client.cloudRun(id);
15794
+ }
15795
+ emit(run2, options.pretty);
15796
+ if (run2.status === "failed") process.exitCode = 1;
15797
+ });
15798
+ var cloudRun = program.command("run").description("Retrieve or cancel a cloud run.");
15799
+ for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<run-id>").action(async (id) => {
15800
+ const options = program.opts();
15801
+ const client = new ControlPlaneClient(options.apiUrl, await loadCredential(options.apiUrl));
15802
+ emit(await (action === "status" ? client.cloudRun(id) : client.cancelCloudRun(id)), options.pretty);
15718
15803
  });
15719
- var record2 = program.command("record").description("Manage a browser recording session.");
15720
15804
  record2.command("start").option("--url <url>").option("--title <title>").option("--description <description>", "").option("--headed").option("--cdp-url <url>").option("--width <pixels>", "", "1280").option("--height <pixels>", "", "720").option("--max-duration <seconds>", "", "600").action(async (commandOptions) => {
15721
15805
  const options = program.opts();
15722
15806
  const token = await loadCredential(options.apiUrl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runalabs/rill-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Record, inspect, and share agent-controlled browser runs with Rill.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {