@runalabs/rill-cli 0.1.3 → 0.1.5
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 +229 -16
- package/dist/cli.js +97 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,32 +1,245 @@
|
|
|
1
1
|
# @runalabs/rill-cli
|
|
2
2
|
|
|
3
|
-
Record,
|
|
3
|
+
Record a browser flow, keep its video and runtime evidence together, and return an inspectable share link. Use Rill to reproduce a bug, verify a change, or hand a browser run to another developer or agent.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Rill starts a local Chromium recorder. Your browser tool drives the returned CDP endpoint; Rill captures the selected page and uploads the result when you stop.
|
|
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
|
+
|
|
9
|
+
## Install and verify
|
|
10
|
+
|
|
11
|
+
Browser recording runs on macOS and Linux. Install Node.js 22+, Google Chrome or Chromium, and ffmpeg on `PATH`.
|
|
8
12
|
|
|
9
13
|
```sh
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
# macOS
|
|
15
|
+
brew install ffmpeg
|
|
16
|
+
# Debian / Ubuntu
|
|
17
|
+
sudo apt install ffmpeg
|
|
12
18
|
```
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
## Record
|
|
20
|
+
Then install the scoped package:
|
|
17
21
|
|
|
18
22
|
```sh
|
|
23
|
+
npm install --global @runalabs/rill-cli
|
|
24
|
+
rill --version
|
|
19
25
|
rill login
|
|
20
26
|
rill doctor
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Do not use `npx rill`: the unscoped package belongs to a different product. Doctor checks Node, API reachability, CLI compatibility, authentication, Chrome, and ffmpeg. Resolve failed checks before starting a run.
|
|
30
|
+
|
|
31
|
+
## Authenticate
|
|
32
|
+
|
|
33
|
+
### Interactive login
|
|
34
|
+
|
|
35
|
+
`rill login` prints a verification URL and device code. Open the URL, sign in, and approve the request. The CLI waits for approval and stores the credential in macOS Keychain or Linux Secret Service using `secret-tool`. Linux needs an available Secret Service keyring; use token authentication on headless machines without one.
|
|
36
|
+
|
|
37
|
+
### CI and unattended agents
|
|
38
|
+
|
|
39
|
+
Create a scoped, expiring agent credential in Rill Settings. Inject it as `RILL_TOKEN` through your environment's secret manager, then run `rill doctor`. This token takes precedence over the OS keychain. Do not put real tokens in source files or command examples.
|
|
40
|
+
|
|
41
|
+
The default API is `https://userill.dev`. `RILL_API_URL` changes it; the global `--api-url <url>` flag takes precedence. Login stores credentials per API URL. Manage and revoke credentials in Settings.
|
|
42
|
+
|
|
43
|
+
## First recording
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
rill record start --url https://example.com --title "Checkout verification"
|
|
47
|
+
# Save recordingId and connect your browser tool to the returned cdpUrl.
|
|
48
|
+
# Perform the flow in the existing recorded page.
|
|
23
49
|
rill record stop <recording-id>
|
|
50
|
+
rill inspect https://userill.dev/s/<share-secret>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Start returns `recordingId`, `cdpUrl`, `status`, `bodyCaptureEnabled`, and `maxDurationSeconds`. Stop finalizes capture, uploads video and diagnostics, and waits for playback. Confirm `status: "ready"` and a non-null `shareUrl` before returning the evidence link. Local capture finishing does not establish remote readiness.
|
|
54
|
+
|
|
55
|
+
### Connect Playwright
|
|
56
|
+
|
|
57
|
+
In a project with `playwright-core` installed, save this as `rill-run.mjs`:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
import { chromium } from 'playwright-core';
|
|
61
|
+
|
|
62
|
+
const browser = await chromium.connectOverCDP(process.argv[2]);
|
|
63
|
+
try {
|
|
64
|
+
const page = browser.contexts()[0].pages()[0];
|
|
65
|
+
if (!page) throw new Error('The recorded page is unavailable.');
|
|
66
|
+
await page.waitForLoadState('domcontentloaded');
|
|
67
|
+
// Add your flow and assertions here, using this page.
|
|
68
|
+
console.log(await page.title());
|
|
69
|
+
} finally {
|
|
70
|
+
// Disconnect this client; let rill record stop finalize the recording.
|
|
71
|
+
await browser.close();
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Run `node rill-run.mjs "<cdpUrl>"` with the exact endpoint from start. Then run `rill record stop <recording-id>`, including when your verification fails. A page title alone does not verify your app's behavior.
|
|
76
|
+
|
|
77
|
+
Capture follows the first page selected at recording start. Keep the flow in that page: new tabs and popups do not receive complete video and diagnostic coverage. Keep the recorded page open until Rill stops.
|
|
78
|
+
|
|
79
|
+
### Recording options
|
|
80
|
+
|
|
81
|
+
| Option | Behavior |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `--url <url>` | Open the target in a managed browser. |
|
|
84
|
+
| `--title <title>` | Name the run; defaults to a timestamped title. |
|
|
85
|
+
| `--description <text>` | Describe what the run demonstrates. |
|
|
86
|
+
| `--headed` | Show the managed browser; default is headless. |
|
|
87
|
+
| `--cdp-url <url>` | Attach to an existing Chromium browser and record its first open page. Navigate that page with your browser tool. |
|
|
88
|
+
| `--width <pixels>` / `--height <pixels>` | Managed viewport; defaults to 1280 × 720. |
|
|
89
|
+
| `--max-duration <seconds>` | Automatically finalize local capture; defaults to 600. Still call `record stop` to upload. |
|
|
90
|
+
|
|
91
|
+
Rill closes browsers it launches during finalization. Attached browsers remain open.
|
|
92
|
+
|
|
93
|
+
## Set up a coding agent
|
|
94
|
+
|
|
95
|
+
Install and authenticate the CLI where the agent runs. No separate skill installation is required. Optionally save the following workflow in your agent's project instructions or skill setup; it is also available in the [public agent instructions](https://userill.dev/docs/cli#agents):
|
|
96
|
+
|
|
97
|
+
1. Run `rill doctor` and resolve failed checks.
|
|
98
|
+
2. Start a run titled with the claim to verify.
|
|
99
|
+
3. Preserve `recordingId`; connect to `cdpUrl` using a browser tool that supports CDP.
|
|
100
|
+
4. Perform the authorized flow in the existing recorded page.
|
|
101
|
+
5. Stop the same recording and wait for readiness.
|
|
102
|
+
6. Inspect the returned share URL.
|
|
103
|
+
7. Return the link with what passed, failed, or could not be verified.
|
|
104
|
+
|
|
105
|
+
For a bug fix, record the reproduction and verification as separate runs. Attach the ready link to the handoff or pull request.
|
|
106
|
+
|
|
107
|
+
## Inspect a shared run
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
rill inspect https://userill.dev/s/<share-secret>
|
|
111
|
+
rill inspect https://userill.dev/s/<share-secret> > context.json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Inspect returns bounded JSON context for another agent without requiring login. Browser recordings carry video and synchronized console messages, exceptions, network metadata, navigation, and interaction events. Treat this as bounded evidence, not a guarantee of complete capture. Use the share page to watch the run and examine its diagnostics.
|
|
115
|
+
|
|
116
|
+
An active share URL is required. Expired, replaced, revoked, or deleted shares cannot be inspected; ask the owner for a current link.
|
|
117
|
+
|
|
118
|
+
## Upload an existing video
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
rill upload ./test-results/checkout.webm --title "Checkout test"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Upload WebM or MP4 evidence. The command waits for readiness and returns the recording ID and share URL. Existing uploads report `diagnosticsAvailable: false`; they do not import Playwright traces or reconstruct diagnostic events.
|
|
125
|
+
|
|
126
|
+
Both `upload` and `record stop` accept `--no-wait`. This still waits for upload but skips playback processing: the result is `status: "processing"` with `shareUrl: null`. Check the recording in the dashboard before sharing.
|
|
127
|
+
|
|
128
|
+
## Command reference
|
|
129
|
+
|
|
130
|
+
Use `rill --help` or `<command> --help` for supported flags.
|
|
131
|
+
|
|
132
|
+
| Command | Behavior |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| `rill login` | Authorize a device code and store a credential. |
|
|
135
|
+
| `rill doctor` | Check prerequisites, API compatibility, and authentication. |
|
|
136
|
+
| `rill record start [options]` | Start a managed or attached browser recording. |
|
|
137
|
+
| `rill record stop <recording-id>` | Finalize and upload; supports `--no-wait` and `--no-feedback`. |
|
|
138
|
+
| `rill record status <recording-id>` | Read local state, falling back to remote state if the local session is unavailable. |
|
|
139
|
+
| `rill record cancel <recording-id>` | Abandon a run created by this credential and release its remote quota reservation. |
|
|
140
|
+
| `rill upload <video-file>` | Upload video-only evidence; supports `--title` and `--no-wait`. |
|
|
141
|
+
| `rill inspect <share-url>` | Read bounded context without login. |
|
|
142
|
+
| `rill drafts list` | List directory names under the local draft root. |
|
|
143
|
+
| `rill drafts purge` | Delete draft directories last modified more than 24 hours ago. |
|
|
144
|
+
| `rill feedback submit <recording-id>` | Submit optional Rill-specific feedback. |
|
|
145
|
+
|
|
146
|
+
## Output and errors
|
|
147
|
+
|
|
148
|
+
Data commands emit JSON on stdout and human progress on stderr. Use global `--pretty` for indented JSON. Help and version commands print plain text.
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
rill --pretty record status <recording-id>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Selected fields from a successful stop result:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"schemaVersion": 1,
|
|
159
|
+
"recordingId": "<recording-id>",
|
|
160
|
+
"status": "ready",
|
|
161
|
+
"shareUrl": "https://userill.dev/s/<share-secret>"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Stop also returns duration, stop reason, diagnostic summary, and feedback instructions when applicable.
|
|
166
|
+
|
|
167
|
+
Success exits 0; command failures exit 1. Runtime failures emit an `error` object on stdout containing `code`, `message`, and optional `recovery` and `details`. Argument parsing failures may print plain text to stderr. Doctor exits 1 with its JSON check report when a check fails.
|
|
168
|
+
|
|
169
|
+
Check the exit status first, then structured fields. Known codes include `authentication_required`, `scope_denied`, `quota_exceeded`, `browser_unavailable`, `recording_not_found`, `upload_interrupted`, `processing_failed`, `share_revoked`, `validation_failed`, and the fallback `internal_error`. Not every failure has a specific code; avoid branching on message text.
|
|
170
|
+
|
|
171
|
+
## Recovery and troubleshooting
|
|
172
|
+
|
|
173
|
+
- **Doctor fails:** read the failed check's recovery field. Correct the credential, API connectivity, or executable path before recording.
|
|
174
|
+
- **Quota exceeded:** inspect `error.details.quota.type` and `blockingRecordingId` when present. Check the blocking recording; cancel only an abandoned run. Review storage and daily allowance in Billing.
|
|
175
|
+
- **Upload interrupted or processing timed out:** keep the recording ID and local artifacts. Check the dashboard for remote readiness. `record status` prefers local state and can report local `ready` without a share URL; this does not prove remote playback readiness.
|
|
176
|
+
- **Duration limit reached:** local capture finalizes automatically, but `record stop` is still needed to upload.
|
|
177
|
+
- **Local session missing:** `record status` falls back to remote state. `drafts list` locates local directories, which may be incomplete. There is no `drafts resume` command.
|
|
178
|
+
- **Share cannot be inspected:** open the complete URL in a browser; ask the owner for a current link if needed.
|
|
179
|
+
|
|
180
|
+
A failed local startup automatically attempts to cancel the remote reservation it just created. `record cancel` changes remote state and does not stop the local browser; finish local capture before abandoning the remote reservation.
|
|
181
|
+
|
|
182
|
+
When finalized artifacts survive, retain `recording.webm` and `diagnostics-v1.ndjson.gz` in the recording's draft directory. `rill upload <path-to-recording.webm>` is a video-only fallback: it creates a new recording and does not restore the original diagnostics. Successful stop with readiness removes that run's local artifact directory.
|
|
183
|
+
|
|
184
|
+
`drafts purge` deletes local draft directories older than 24 hours by modification time, without a confirmation prompt. Recover needed evidence before purging.
|
|
185
|
+
|
|
186
|
+
## Environment variables
|
|
187
|
+
|
|
188
|
+
| Variable | Purpose |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `RILL_TOKEN` | Agent credential; overrides the keychain. |
|
|
191
|
+
| `RILL_API_URL` | API origin; defaults to `https://userill.dev`. Global `--api-url` takes precedence. |
|
|
192
|
+
| `RILL_CHROME_PATH` | Chrome or Chromium executable path. |
|
|
193
|
+
| `RILL_FFMPEG` | ffmpeg executable; defaults to `ffmpeg` on `PATH`. |
|
|
194
|
+
| `RILL_HOME` | Recorder state directory; defaults to `~/.rill`. |
|
|
195
|
+
| `RILL_SOCKET` | Recorder socket; defaults to `recorder.sock` inside `RILL_HOME`. |
|
|
196
|
+
| `RILL_TEMP` | Artifact root; defaults to `rill-recordings` inside the system temporary directory. |
|
|
197
|
+
|
|
198
|
+
Set recorder variables before starting the background recorder; an existing recorder retains its original environment. Use the same configuration for recording, stopping, and draft recovery.
|
|
199
|
+
|
|
200
|
+
## Sharing and privacy
|
|
201
|
+
|
|
202
|
+
Ready recordings get an unlisted share link that expires after seven days. In the recording page, copy it, replace it with a fresh link, or stop sharing. Finished recordings can be deleted after a five-second undo window.
|
|
203
|
+
|
|
204
|
+
Diagnostic redaction targets cookies, credential headers, URL query values, fragments, embedded credentials, and secret-shaped values. Network-body capture defaults off and is controlled by the human-issued credential; the agent cannot enable it for itself. Redaction is best-effort and does not mask the video. Avoid displaying secrets, review evidence before sharing, and treat share URLs as sensitive.
|
|
205
|
+
|
|
206
|
+
## Optional agent feedback
|
|
207
|
+
|
|
208
|
+
Feedback begins with `rill doctor` and `rill record start`. When the workspace enables it, their JSON includes `feedbackRequested: true` and a `feedbackBrief` asking the agent to observe what went well, confusion or friction, and possible improvements throughout setup and the run. Review only Rill and report observed experience, not invented praise or issues.
|
|
209
|
+
|
|
210
|
+
At completion or abandonment, `record stop` still returns the recommended, pre-authorized `suggestedAction`. Submit a short review if practical:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
rill feedback submit <recording-id> --outcome succeeded --helped "The CDP URL was easy to connect to" --friction "Upload progress was unclear" --improvement "Explain upload and processing stages"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Startup errors retain the brief under `error.details`, with the recording ID when one was reserved. If no recording exists, submit setup feedback without an ID using `--outcome failed` or `--outcome abandoned`. Successful submission returns `feedbackStatus: "submitted"` and the recording ID (null for setup-only feedback).
|
|
217
|
+
|
|
218
|
+
Only use `--no-feedback` when the user explicitly requests it, including in demos and tests. Respect the workspace preference. Feedback is optional, pre-authorized, and must not delay or affect the recording result. Authentication or connectivity failures may prevent feedback; preserve the original task result.
|
|
219
|
+
|
|
220
|
+
Setup-only feedback requires the API endpoint `POST /api/feedback` and migration `0012_setup_feedback.sql`. Deploy the migration and API before distributing this CLI change. Existing recording feedback remains compatible.
|
|
221
|
+
|
|
222
|
+
## Update and uninstall
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
npm install --global @runalabs/rill-cli@latest
|
|
226
|
+
rill --version
|
|
227
|
+
rill doctor
|
|
24
228
|
```
|
|
25
229
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
230
|
+
Remove the package:
|
|
231
|
+
|
|
232
|
+
```sh
|
|
233
|
+
npm uninstall --global @runalabs/rill-cli
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
This leaves keychain credentials, recorder state, local drafts, and remote recordings in place. Revoke the credential in Settings if access should end, and remove `RILL_TOKEN` from the environment or secret manager. To remove a stored local credential for the default API:
|
|
237
|
+
|
|
238
|
+
```sh
|
|
239
|
+
# macOS
|
|
240
|
+
security delete-generic-password -a https://userill.dev -s rill
|
|
241
|
+
# Linux
|
|
242
|
+
secret-tool clear service rill api-url https://userill.dev
|
|
243
|
+
```
|
|
30
244
|
|
|
31
|
-
|
|
32
|
-
progress is written to stderr.
|
|
245
|
+
Substitute your API URL if you configured another environment. These commands clear the local credential; server-side revocation is managed in Settings.
|
package/dist/cli.js
CHANGED
|
@@ -601,7 +601,7 @@ var ControlPlaneClient = class {
|
|
|
601
601
|
return this.request("/api/health", {}, false);
|
|
602
602
|
}
|
|
603
603
|
agentSession() {
|
|
604
|
-
return this.request("/api/agent/session");
|
|
604
|
+
return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
|
|
605
605
|
}
|
|
606
606
|
reportCliCommand(input) {
|
|
607
607
|
return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
|
|
@@ -622,7 +622,7 @@ var ControlPlaneClient = class {
|
|
|
622
622
|
});
|
|
623
623
|
}
|
|
624
624
|
submitRecordingFeedback(recordingId, input) {
|
|
625
|
-
return this.request(`/api/recordings/${recordingId}/feedback
|
|
625
|
+
return this.request(recordingId ? `/api/recordings/${recordingId}/feedback` : "/api/feedback", {
|
|
626
626
|
method: "POST",
|
|
627
627
|
body: JSON.stringify(input)
|
|
628
628
|
});
|
|
@@ -794,6 +794,62 @@ function fail(error) {
|
|
|
794
794
|
process.exitCode = 1;
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
+
// src/feedback-protocol.ts
|
|
798
|
+
function suggestedFeedbackAction(recordingId) {
|
|
799
|
+
return {
|
|
800
|
+
type: "rill.feedback.submit",
|
|
801
|
+
recommended: true,
|
|
802
|
+
authorizedBy: "workspace_opt_in",
|
|
803
|
+
confirmationRequired: false,
|
|
804
|
+
failurePolicy: "non_blocking",
|
|
805
|
+
recordingId
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function recordingFeedbackFields(recordingId, feedbackRequested) {
|
|
809
|
+
return feedbackRequested ? { feedbackRequested: true, suggestedAction: suggestedFeedbackAction(recordingId) } : { feedbackRequested: false };
|
|
810
|
+
}
|
|
811
|
+
function feedbackSubmissionReceipt(recordingId) {
|
|
812
|
+
return {
|
|
813
|
+
schemaVersion: 1,
|
|
814
|
+
recordingId,
|
|
815
|
+
feedbackStatus: "submitted"
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function runFeedbackFields(feedbackEnabled, recordingId = null) {
|
|
819
|
+
return feedbackEnabled ? {
|
|
820
|
+
feedbackRequested: true,
|
|
821
|
+
feedbackBrief: {
|
|
822
|
+
authorizedBy: "workspace_opt_in",
|
|
823
|
+
confirmationRequired: false,
|
|
824
|
+
failurePolicy: "non_blocking",
|
|
825
|
+
recordingId,
|
|
826
|
+
instruction: "Observe your Rill experience throughout setup and the run. At completion or abandonment, submit a short factual review if practical. Feedback is optional and must not delay or affect the recording result. Only use --no-feedback when the user explicitly requests it.",
|
|
827
|
+
prompts: {
|
|
828
|
+
whatHelped: "What went well? Which Rill commands, browser setup steps, outputs, or recovery guidance helped?",
|
|
829
|
+
friction: "Where did you get confused, get stuck, or need to guess? Consider prerequisites, starting or connecting to the browser, commands, diagnostics, uploads, and recovery.",
|
|
830
|
+
improvement: "What possible improvements would make Rill easier or more useful? Prioritize actionable ideas grounded in this run."
|
|
831
|
+
},
|
|
832
|
+
submitCommand: `rill feedback submit${recordingId ? ` ${recordingId}` : ""} --outcome <${recordingId ? "succeeded|failed|abandoned" : "failed|abandoned"}> --helped <text> --friction <text> --improvement <text>`,
|
|
833
|
+
scope: "Review Rill, not the target website. Report only observed experience; do not invent problems or praise. Exclude credentials, personal data, and copied page content."
|
|
834
|
+
}
|
|
835
|
+
} : { feedbackRequested: false };
|
|
836
|
+
}
|
|
837
|
+
async function withRunFeedback(session, start) {
|
|
838
|
+
const enabled = await session().then((value) => value.agentFeedbackEnabled).catch(() => false);
|
|
839
|
+
let recordingId = null;
|
|
840
|
+
try {
|
|
841
|
+
const result = await start((id) => {
|
|
842
|
+
recordingId = id;
|
|
843
|
+
});
|
|
844
|
+
return { ...result, ...runFeedbackFields(enabled, result.recordingId) };
|
|
845
|
+
} catch (cause) {
|
|
846
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
847
|
+
const structured = error;
|
|
848
|
+
structured.details = { ...structured.details, ...runFeedbackFields(enabled, recordingId) };
|
|
849
|
+
throw error;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
797
853
|
// src/doctor.ts
|
|
798
854
|
import { spawn as spawn5 } from "child_process";
|
|
799
855
|
|
|
@@ -816,6 +872,7 @@ var defaultDependencies = {
|
|
|
816
872
|
async function runDoctor(apiUrl, overrides = {}) {
|
|
817
873
|
const dependencies = { ...defaultDependencies, ...overrides };
|
|
818
874
|
const checks = [];
|
|
875
|
+
let feedbackEnabled = false;
|
|
819
876
|
if (compareVersions(normalizeVersion(dependencies.nodeVersion), "22.0.0") >= 0) {
|
|
820
877
|
checks.push({ name: "node", status: "pass", message: `Node.js ${dependencies.nodeVersion} is supported.`, version: dependencies.nodeVersion });
|
|
821
878
|
} else {
|
|
@@ -849,6 +906,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
849
906
|
} else {
|
|
850
907
|
try {
|
|
851
908
|
const session = await dependencies.agentSession(apiUrl, token);
|
|
909
|
+
feedbackEnabled = session.agentFeedbackEnabled;
|
|
852
910
|
checks.push({ name: "authentication", status: "pass", message: `Credential is valid with ${session.scopes.length} scope${session.scopes.length === 1 ? "" : "s"}.` });
|
|
853
911
|
} catch (error) {
|
|
854
912
|
checks.push({ name: "authentication", status: "fail", message: `Credential validation failed: ${errorMessage(error)}`, recovery: `Run \`rill --api-url ${apiUrl} login\` to authorize a fresh credential.` });
|
|
@@ -857,7 +915,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
857
915
|
}
|
|
858
916
|
checks.push(await executableCheck("chrome", dependencies.chromePath, ["--version"], dependencies.commandVersion, "Install Google Chrome or Chromium, or set RILL_CHROME_PATH to its executable."));
|
|
859
917
|
checks.push(await executableCheck("ffmpeg", dependencies.ffmpegPath, ["-version"], dependencies.commandVersion, process.platform === "darwin" ? "Install ffmpeg with `brew install ffmpeg`, or set RILL_FFMPEG." : "Install ffmpeg using the system package manager, or set RILL_FFMPEG."));
|
|
860
|
-
return { schemaVersion: 1, status: checks.some((check) => check.status === "fail") ? "error" : "ok", apiUrl, checks };
|
|
918
|
+
return { schemaVersion: 1, status: checks.some((check) => check.status === "fail") ? "error" : "ok", apiUrl, checks, ...runFeedbackFields(feedbackEnabled) };
|
|
861
919
|
}
|
|
862
920
|
async function executableCheck(name, path, args, run2, recovery) {
|
|
863
921
|
try {
|
|
@@ -979,26 +1037,30 @@ record.command("start").option("--url <url>").option("--title <title>").option("
|
|
|
979
1037
|
const options = program.opts();
|
|
980
1038
|
const token = await loadCredential(options.apiUrl);
|
|
981
1039
|
const client = new ControlPlaneClient(options.apiUrl, token);
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1040
|
+
const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
|
|
1041
|
+
const width = Number(commandOptions.width);
|
|
1042
|
+
const height = Number(commandOptions.height);
|
|
1043
|
+
const remote = await client.createRecording({ title: commandOptions.title ?? `Browser recording \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}`, description: commandOptions.description, sourceUrl: commandOptions.url ?? null, width, height });
|
|
1044
|
+
rememberRecording(remote.recordingId);
|
|
1045
|
+
const local = await startRecordingWithCompensation(remote, {
|
|
1046
|
+
startLocal: async () => {
|
|
1047
|
+
await ensureDaemon(process.argv[1]);
|
|
1048
|
+
return startRecording({ recordingId: remote.recordingId, url: commandOptions.url, cdpUrl: commandOptions.cdpUrl, headed: Boolean(commandOptions.headed), width, height, maxDurationSeconds: Number(commandOptions.maxDuration), captureNetworkBodies: remote.bodyCaptureEnabled });
|
|
1049
|
+
},
|
|
1050
|
+
cancelRemote: (recordingId, reason) => client.cancelRecording(recordingId, reason)
|
|
1051
|
+
});
|
|
1052
|
+
await client.markRecordingStarted(remote.recordingId, {
|
|
1053
|
+
cliVersion: CLI_VERSION,
|
|
1054
|
+
os: process.platform,
|
|
1055
|
+
arch: process.arch,
|
|
1056
|
+
headed: Boolean(commandOptions.headed),
|
|
1057
|
+
attached: Boolean(commandOptions.cdpUrl)
|
|
1058
|
+
}).catch(() => void 0);
|
|
1059
|
+
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, maxDurationSeconds: Number(commandOptions.maxDuration) };
|
|
991
1060
|
});
|
|
992
|
-
|
|
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);
|
|
999
|
-
emit({ schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, maxDurationSeconds: Number(commandOptions.maxDuration) }, options.pretty);
|
|
1061
|
+
emit(result, options.pretty);
|
|
1000
1062
|
});
|
|
1001
|
-
record.command("stop").argument("<recording-id>").option("--no-wait").option("--no-feedback").action(async (recordingId, commandOptions) => {
|
|
1063
|
+
record.command("stop").argument("<recording-id>").option("--no-wait").option("--no-feedback", "Disable feedback only when the user explicitly requests it").action(async (recordingId, commandOptions) => {
|
|
1002
1064
|
const options = program.opts();
|
|
1003
1065
|
await ensureDaemon(process.argv[1]);
|
|
1004
1066
|
const local = await stopRecording(recordingId);
|
|
@@ -1016,23 +1078,34 @@ record.command("stop").argument("<recording-id>").option("--no-wait").option("--
|
|
|
1016
1078
|
await Promise.all([client.uploadVideo(provisioned.uploadUrl, local.result.videoPath), client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics)]);
|
|
1017
1079
|
const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recordingId);
|
|
1018
1080
|
if (remote) process.stderr.write("\n");
|
|
1019
|
-
const result = {
|
|
1081
|
+
const result = {
|
|
1082
|
+
schemaVersion: 1,
|
|
1083
|
+
recordingId,
|
|
1084
|
+
status: remote ? "ready" : "processing",
|
|
1085
|
+
shareUrl: remote?.shareUrl ?? null,
|
|
1086
|
+
durationSeconds: local.result.durationSeconds,
|
|
1087
|
+
stoppedReason: local.result.stoppedReason,
|
|
1088
|
+
diagnostics: local.result.diagnostics,
|
|
1089
|
+
...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
|
|
1090
|
+
};
|
|
1020
1091
|
emit(result, options.pretty);
|
|
1021
1092
|
if (remote) await rm2(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
1022
1093
|
});
|
|
1023
1094
|
var feedback = program.command("feedback").description("Submit a short Rill product review for a recorded run.");
|
|
1024
|
-
feedback.command("submit").argument("
|
|
1095
|
+
feedback.command("submit").argument("[recording-id]", "Omit only when setup failed before a recording was created").requiredOption("--outcome <outcome>", "succeeded, failed, or abandoned").option("--helped <text>", "What went well or helped during setup and the run", "").option("--friction <text>", "What created friction during the run", "").option("--improvement <text>", "Possible Rill improvements, prioritized by usefulness", "").action(async (recordingId, commandOptions) => {
|
|
1096
|
+
if (!recordingId && commandOptions.outcome === "succeeded") throw new Error("validation_failed: Setup feedback without a recording must have a failed or abandoned outcome.");
|
|
1025
1097
|
if (!["succeeded", "failed", "abandoned"].includes(commandOptions.outcome)) throw new Error("validation_failed: Feedback outcome must be succeeded, failed, or abandoned.");
|
|
1026
1098
|
if (![commandOptions.helped, commandOptions.friction, commandOptions.improvement].some((value) => value.trim())) throw new Error("validation_failed: Include at least one short feedback answer.");
|
|
1027
1099
|
const options = program.opts();
|
|
1028
1100
|
const token = await loadCredential(options.apiUrl);
|
|
1029
1101
|
const client = new ControlPlaneClient(options.apiUrl, token);
|
|
1030
|
-
await client.submitRecordingFeedback(recordingId, {
|
|
1102
|
+
await client.submitRecordingFeedback(recordingId ?? null, {
|
|
1031
1103
|
outcome: commandOptions.outcome,
|
|
1032
1104
|
whatHelped: commandOptions.helped,
|
|
1033
1105
|
friction: commandOptions.friction,
|
|
1034
1106
|
improvement: commandOptions.improvement
|
|
1035
1107
|
});
|
|
1108
|
+
emit(feedbackSubmissionReceipt(recordingId ?? null), options.pretty);
|
|
1036
1109
|
});
|
|
1037
1110
|
record.command("status").argument("<recording-id>").action(async (recordingId) => {
|
|
1038
1111
|
const options = program.opts();
|