@runalabs/rill-cli 0.1.1 → 0.1.3

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 +4 -4
  2. package/dist/cli.js +132 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,23 +2,23 @@
2
2
 
3
3
  Record, inspect, and share agent-controlled browser runs with Rill.
4
4
 
5
+ After `rill record stop`, compatible agent hosts may receive `feedbackRequested: true`. They can submit a short Rill product review with `rill feedback submit <recording-id>`. Use `rill record stop <recording-id> --no-feedback` to opt out for one run.
6
+
5
7
  ## Install
6
8
 
7
9
  ```sh
8
- npm install --global @runalabs/rill-cli@0.1.1
10
+ npm install --global @runalabs/rill-cli@0.1.3
9
11
  rill doctor
10
12
  ```
11
13
 
12
14
  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.
15
15
 
16
16
  ## Record
17
17
 
18
18
  ```sh
19
19
  rill login
20
20
  rill doctor
21
- rill record start --url https://staging.example.com --title "Agent reproduction"
21
+ rill record start --url https://example.com --title "Agent reproduction"
22
22
  # Connect browser automation to the returned cdpUrl.
23
23
  rill record stop <recording-id>
24
24
  ```
package/dist/cli.js CHANGED
@@ -603,9 +603,30 @@ 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
+ }
624
+ submitRecordingFeedback(recordingId, input) {
625
+ return this.request(`/api/recordings/${recordingId}/feedback`, {
626
+ method: "POST",
627
+ body: JSON.stringify(input)
628
+ });
629
+ }
609
630
  provisionVideo(recordingId, input) {
610
631
  return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
611
632
  }
@@ -690,6 +711,33 @@ var ControlPlaneClient = class {
690
711
  }
691
712
  };
692
713
 
714
+ // src/analytics.ts
715
+ var commandCategories = /* @__PURE__ */ new Map([
716
+ ["doctor", "doctor"],
717
+ ["record start", "record_start"],
718
+ ["record stop", "record_stop"],
719
+ ["record status", "record_status"],
720
+ ["record cancel", "record_cancel"],
721
+ ["upload", "upload"],
722
+ ["drafts list", "drafts_list"],
723
+ ["drafts purge", "drafts_purge"]
724
+ ]);
725
+ function cliCommandCategory(commandPath2) {
726
+ return commandCategories.get(commandPath2.join(" ")) ?? null;
727
+ }
728
+ function cliDurationBucket(milliseconds) {
729
+ if (milliseconds < 6e4) return "under_1m";
730
+ if (milliseconds < 5 * 6e4) return "1m_to_5m";
731
+ if (milliseconds < 15 * 6e4) return "5m_to_15m";
732
+ return "15m_plus";
733
+ }
734
+ function cliFailureCode(error) {
735
+ const directCode = typeof error === "object" && error !== null && "code" in error ? error.code : null;
736
+ if (typeof directCode === "string" && /^[a-z0-9_]{1,80}$/.test(directCode)) return directCode;
737
+ const message = error instanceof Error ? error.message : "";
738
+ return message.match(/^([a-z][a-z0-9_]{0,79}):/)?.[1] ?? "unexpected_error";
739
+ }
740
+
693
741
  // src/keychain.ts
694
742
  import { spawn as spawn4 } from "child_process";
695
743
  var service = "rill";
@@ -785,8 +833,10 @@ async function runDoctor(apiUrl, overrides = {}) {
785
833
  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
834
  } else if (compareVersions(dependencies.cliVersion, health.minimumCliVersion) < 0) {
787
835
  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) {
836
+ } else if (compareVersions(dependencies.cliVersion, health.recommendedCliVersion) < 0) {
789
837
  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}\`.` });
838
+ } else if (compareVersions(dependencies.cliVersion, health.recommendedCliVersion) > 0) {
839
+ checks.push({ name: "cli_version", status: "pass", message: `CLI ${dependencies.cliVersion} is supported.`, version: dependencies.cliVersion });
790
840
  } else {
791
841
  checks.push({ name: "cli_version", status: "pass", message: `CLI ${dependencies.cliVersion} is the recommended version.`, version: dependencies.cliVersion });
792
842
  }
@@ -889,9 +939,16 @@ async function remoteAwareRecordingStatus(recordingId, dependencies) {
889
939
  }
890
940
  }
891
941
 
942
+ // src/config.ts
943
+ var PRODUCTION_API_URL = "https://userill.dev";
944
+ function controlPlaneUrl(environmentUrl) {
945
+ return environmentUrl ?? PRODUCTION_API_URL;
946
+ }
947
+
892
948
  // src/cli.ts
893
949
  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);
950
+ 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);
951
+ var activeTelemetry = null;
895
952
  program.command("daemon", { hidden: true }).action(() => runDaemon());
896
953
  program.command("doctor").description("Validate Rill prerequisites before recording.").action(async () => {
897
954
  const options = program.opts();
@@ -932,24 +989,51 @@ record.command("start").option("--url <url>").option("--title <title>").option("
932
989
  },
933
990
  cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
934
991
  });
992
+ await client.markRecordingStarted(remote.recordingId, {
993
+ cliVersion: CLI_VERSION,
994
+ os: process.platform,
995
+ arch: process.arch,
996
+ headed: Boolean(commandOptions.headed),
997
+ attached: Boolean(commandOptions.cdpUrl)
998
+ }).catch(() => void 0);
935
999
  emit({ schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, maxDurationSeconds: Number(commandOptions.maxDuration) }, options.pretty);
936
1000
  });
937
- record.command("stop").argument("<recording-id>").option("--no-wait").action(async (recordingId, commandOptions) => {
1001
+ record.command("stop").argument("<recording-id>").option("--no-wait").option("--no-feedback").action(async (recordingId, commandOptions) => {
938
1002
  const options = program.opts();
939
1003
  await ensureDaemon(process.argv[1]);
940
1004
  const local = await stopRecording(recordingId);
941
1005
  if (!local.result) throw new Error(local.error ?? "The recorder did not produce an artifact.");
942
1006
  const token = await loadCredential(options.apiUrl);
943
1007
  const client = new ControlPlaneClient(options.apiUrl, token);
1008
+ const stopped = await client.markRecordingStopped(recordingId, {
1009
+ durationSeconds: local.result.durationSeconds,
1010
+ stoppedReason: local.result.stoppedReason,
1011
+ diagnosticsAvailable: true,
1012
+ requestFeedback: commandOptions.feedback
1013
+ }).catch(() => null);
944
1014
  const video = await stat2(local.result.videoPath);
945
- const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath) });
1015
+ const provisioned = await client.provisionVideo(recordingId, { sizeBytes: video.size, filename: basename(local.result.videoPath), uploadKind: "recorded_browser" });
946
1016
  await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
947
1017
  const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recordingId);
948
1018
  if (remote) process.stderr.write("\n");
949
- const result = { schemaVersion: 1, recordingId, status: remote ? "ready" : "processing", shareUrl: remote?.shareUrl ?? null, durationSeconds: local.result.durationSeconds, stoppedReason: local.result.stoppedReason, diagnostics: local.result.diagnostics };
1019
+ const result = { schemaVersion: 1, recordingId, status: remote ? "ready" : "processing", shareUrl: remote?.shareUrl ?? null, durationSeconds: local.result.durationSeconds, stoppedReason: local.result.stoppedReason, diagnostics: local.result.diagnostics, feedbackRequested: stopped?.feedbackRequested ?? false };
950
1020
  emit(result, options.pretty);
951
1021
  if (remote) await rm2(dirname2(local.result.videoPath), { recursive: true, force: true });
952
1022
  });
1023
+ var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
1024
+ feedback.command("submit").argument("<recording-id>").requiredOption("--outcome <outcome>", "succeeded, failed, or abandoned").option("--helped <text>", "What helped during the run", "").option("--friction <text>", "What created friction during the run", "").option("--improvement <text>", "The single most useful Rill improvement", "").action(async (recordingId, commandOptions) => {
1025
+ if (!["succeeded", "failed", "abandoned"].includes(commandOptions.outcome)) throw new Error("validation_failed: Feedback outcome must be succeeded, failed, or abandoned.");
1026
+ if (![commandOptions.helped, commandOptions.friction, commandOptions.improvement].some((value) => value.trim())) throw new Error("validation_failed: Include at least one short feedback answer.");
1027
+ const options = program.opts();
1028
+ const token = await loadCredential(options.apiUrl);
1029
+ const client = new ControlPlaneClient(options.apiUrl, token);
1030
+ await client.submitRecordingFeedback(recordingId, {
1031
+ outcome: commandOptions.outcome,
1032
+ whatHelped: commandOptions.helped,
1033
+ friction: commandOptions.friction,
1034
+ improvement: commandOptions.improvement
1035
+ });
1036
+ });
953
1037
  record.command("status").argument("<recording-id>").action(async (recordingId) => {
954
1038
  const options = program.opts();
955
1039
  const token = await loadCredential(options.apiUrl);
@@ -975,7 +1059,7 @@ program.command("upload").argument("<video-file>").option("--title <title>").opt
975
1059
  const client = new ControlPlaneClient(options.apiUrl, token);
976
1060
  const file = await stat2(videoFile);
977
1061
  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) });
1062
+ const upload = await client.provisionVideo(recording.recordingId, { sizeBytes: file.size, filename: basename(videoFile), uploadKind: "existing_video" });
979
1063
  await client.uploadVideo(upload.uploadUrl, videoFile);
980
1064
  const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recording.recordingId);
981
1065
  if (remote) process.stderr.write("\n");
@@ -1007,4 +1091,45 @@ drafts.command("purge").action(async () => {
1007
1091
  }
1008
1092
  emit({ schemaVersion: 1, purged }, options.pretty);
1009
1093
  });
1010
- program.parseAsync().catch(fail);
1094
+ program.hook("preAction", async (_command, actionCommand) => {
1095
+ const command = cliCommandCategory(commandPath(actionCommand));
1096
+ if (!command) return;
1097
+ const options = program.opts();
1098
+ const token = await loadCredential(options.apiUrl).catch(() => null);
1099
+ if (!token) return;
1100
+ const client = new ControlPlaneClient(options.apiUrl, token);
1101
+ const session = await client.agentSession().catch(() => null);
1102
+ if (!session?.analyticsEnabled) return;
1103
+ activeTelemetry = { client, command, startedAt: Date.now() };
1104
+ await client.reportCliCommand({ phase: "started", command, cliVersion: CLI_VERSION, os: process.platform, arch: process.arch }).catch(() => void 0);
1105
+ });
1106
+ program.hook("postAction", async () => {
1107
+ await finishCliTelemetry(process.exitCode ? new Error("command_reported_failure: Command returned a failure status.") : null);
1108
+ });
1109
+ program.parseAsync().catch(async (error) => {
1110
+ await finishCliTelemetry(error);
1111
+ fail(error);
1112
+ });
1113
+ function commandPath(actionCommand) {
1114
+ const path = [];
1115
+ let command = actionCommand;
1116
+ while (command && command !== program) {
1117
+ path.unshift(command.name());
1118
+ command = command.parent;
1119
+ }
1120
+ return path;
1121
+ }
1122
+ async function finishCliTelemetry(error) {
1123
+ const telemetry = activeTelemetry;
1124
+ activeTelemetry = null;
1125
+ if (!telemetry) return;
1126
+ await telemetry.client.reportCliCommand({
1127
+ phase: error ? "failed" : "completed",
1128
+ command: telemetry.command,
1129
+ cliVersion: CLI_VERSION,
1130
+ os: process.platform,
1131
+ arch: process.arch,
1132
+ durationBucket: cliDurationBucket(Date.now() - telemetry.startedAt),
1133
+ ...error ? { failureCode: cliFailureCode(error) } : {}
1134
+ }).catch(() => void 0);
1135
+ }
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.3",
4
4
  "description": "Record, inspect, and share agent-controlled browser runs with Rill.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {