@runalabs/rill-cli 0.1.1 → 0.1.2

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 +3 -4
  2. package/dist/cli.js +109 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,20 +5,19 @@ Record, inspect, and share agent-controlled browser runs with Rill.
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- npm install --global @runalabs/rill-cli@0.1.1
8
+ npm install --global @runalabs/rill-cli@0.1.2
9
9
  rill doctor
10
10
  ```
11
11
 
12
12
  Rill requires Node.js 22 or newer, Google Chrome or Chromium, and `ffmpeg`.
13
- The CLI defaults to the Rill private-alpha staging API. Set `RILL_API_URL` or
14
- pass `--api-url` to target another environment.
13
+ The CLI connects to `https://userill.dev` by default.
15
14
 
16
15
  ## Record
17
16
 
18
17
  ```sh
19
18
  rill login
20
19
  rill doctor
21
- rill record start --url https://staging.example.com --title "Agent reproduction"
20
+ rill record start --url https://example.com --title "Agent reproduction"
22
21
  # Connect browser automation to the returned cdpUrl.
23
22
  rill record stop <recording-id>
24
23
  ```
package/dist/cli.js CHANGED
@@ -603,9 +603,24 @@ var ControlPlaneClient = class {
603
603
  agentSession() {
604
604
  return this.request("/api/agent/session");
605
605
  }
606
+ reportCliCommand(input) {
607
+ return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
608
+ }
606
609
  createRecording(input) {
607
610
  return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
608
611
  }
612
+ markRecordingStarted(recordingId, input) {
613
+ return this.request(`/api/recordings/${recordingId}/local-started`, {
614
+ method: "POST",
615
+ body: JSON.stringify(input)
616
+ });
617
+ }
618
+ markRecordingStopped(recordingId, input) {
619
+ return this.request(`/api/recordings/${recordingId}/local-stopped`, {
620
+ method: "POST",
621
+ body: JSON.stringify(input)
622
+ });
623
+ }
609
624
  provisionVideo(recordingId, input) {
610
625
  return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
611
626
  }
@@ -690,6 +705,33 @@ var ControlPlaneClient = class {
690
705
  }
691
706
  };
692
707
 
708
+ // src/analytics.ts
709
+ var commandCategories = /* @__PURE__ */ new Map([
710
+ ["doctor", "doctor"],
711
+ ["record start", "record_start"],
712
+ ["record stop", "record_stop"],
713
+ ["record status", "record_status"],
714
+ ["record cancel", "record_cancel"],
715
+ ["upload", "upload"],
716
+ ["drafts list", "drafts_list"],
717
+ ["drafts purge", "drafts_purge"]
718
+ ]);
719
+ function cliCommandCategory(commandPath2) {
720
+ return commandCategories.get(commandPath2.join(" ")) ?? null;
721
+ }
722
+ function cliDurationBucket(milliseconds) {
723
+ if (milliseconds < 6e4) return "under_1m";
724
+ if (milliseconds < 5 * 6e4) return "1m_to_5m";
725
+ if (milliseconds < 15 * 6e4) return "5m_to_15m";
726
+ return "15m_plus";
727
+ }
728
+ function cliFailureCode(error) {
729
+ const directCode = typeof error === "object" && error !== null && "code" in error ? error.code : null;
730
+ if (typeof directCode === "string" && /^[a-z0-9_]{1,80}$/.test(directCode)) return directCode;
731
+ const message = error instanceof Error ? error.message : "";
732
+ return message.match(/^([a-z][a-z0-9_]{0,79}):/)?.[1] ?? "unexpected_error";
733
+ }
734
+
693
735
  // src/keychain.ts
694
736
  import { spawn as spawn4 } from "child_process";
695
737
  var service = "rill";
@@ -785,8 +827,10 @@ async function runDoctor(apiUrl, overrides = {}) {
785
827
  checks.push({ name: "cli_version", status: "fail", message: `CLI ${dependencies.cliVersion} could not be checked against the API compatibility policy.`, version: dependencies.cliVersion, recovery: "Deploy a Rill API that advertises minimumCliVersion and recommendedCliVersion." });
786
828
  } else if (compareVersions(dependencies.cliVersion, health.minimumCliVersion) < 0) {
787
829
  checks.push({ name: "cli_version", status: "fail", message: `CLI ${dependencies.cliVersion} is older than the minimum supported version ${health.minimumCliVersion}.`, version: dependencies.cliVersion, recovery: `Run \`npm install --global @runalabs/rill-cli@${health.recommendedCliVersion}\`.` });
788
- } else if (compareVersions(dependencies.cliVersion, health.recommendedCliVersion) !== 0) {
830
+ } else if (compareVersions(dependencies.cliVersion, health.recommendedCliVersion) < 0) {
789
831
  checks.push({ name: "cli_version", status: "warn", message: `CLI ${dependencies.cliVersion} is supported; ${health.recommendedCliVersion} is recommended.`, version: dependencies.cliVersion, recovery: `Run \`npm install --global @runalabs/rill-cli@${health.recommendedCliVersion}\`.` });
832
+ } else if (compareVersions(dependencies.cliVersion, health.recommendedCliVersion) > 0) {
833
+ checks.push({ name: "cli_version", status: "pass", message: `CLI ${dependencies.cliVersion} is supported.`, version: dependencies.cliVersion });
790
834
  } else {
791
835
  checks.push({ name: "cli_version", status: "pass", message: `CLI ${dependencies.cliVersion} is the recommended version.`, version: dependencies.cliVersion });
792
836
  }
@@ -889,9 +933,16 @@ async function remoteAwareRecordingStatus(recordingId, dependencies) {
889
933
  }
890
934
  }
891
935
 
936
+ // src/config.ts
937
+ var PRODUCTION_API_URL = "https://userill.dev";
938
+ function controlPlaneUrl(environmentUrl) {
939
+ return environmentUrl ?? PRODUCTION_API_URL;
940
+ }
941
+
892
942
  // src/cli.ts
893
943
  var program = new Command();
894
- program.name("rill").description("Record, inspect, and share agent browser runs.").version(CLI_VERSION).option("--api-url <url>", "Rill control plane", process.env.RILL_API_URL ?? "https://rill-staging.tight-shadow-8e12.workers.dev").option("--pretty", "Pretty-print JSON output", false);
944
+ 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);
945
+ var activeTelemetry = null;
895
946
  program.command("daemon", { hidden: true }).action(() => runDaemon());
896
947
  program.command("doctor").description("Validate Rill prerequisites before recording.").action(async () => {
897
948
  const options = program.opts();
@@ -932,6 +983,13 @@ record.command("start").option("--url <url>").option("--title <title>").option("
932
983
  },
933
984
  cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
934
985
  });
986
+ await client.markRecordingStarted(remote.recordingId, {
987
+ cliVersion: CLI_VERSION,
988
+ os: process.platform,
989
+ arch: process.arch,
990
+ headed: Boolean(commandOptions.headed),
991
+ attached: Boolean(commandOptions.cdpUrl)
992
+ }).catch(() => void 0);
935
993
  emit({ schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, maxDurationSeconds: Number(commandOptions.maxDuration) }, options.pretty);
936
994
  });
937
995
  record.command("stop").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
@@ -941,8 +999,13 @@ record.command("stop").argument("<recording-id>").option("--no-wait").action(asy
941
999
  if (!local.result) throw new Error(local.error ?? "The recorder did not produce an artifact.");
942
1000
  const token = await loadCredential(options.apiUrl);
943
1001
  const client = new ControlPlaneClient(options.apiUrl, token);
1002
+ await client.markRecordingStopped(recordingId, {
1003
+ durationSeconds: local.result.durationSeconds,
1004
+ stoppedReason: local.result.stoppedReason,
1005
+ diagnosticsAvailable: true
1006
+ }).catch(() => void 0);
944
1007
  const video = await stat2(local.result.videoPath);
945
- const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath) });
1008
+ const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
946
1009
  await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
947
1010
  const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recordingId);
948
1011
  if (remote) process.stderr.write("\n");
@@ -975,7 +1038,7 @@ program.command("upload").argument("<video-file>").option("--title <title>").opt
975
1038
  const client = new ControlPlaneClient(options.apiUrl, token);
976
1039
  const file = await stat2(videoFile);
977
1040
  const recording = await client.createRecording({ title: commandOptions.title ?? basename(videoFile), sourceUrl: null });
978
- const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file.size, filename: basename(videoFile) });
1041
+ const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file.size, filename: basename(videoFile), uploadKind: "existing_video" });
979
1042
  await client.uploadVideo(upload.uploadUrl, videoFile);
980
1043
  const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recording.recordingId);
981
1044
  if (remote) process.stderr.write("\n");
@@ -1007,4 +1070,45 @@ drafts.command("purge").action(async () => {
1007
1070
  }
1008
1071
  emit({ schemaVersion: 1, purged }, options.pretty);
1009
1072
  });
1010
- program.parseAsync().catch(fail);
1073
+ program.hook("preAction", async (_command, actionCommand) => {
1074
+ const command = cliCommandCategory(commandPath(actionCommand));
1075
+ if (!command) return;
1076
+ const options = program.opts();
1077
+ const token = await loadCredential(options.apiUrl).catch(() => null);
1078
+ if (!token) return;
1079
+ const client = new ControlPlaneClient(options.apiUrl, token);
1080
+ const session = await client.agentSession().catch(() => null);
1081
+ if (!session?.analyticsEnabled) return;
1082
+ activeTelemetry = { client, command, startedAt: Date.now() };
1083
+ await client.reportCliCommand({ phase: "started", command, cliVersion: CLI_VERSION, os: process.platform, arch: process.arch }).catch(() => void 0);
1084
+ });
1085
+ program.hook("postAction", async () => {
1086
+ await finishCliTelemetry(process.exitCode ? new Error("command_reported_failure: Command returned a failure status.") : null);
1087
+ });
1088
+ program.parseAsync().catch(async (error) => {
1089
+ await finishCliTelemetry(error);
1090
+ fail(error);
1091
+ });
1092
+ function commandPath(actionCommand) {
1093
+ const path = [];
1094
+ let command = actionCommand;
1095
+ while (command && command !== program) {
1096
+ path.unshift(command.name());
1097
+ command = command.parent;
1098
+ }
1099
+ return path;
1100
+ }
1101
+ async function finishCliTelemetry(error) {
1102
+ const telemetry = activeTelemetry;
1103
+ activeTelemetry = null;
1104
+ if (!telemetry) return;
1105
+ await telemetry.client.reportCliCommand({
1106
+ phase: error ? "failed" : "completed",
1107
+ command: telemetry.command,
1108
+ cliVersion: CLI_VERSION,
1109
+ os: process.platform,
1110
+ arch: process.arch,
1111
+ durationBucket: cliDurationBucket(Date.now() - telemetry.startedAt),
1112
+ ...error ? { failureCode: cliFailureCode(error) } : {}
1113
+ }).catch(() => void 0);
1114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runalabs/rill-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Record, inspect, and share agent-controlled browser runs with Rill.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {