@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.
- package/README.md +4 -4
- package/dist/cli.js +132 -7
- 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.
|
|
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://
|
|
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)
|
|
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
|
|
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.
|
|
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
|
+
}
|