@runalabs/rill-cli 0.1.11 → 0.1.13
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 +53 -4
- package/dist/cli.js +689 -511
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,9 +5,503 @@ var __export = (target, all) => {
|
|
|
5
5
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
// src/agent-session.ts
|
|
9
|
+
import { hostname, homedir } from "os";
|
|
10
|
+
import { mkdir, rm, stat } from "fs/promises";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { createHash, randomBytes } from "crypto";
|
|
13
|
+
|
|
14
|
+
// src/recording-guidance.ts
|
|
15
|
+
function cliCommand(apiUrl, ...args) {
|
|
16
|
+
const quote = (value) => /^[a-zA-Z0-9_./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
17
|
+
return ["rill", "--api-url", apiUrl, ...args].map(quote).join(" ");
|
|
18
|
+
}
|
|
19
|
+
var transportRecovery = "Use macOS or Linux for browser recording. On Windows, use RILL_TOKEN with rill upload <video-file> for video-only sharing; diagnostics are not imported. RILL_SOCKET does not enable Windows named-pipe or TCP support in this release.";
|
|
20
|
+
function recorderTransportCheck(platform = process.platform) {
|
|
21
|
+
return platform === "win32" ? { name: "recorder_transport", status: "fail", message: "The local recorder transport does not support Windows.", recovery: transportRecovery } : { name: "recorder_transport", status: "pass", message: "The local recorder uses a Unix-domain socket." };
|
|
22
|
+
}
|
|
23
|
+
function assertRecorderTransport(platform = process.platform) {
|
|
24
|
+
const check2 = recorderTransportCheck(platform);
|
|
25
|
+
if (check2.status === "fail") throw Object.assign(new Error(`unsupported_platform: ${check2.message}`), { code: "unsupported_platform", recovery: check2.recovery });
|
|
26
|
+
}
|
|
27
|
+
function recordingNextStep(apiUrl, recordingId, status, options = {}) {
|
|
28
|
+
if (status === "uploading" && options.resumableFrames) return { nextCommand: cliCommand(apiUrl, "record", "retry", recordingId, "--no-wait"), message: "Frame upload is incomplete. If the uploader is still running, let it finish. If it exited or was interrupted, run nextCommand to resume missing parts and finalize the upload. Do not start a second uploader while the first is active." };
|
|
29
|
+
if (status === "captured") return { nextCommand: cliCommand(apiUrl, "record", "stop", recordingId, "--no-wait"), message: "Captured locally; upload required. This is not a playable share yet." };
|
|
30
|
+
if (["uploading", "queued", "encoding", "processing", "finalizing"].includes(status)) return { nextCommand: cliCommand(apiUrl, "record", "status", recordingId), message: "Finalizing recordings hold a concurrency slot. Wait for ready; cancel only if abandoning the recording." };
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
function browserAttachment(cdpUrl) {
|
|
34
|
+
return {
|
|
35
|
+
protocol: "cdp",
|
|
36
|
+
cdpUrl,
|
|
37
|
+
playwrightMethod: "chromium.connectOverCDP",
|
|
38
|
+
instructions: "Attach to this endpoint and use the existing browser context. A browser tool must explicitly support CDP attachment; do not drive a separate browser. Disconnect the automation client without closing the recorded browser, then run record stop.",
|
|
39
|
+
shellFallback: "See the shell-only Playwright example in the Rill CLI README; install playwright-core in a temporary directory outside the project."
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/agent-session.ts
|
|
44
|
+
var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
45
|
+
var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
|
|
46
|
+
var AgentAuthError = class extends Error {
|
|
47
|
+
constructor(code, message) {
|
|
48
|
+
super(`${code}: ${message}`);
|
|
49
|
+
this.code = code;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function parseAuthorization(value) {
|
|
53
|
+
if (!value?.startsWith("{")) return null;
|
|
54
|
+
const saved = JSON.parse(value);
|
|
55
|
+
if (saved.version !== 1 || typeof saved.anonymous !== "boolean") throw new Error("Saved Rill authorization is invalid. Run rill connect --email <your-email>.");
|
|
56
|
+
return saved;
|
|
57
|
+
}
|
|
58
|
+
async function request(apiUrl, path, body, form = false) {
|
|
59
|
+
const url2 = new URL(apiUrl);
|
|
60
|
+
if (url2.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)) throw new Error("Agent authentication requires HTTPS.");
|
|
61
|
+
const response = await fetch(`${apiUrl}${path}`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
redirect: "error",
|
|
64
|
+
signal: AbortSignal.timeout(15e3),
|
|
65
|
+
headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
|
|
66
|
+
body: form ? new URLSearchParams(Object.entries(body).map(([key, value2]) => [key, String(value2)])) : JSON.stringify(body)
|
|
67
|
+
});
|
|
68
|
+
const value = await response.json();
|
|
69
|
+
if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function withTokens(saved, tokens) {
|
|
73
|
+
return {
|
|
74
|
+
...saved,
|
|
75
|
+
accessToken: tokens.access_token,
|
|
76
|
+
accessExpiresAt: Date.now() + tokens.expires_in * 1e3,
|
|
77
|
+
...tokens.identity_assertion ? { identityAssertion: tokens.identity_assertion, assertionExpires: tokens.assertion_expires } : {}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var save = (apiUrl, value) => saveCredential(apiUrl, JSON.stringify(value));
|
|
81
|
+
var pending = /* @__PURE__ */ new Map();
|
|
82
|
+
async function loadAgentCredential(apiUrl) {
|
|
83
|
+
if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
|
|
84
|
+
const inFlight = pending.get(apiUrl);
|
|
85
|
+
if (inFlight) return inFlight;
|
|
86
|
+
const promise2 = withAuthorizationLock(apiUrl, () => readAgentCredential(apiUrl));
|
|
87
|
+
pending.set(apiUrl, promise2);
|
|
88
|
+
try {
|
|
89
|
+
return await promise2;
|
|
90
|
+
} finally {
|
|
91
|
+
pending.delete(apiUrl);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function readAgentCredential(apiUrl) {
|
|
95
|
+
if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
|
|
96
|
+
const raw = await loadStoredCredential(apiUrl);
|
|
97
|
+
const saved = parseAuthorization(raw);
|
|
98
|
+
if (!saved) return raw;
|
|
99
|
+
if (saved.accessToken && (saved.accessExpiresAt ?? 0) > Date.now() + 6e4) return saved.accessToken;
|
|
100
|
+
if (!saved.identityAssertion && saved.anonymous && saved.registrationToken) return null;
|
|
101
|
+
if (!saved.identityAssertion) throw new Error(saved.connection ? `Connection is ${saved.connection.status}. Run ${cliCommand(apiUrl, "connect", "status", saved.connection.id)}.` : "Connection is awaiting approval. Run rill connect --email <your-email>.");
|
|
102
|
+
const tokens = await request(apiUrl, "/oauth2/token", { grant_type: JWT_GRANT, assertion: saved.identityAssertion }, true);
|
|
103
|
+
await save(apiUrl, withTokens(saved, tokens));
|
|
104
|
+
return tokens.access_token;
|
|
105
|
+
}
|
|
106
|
+
async function ensureAnonymousCredential(apiUrl) {
|
|
107
|
+
return withAuthorizationLock(apiUrl, () => createAnonymousCredential(apiUrl));
|
|
108
|
+
}
|
|
109
|
+
async function withAuthorizationLock(apiUrl, action) {
|
|
110
|
+
const root = join(homedir(), ".rill", "auth-locks");
|
|
111
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
112
|
+
const lock = join(root, createHash("sha256").update(apiUrl).digest("hex"));
|
|
113
|
+
const deadline = Date.now() + 3e4;
|
|
114
|
+
while (true) {
|
|
115
|
+
try {
|
|
116
|
+
await mkdir(lock, { mode: 448 });
|
|
117
|
+
break;
|
|
118
|
+
} catch (error51) {
|
|
119
|
+
if (error51.code !== "EEXIST") throw error51;
|
|
120
|
+
const info = await stat(lock).catch(() => null);
|
|
121
|
+
if (info && Date.now() - info.mtimeMs > 12e4) {
|
|
122
|
+
await rm(lock, { recursive: true, force: true });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return await action();
|
|
131
|
+
} finally {
|
|
132
|
+
await rm(lock, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function createAnonymousCredential(apiUrl) {
|
|
136
|
+
const existing = await readAgentCredential(apiUrl);
|
|
137
|
+
if (existing) return existing;
|
|
138
|
+
const stored = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
139
|
+
const initial = stored?.registrationToken ? stored : { version: 1, anonymous: true, registrationToken: randomBytes(32).toString("hex") };
|
|
140
|
+
await save(apiUrl, initial);
|
|
141
|
+
const registered = await request(apiUrl, "/agent/identity", { type: "anonymous", analytics_enabled: process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1", registration_token: initial.registrationToken, name: `${hostname()} \xB7 ${process.platform}` });
|
|
142
|
+
const saved = { ...initial, registrationToken: void 0, identityAssertion: registered.identity_assertion, assertionExpires: registered.assertion_expires, claimToken: registered.claim_token };
|
|
143
|
+
await save(apiUrl, saved);
|
|
144
|
+
process.stderr.write("Recording anonymously: three recordings, up to five minutes each. Recordings and share links expire after seven days. Run rill connect --email <your-email> to keep them.\n");
|
|
145
|
+
return await readAgentCredential(apiUrl);
|
|
146
|
+
}
|
|
147
|
+
async function connectAgent(apiUrl, email3, options = {}) {
|
|
148
|
+
let result = await withAuthorizationLock(apiUrl, () => beginConnection(apiUrl, email3));
|
|
149
|
+
if (options.wait === false) return result;
|
|
150
|
+
while (result.status === "pending") {
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, result.retryAfterSeconds * 1e3));
|
|
152
|
+
result = await pollConnection(apiUrl, result.authorizationId);
|
|
153
|
+
}
|
|
154
|
+
if (result.status === "expired") throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again using the same saved identity.");
|
|
155
|
+
if (result.status === "denied") throw new AgentAuthError("access_denied", "Connection request was denied or revoked. Run rill connect again only when ready to request approval.");
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
async function beginConnection(apiUrl, email3) {
|
|
159
|
+
if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
|
|
160
|
+
let saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
161
|
+
if (saved?.connection?.status === "pending" && saved.connection.expiresAt > Date.now()) {
|
|
162
|
+
if (saved.connection.email !== email3) throw new Error("A connection for another email is pending. Let it expire before starting a different connection.");
|
|
163
|
+
process.stderr.write(`Open ${saved.connection.verificationUri}
|
|
164
|
+
Sign in as ${email3} and enter ${saved.connection.userCode}. Approve only the request you started.
|
|
165
|
+
`);
|
|
166
|
+
return connectionView(apiUrl, saved);
|
|
167
|
+
}
|
|
168
|
+
let claim;
|
|
169
|
+
if (saved?.claimToken && saved.anonymous) {
|
|
170
|
+
try {
|
|
171
|
+
const result = await request(apiUrl, "/agent/identity/claim", { claim_token: saved.claimToken, email: email3 });
|
|
172
|
+
claim = result.claim_attempt;
|
|
173
|
+
} catch (error51) {
|
|
174
|
+
if (!(error51 instanceof AgentAuthError) || error51.code !== "claim_expired") throw error51;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!claim) {
|
|
178
|
+
await save(apiUrl, saved ?? { version: 1, anonymous: true });
|
|
179
|
+
const result = await request(apiUrl, "/agent/identity", { type: "service_auth", login_hint: email3, name: `${hostname()} \xB7 ${process.platform}` });
|
|
180
|
+
saved = { version: 1, anonymous: true, claimToken: result.claim_token };
|
|
181
|
+
await save(apiUrl, saved);
|
|
182
|
+
claim = result.claim;
|
|
183
|
+
}
|
|
184
|
+
process.stderr.write(`Open ${claim.verification_uri}
|
|
185
|
+
Sign in as ${email3} and enter ${claim.user_code}. Approve only the request you started.
|
|
186
|
+
`);
|
|
187
|
+
saved = { ...saved, connection: {
|
|
188
|
+
id: randomBytes(16).toString("hex"),
|
|
189
|
+
email: email3,
|
|
190
|
+
verificationUri: claim.verification_uri,
|
|
191
|
+
userCode: claim.user_code,
|
|
192
|
+
expiresAt: Date.now() + claim.expires_in * 1e3,
|
|
193
|
+
interval: claim.interval,
|
|
194
|
+
nextPollAt: Date.now() + claim.interval * 1e3,
|
|
195
|
+
status: "pending"
|
|
196
|
+
} };
|
|
197
|
+
await save(apiUrl, saved);
|
|
198
|
+
return connectionView(apiUrl, saved);
|
|
199
|
+
}
|
|
200
|
+
function connectionView(apiUrl, saved) {
|
|
201
|
+
const connection = saved.connection;
|
|
202
|
+
return {
|
|
203
|
+
status: connection.status,
|
|
204
|
+
authorizationId: connection.id,
|
|
205
|
+
apiUrl,
|
|
206
|
+
expiresAt: connection.status === "authenticated" ? saved.assertionExpires : new Date(connection.expiresAt).toISOString(),
|
|
207
|
+
retryAfterSeconds: connection.status === "pending" ? Math.max(0, Math.ceil((connection.nextPollAt - Date.now()) / 1e3)) : 0,
|
|
208
|
+
...connection.status === "pending" ? {
|
|
209
|
+
verificationUri: connection.verificationUri,
|
|
210
|
+
userCode: connection.userCode,
|
|
211
|
+
nextCommand: cliCommand(apiUrl, "connect", "status", connection.id)
|
|
212
|
+
} : connection.status === "authenticated" ? {} : { nextCommand: cliCommand(apiUrl, "connect", "--email", connection.email) }
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async function pollConnection(apiUrl, authorizationId) {
|
|
216
|
+
return withAuthorizationLock(apiUrl, () => pollSavedConnection(apiUrl, authorizationId));
|
|
217
|
+
}
|
|
218
|
+
async function pollSavedConnection(apiUrl, authorizationId) {
|
|
219
|
+
if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before polling a local connection.");
|
|
220
|
+
let saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
221
|
+
if (!saved?.connection || saved.connection.id !== authorizationId) throw new AgentAuthError("authorization_not_found", "No matching saved connection for this API URL. Use the latest connect response.");
|
|
222
|
+
const connection = saved.connection;
|
|
223
|
+
if (connection.status !== "pending") return connectionView(apiUrl, saved);
|
|
224
|
+
if (Date.now() >= connection.expiresAt) {
|
|
225
|
+
connection.status = "expired";
|
|
226
|
+
await save(apiUrl, saved);
|
|
227
|
+
return connectionView(apiUrl, saved);
|
|
228
|
+
}
|
|
229
|
+
if (Date.now() < connection.nextPollAt) return connectionView(apiUrl, saved);
|
|
230
|
+
connection.nextPollAt = Date.now() + connection.interval * 1e3;
|
|
231
|
+
await save(apiUrl, saved);
|
|
232
|
+
try {
|
|
233
|
+
const tokens = await request(apiUrl, "/oauth2/token", { grant_type: CLAIM_GRANT, claim_token: saved.claimToken }, true);
|
|
234
|
+
saved = withTokens({ version: 1, anonymous: false, connection: { ...connection, status: "authenticated", userCode: "" } }, tokens);
|
|
235
|
+
} catch (error51) {
|
|
236
|
+
if (!(error51 instanceof AgentAuthError)) throw error51;
|
|
237
|
+
if (error51.code === "expired_token" || error51.code === "claim_expired") connection.status = "expired";
|
|
238
|
+
else if (error51.code === "access_denied" || error51.code === "claim_revoked") connection.status = "denied";
|
|
239
|
+
else if (error51.code === "slow_down") {
|
|
240
|
+
connection.interval += 5;
|
|
241
|
+
connection.nextPollAt = Date.now() + connection.interval * 1e3;
|
|
242
|
+
} else if (error51.code !== "authorization_pending") throw error51;
|
|
243
|
+
}
|
|
244
|
+
await save(apiUrl, saved);
|
|
245
|
+
return connectionView(apiUrl, saved);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/keychain.ts
|
|
249
|
+
import { spawn } from "child_process";
|
|
250
|
+
var service = "rill";
|
|
251
|
+
async function saveCredential(apiUrl, token) {
|
|
252
|
+
if (process.platform === "darwin") {
|
|
253
|
+
await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (process.platform === "linux") {
|
|
257
|
+
await run("secret-tool", ["store", "--label=Rill agent credential", "service", service, "api-url", apiUrl], token);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
throw new Error("OS keychain storage is not supported on this platform yet. Use RILL_TOKEN.");
|
|
261
|
+
}
|
|
262
|
+
function loadCredential(apiUrl) {
|
|
263
|
+
return loadAgentCredential(apiUrl);
|
|
264
|
+
}
|
|
265
|
+
async function loadStoredCredential(apiUrl) {
|
|
266
|
+
try {
|
|
267
|
+
if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
|
|
268
|
+
if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
|
|
269
|
+
} catch (error51) {
|
|
270
|
+
const code = error51.exitCode;
|
|
271
|
+
if (process.platform === "darwin" && code === 44) return null;
|
|
272
|
+
if (process.platform === "linux" && code === 1 && !error51.message.trim()) return null;
|
|
273
|
+
throw new Error("Cannot read the OS credential store. Unlock your keychain or enable Secret Service; your identity has not been replaced.");
|
|
274
|
+
}
|
|
275
|
+
throw new Error("Local authorization requires macOS Keychain or Linux Secret Service.");
|
|
276
|
+
}
|
|
277
|
+
function run(command, args, input) {
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
280
|
+
const stdout = [];
|
|
281
|
+
const stderr = [];
|
|
282
|
+
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
283
|
+
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
284
|
+
child.once("error", reject);
|
|
285
|
+
child.once("exit", (code) => code === 0 ? resolve(Buffer.concat(stdout).toString("utf8")) : reject(Object.assign(new Error(Buffer.concat(stderr).toString("utf8").trim()), { exitCode: code })));
|
|
286
|
+
if (input) child.stdin.end(input);
|
|
287
|
+
else child.stdin.end();
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/control-plane.ts
|
|
292
|
+
import { createReadStream } from "fs";
|
|
293
|
+
import { readFile, stat as stat2 } from "fs/promises";
|
|
294
|
+
import { Upload } from "tus-js-client";
|
|
295
|
+
var ControlPlaneError = class extends Error {
|
|
296
|
+
constructor(code, message, recovery, details, httpStatus) {
|
|
297
|
+
super(`${code}: ${message}${recovery ? `: ${recovery}` : ""}`);
|
|
298
|
+
this.code = code;
|
|
299
|
+
this.recovery = recovery;
|
|
300
|
+
this.details = details;
|
|
301
|
+
this.httpStatus = httpStatus;
|
|
302
|
+
this.name = "ControlPlaneError";
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
var ControlPlaneHttpError = class extends Error {
|
|
306
|
+
constructor(httpStatus) {
|
|
307
|
+
super(`Request failed (${httpStatus}).`);
|
|
308
|
+
this.httpStatus = httpStatus;
|
|
309
|
+
this.name = "ControlPlaneHttpError";
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
var ControlPlaneClient = class {
|
|
313
|
+
constructor(apiUrl, token) {
|
|
314
|
+
this.apiUrl = apiUrl;
|
|
315
|
+
this.token = token;
|
|
316
|
+
}
|
|
317
|
+
recordingStatusCommand(id) {
|
|
318
|
+
return cliCommand(this.apiUrl, "record", "status", id);
|
|
319
|
+
}
|
|
320
|
+
encodingCapabilities() {
|
|
321
|
+
return this.request("/api/encoding/capabilities");
|
|
322
|
+
}
|
|
323
|
+
beginEncoding(id, manifest) {
|
|
324
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}`, { method: "POST", body: JSON.stringify(manifest) });
|
|
325
|
+
}
|
|
326
|
+
encodingStatus(id) {
|
|
327
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}`);
|
|
328
|
+
}
|
|
329
|
+
uploadEncodingPart(id, part, bytes) {
|
|
330
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}/parts/${part}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "Content-Length": String(bytes.length) }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(12e4) });
|
|
331
|
+
}
|
|
332
|
+
completeEncoding(id) {
|
|
333
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}/complete`, { method: "POST" });
|
|
334
|
+
}
|
|
335
|
+
retryEncoding(id) {
|
|
336
|
+
return this.request(`/api/encoding/${encodeURIComponent(id)}/retry`, { method: "POST" });
|
|
337
|
+
}
|
|
338
|
+
createCloudRun(input) {
|
|
339
|
+
return this.request("/api/cloud/runs", { method: "POST", body: JSON.stringify(input) });
|
|
340
|
+
}
|
|
341
|
+
cloudRun(id) {
|
|
342
|
+
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}`);
|
|
343
|
+
}
|
|
344
|
+
cancelCloudRun(id) {
|
|
345
|
+
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
|
|
346
|
+
}
|
|
347
|
+
health() {
|
|
348
|
+
return this.request("/api/health", {}, false);
|
|
349
|
+
}
|
|
350
|
+
agentSession() {
|
|
351
|
+
return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
|
|
352
|
+
}
|
|
353
|
+
reportCliCommand(input) {
|
|
354
|
+
return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
|
|
355
|
+
}
|
|
356
|
+
createRecording(input) {
|
|
357
|
+
return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
|
|
358
|
+
}
|
|
359
|
+
markRecordingStarted(recordingId, input) {
|
|
360
|
+
return this.request(`/api/recordings/${recordingId}/local-started`, {
|
|
361
|
+
method: "POST",
|
|
362
|
+
body: JSON.stringify(input)
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
markRecordingStopped(recordingId, input) {
|
|
366
|
+
return this.request(`/api/recordings/${recordingId}/local-stopped`, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
body: JSON.stringify(input)
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
submitRecordingFeedback(recordingId, input) {
|
|
372
|
+
return this.request(recordingId ? `/api/recordings/${recordingId}/feedback` : "/api/feedback", {
|
|
373
|
+
method: "POST",
|
|
374
|
+
body: JSON.stringify(input)
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
provisionVideo(recordingId, input) {
|
|
378
|
+
return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
|
|
379
|
+
}
|
|
380
|
+
recordingStatus(recordingId) {
|
|
381
|
+
return this.request(`/api/recordings/${recordingId}`);
|
|
382
|
+
}
|
|
383
|
+
cancelRecording(recordingId, reason = "cancelled_by_agent") {
|
|
384
|
+
return this.request(`/api/recordings/${recordingId}/cancel`, {
|
|
385
|
+
method: "POST",
|
|
386
|
+
body: JSON.stringify({ reason })
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async waitUntilPlayable(recordingId, timeoutMs = 10 * 6e4) {
|
|
390
|
+
const deadline = Date.now() + timeoutMs;
|
|
391
|
+
let intervalMs = 1e3;
|
|
392
|
+
while (Date.now() < deadline) {
|
|
393
|
+
const recording = await this.recordingStatus(recordingId);
|
|
394
|
+
if (recording.status === "ready" && recording.shareUrl) {
|
|
395
|
+
return { ...recording, shareUrl: new URL(recording.shareUrl, this.apiUrl).toString() };
|
|
396
|
+
}
|
|
397
|
+
if (recording.status === "failed") throw new Error(`processing_failed: ${recording.failure_message ?? "Cloudflare Stream could not process the recording."}`);
|
|
398
|
+
process.stderr.write("\rUpload complete; waiting for playable video. ");
|
|
399
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
400
|
+
intervalMs = Math.min(5e3, Math.round(intervalMs * 1.4));
|
|
401
|
+
}
|
|
402
|
+
throw new ControlPlaneError("processing_pending", "Upload complete; the recording is not playable yet.", `Run ${cliCommand(this.apiUrl, "record", "status", recordingId)} to continue checking.`, { recordingId, nextCommand: cliCommand(this.apiUrl, "record", "status", recordingId) });
|
|
403
|
+
}
|
|
404
|
+
async uploadVideo(uploadUrl, filePath) {
|
|
405
|
+
const file2 = await stat2(filePath);
|
|
406
|
+
if (uploadUrl.startsWith(this.apiUrl)) {
|
|
407
|
+
const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
|
|
408
|
+
if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
|
|
409
|
+
process.stderr.write("Upload complete; playback processing may continue.\n");
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
await new Promise((resolve, reject) => {
|
|
413
|
+
const upload = new Upload(createReadStream(filePath), {
|
|
414
|
+
uploadUrl,
|
|
415
|
+
uploadSize: file2.size,
|
|
416
|
+
retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
|
|
417
|
+
removeFingerprintOnSuccess: true,
|
|
418
|
+
onError: reject,
|
|
419
|
+
onSuccess: () => resolve(),
|
|
420
|
+
onProgress: (sent, total) => process.stderr.write(`\ruploading ${Math.round(sent / total * 100)}%`)
|
|
421
|
+
});
|
|
422
|
+
upload.start();
|
|
423
|
+
});
|
|
424
|
+
process.stderr.write("\nUpload complete; playback processing may continue.\n");
|
|
425
|
+
}
|
|
426
|
+
async uploadDiagnostics(recordingId, filePath, summary) {
|
|
427
|
+
const body = await readFile(filePath);
|
|
428
|
+
return this.request(`/api/recordings/${recordingId}/diagnostics`, { method: "PUT", headers: { "Content-Encoding": "gzip", "Content-Type": "application/x-ndjson", "Content-Length": String(body.length), "X-Rill-Summary": JSON.stringify(summary) }, body });
|
|
429
|
+
}
|
|
430
|
+
listRecordings() {
|
|
431
|
+
return this.request("/api/recordings");
|
|
432
|
+
}
|
|
433
|
+
createDeviceAuthorization(input) {
|
|
434
|
+
return this.request("/api/device/authorizations", { method: "POST", body: JSON.stringify(input) }, false);
|
|
435
|
+
}
|
|
436
|
+
pollDeviceAuthorization(deviceCode) {
|
|
437
|
+
return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
|
|
438
|
+
}
|
|
439
|
+
inspect(shareUrl, cursor) {
|
|
440
|
+
if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
|
|
441
|
+
const url2 = new URL(shareUrl);
|
|
442
|
+
const slug = url2.pathname.split("/").filter(Boolean).at(-1);
|
|
443
|
+
if (!slug) throw new Error("The share URL does not contain a slug.");
|
|
444
|
+
return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
445
|
+
if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
|
|
446
|
+
return response.json();
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
reviewPath(shareUrl) {
|
|
450
|
+
const url2 = new URL(shareUrl);
|
|
451
|
+
if (url2.origin !== new URL(this.apiUrl).origin) throw new Error("The share URL must match --api-url before sending agent credentials.");
|
|
452
|
+
const match = /^\/s\/([^/]+)\/?$/.exec(url2.pathname);
|
|
453
|
+
if (!match) throw new Error("Expected a Rill /s/<slug> review link.");
|
|
454
|
+
return `/api/review/shares/${encodeURIComponent(decodeURIComponent(match[1]))}`;
|
|
455
|
+
}
|
|
456
|
+
reply(shareUrl, discussionId, body, requestId) {
|
|
457
|
+
return this.request(`${this.reviewPath(shareUrl)}/discussions/${encodeURIComponent(discussionId)}/replies`, { method: "POST", body: JSON.stringify({ body, requestId }) });
|
|
458
|
+
}
|
|
459
|
+
editReply(shareUrl, commentId, body) {
|
|
460
|
+
return this.request(`${this.reviewPath(shareUrl)}/comments/${encodeURIComponent(commentId)}`, { method: "PATCH", body: JSON.stringify({ body }) });
|
|
461
|
+
}
|
|
462
|
+
async request(path, init = {}, authenticated = true) {
|
|
463
|
+
const headers = new Headers(init.headers);
|
|
464
|
+
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
465
|
+
if (authenticated) {
|
|
466
|
+
const token = typeof this.token === "function" ? await this.token() : this.token;
|
|
467
|
+
if (!token) throw new Error("authentication_required: Run `rill login` or set RILL_TOKEN.");
|
|
468
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
469
|
+
}
|
|
470
|
+
const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
|
|
471
|
+
const value = await response.json().catch(() => null);
|
|
472
|
+
if (!response.ok && value?.error?.code && value.error.message) {
|
|
473
|
+
const quota = value.error.details?.quota;
|
|
474
|
+
const blockingId = quota?.blockingRecordingId;
|
|
475
|
+
const concurrency = quota?.type === "credential_concurrency" || quota?.type === "workspace_concurrency";
|
|
476
|
+
const recovery = value.error.code === "quota_exceeded" && concurrency && blockingId ? `Run ${cliCommand(this.apiUrl, "record", "status", blockingId)}. Uploading and processing recordings hold a concurrency slot: wait for ready. If captured locally, run record stop to upload. Cancel only an abandoned recording.` : value.error.recovery;
|
|
477
|
+
throw new ControlPlaneError(value.error.code, value.error.message, recovery, value.error.details, response.status);
|
|
478
|
+
}
|
|
479
|
+
if (!response.ok) throw new ControlPlaneHttpError(response.status);
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// src/whoami.ts
|
|
485
|
+
async function whoami(apiUrl) {
|
|
486
|
+
const token = await loadCredential(apiUrl);
|
|
487
|
+
if (!token) return { schemaVersion: 1, status: "not_connected", authenticated: false, apiUrl };
|
|
488
|
+
const session = await new ControlPlaneClient(apiUrl, token).agentSession();
|
|
489
|
+
return {
|
|
490
|
+
schemaVersion: 1,
|
|
491
|
+
status: session.anonymous ? "anonymous" : "connected",
|
|
492
|
+
authenticated: true,
|
|
493
|
+
apiUrl,
|
|
494
|
+
anonymous: Boolean(session.anonymous),
|
|
495
|
+
email: session.email ?? null,
|
|
496
|
+
workspaceId: session.workspaceId ?? null,
|
|
497
|
+
scopes: session.scopes,
|
|
498
|
+
expiresAt: session.expiresAt
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
8
502
|
// src/remote-encoding.ts
|
|
9
503
|
import { open, readFile as readFile2 } from "fs/promises";
|
|
10
|
-
import { join } from "path";
|
|
504
|
+
import { join as join2 } from "path";
|
|
11
505
|
|
|
12
506
|
// ../../node_modules/zod/v4/classic/external.js
|
|
13
507
|
var external_exports = {};
|
|
@@ -142,7 +636,7 @@ __export(external_exports, {
|
|
|
142
636
|
guid: () => guid2,
|
|
143
637
|
hash: () => hash,
|
|
144
638
|
hex: () => hex2,
|
|
145
|
-
hostname: () =>
|
|
639
|
+
hostname: () => hostname3,
|
|
146
640
|
httpUrl: () => httpUrl,
|
|
147
641
|
includes: () => _includes,
|
|
148
642
|
instanceof: () => _instanceof,
|
|
@@ -1553,7 +2047,7 @@ __export(regexes_exports, {
|
|
|
1553
2047
|
extendedDuration: () => extendedDuration,
|
|
1554
2048
|
guid: () => guid,
|
|
1555
2049
|
hex: () => hex,
|
|
1556
|
-
hostname: () =>
|
|
2050
|
+
hostname: () => hostname2,
|
|
1557
2051
|
html5Email: () => html5Email,
|
|
1558
2052
|
httpProtocol: () => httpProtocol,
|
|
1559
2053
|
idnEmail: () => idnEmail,
|
|
@@ -1631,7 +2125,7 @@ var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]
|
|
|
1631
2125
|
var cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
|
|
1632
2126
|
var base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
|
|
1633
2127
|
var base64url = /^[A-Za-z0-9_-]*$/;
|
|
1634
|
-
var
|
|
2128
|
+
var hostname2 = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
|
|
1635
2129
|
var domain = /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
|
1636
2130
|
var httpProtocol = /^https?$/;
|
|
1637
2131
|
var e164 = /^\+[1-9]\d{6,14}$/;
|
|
@@ -12503,7 +12997,7 @@ __export(schemas_exports2, {
|
|
|
12503
12997
|
guid: () => guid2,
|
|
12504
12998
|
hash: () => hash,
|
|
12505
12999
|
hex: () => hex2,
|
|
12506
|
-
hostname: () =>
|
|
13000
|
+
hostname: () => hostname3,
|
|
12507
13001
|
httpUrl: () => httpUrl,
|
|
12508
13002
|
instanceof: () => _instanceof,
|
|
12509
13003
|
int: () => int,
|
|
@@ -13123,7 +13617,7 @@ var ZodCustomStringFormat = /* @__PURE__ */ $constructor("ZodCustomStringFormat"
|
|
|
13123
13617
|
function stringFormat(format, fnOrRegex, _params = {}) {
|
|
13124
13618
|
return _stringFormat(ZodCustomStringFormat, format, fnOrRegex, _params);
|
|
13125
13619
|
}
|
|
13126
|
-
function
|
|
13620
|
+
function hostname3(_params) {
|
|
13127
13621
|
return _stringFormat(ZodCustomStringFormat, "hostname", regexes_exports.hostname, _params);
|
|
13128
13622
|
}
|
|
13129
13623
|
function hex2(_params) {
|
|
@@ -14464,251 +14958,79 @@ function convertSchema(schema, ctx) {
|
|
|
14464
14958
|
extraMeta[key] = schema[key];
|
|
14465
14959
|
}
|
|
14466
14960
|
}
|
|
14467
|
-
if (Object.keys(extraMeta).length > 0) {
|
|
14468
|
-
ctx.registry.add(baseSchema, extraMeta);
|
|
14469
|
-
}
|
|
14470
|
-
if (schema.description) {
|
|
14471
|
-
baseSchema = baseSchema.describe(schema.description);
|
|
14472
|
-
}
|
|
14473
|
-
return baseSchema;
|
|
14474
|
-
}
|
|
14475
|
-
function fromJSONSchema(schema, params) {
|
|
14476
|
-
if (typeof schema === "boolean") {
|
|
14477
|
-
return schema ? z.any() : z.never();
|
|
14478
|
-
}
|
|
14479
|
-
let normalized;
|
|
14480
|
-
try {
|
|
14481
|
-
normalized = JSON.parse(JSON.stringify(schema));
|
|
14482
|
-
} catch {
|
|
14483
|
-
throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
|
|
14484
|
-
}
|
|
14485
|
-
const version2 = detectVersion(normalized, params?.defaultTarget);
|
|
14486
|
-
const defs = normalized.$defs || normalized.definitions || {};
|
|
14487
|
-
const ctx = {
|
|
14488
|
-
version: version2,
|
|
14489
|
-
defs,
|
|
14490
|
-
refs: /* @__PURE__ */ new Map(),
|
|
14491
|
-
processing: /* @__PURE__ */ new Set(),
|
|
14492
|
-
rootSchema: normalized,
|
|
14493
|
-
registry: params?.registry ?? globalRegistry
|
|
14494
|
-
};
|
|
14495
|
-
return convertSchema(normalized, ctx);
|
|
14496
|
-
}
|
|
14497
|
-
|
|
14498
|
-
// ../../node_modules/zod/v4/classic/coerce.js
|
|
14499
|
-
var coerce_exports = {};
|
|
14500
|
-
__export(coerce_exports, {
|
|
14501
|
-
bigint: () => bigint3,
|
|
14502
|
-
boolean: () => boolean3,
|
|
14503
|
-
date: () => date4,
|
|
14504
|
-
number: () => number3,
|
|
14505
|
-
string: () => string3
|
|
14506
|
-
});
|
|
14507
|
-
function string3(params) {
|
|
14508
|
-
return _coercedString(ZodString, params);
|
|
14509
|
-
}
|
|
14510
|
-
function number3(params) {
|
|
14511
|
-
return _coercedNumber(ZodNumber, params);
|
|
14512
|
-
}
|
|
14513
|
-
function boolean3(params) {
|
|
14514
|
-
return _coercedBoolean(ZodBoolean, params);
|
|
14515
|
-
}
|
|
14516
|
-
function bigint3(params) {
|
|
14517
|
-
return _coercedBigint(ZodBigInt, params);
|
|
14518
|
-
}
|
|
14519
|
-
function date4(params) {
|
|
14520
|
-
return _coercedDate(ZodDate, params);
|
|
14521
|
-
}
|
|
14522
|
-
|
|
14523
|
-
// ../../node_modules/zod/v4/classic/external.js
|
|
14524
|
-
config(en_default());
|
|
14525
|
-
|
|
14526
|
-
// ../contracts/src/encoding.ts
|
|
14527
|
-
var ENCODING_PART_BYTES = 4 * 1024 * 1024;
|
|
14528
|
-
var ENCODING_MAX_BYTES = 1024 * 1024 * 1024;
|
|
14529
|
-
var frameManifestSchema = external_exports.object({
|
|
14530
|
-
version: external_exports.literal(1),
|
|
14531
|
-
durationSeconds: external_exports.number().positive().max(600),
|
|
14532
|
-
width: external_exports.number().int().min(320).max(3840),
|
|
14533
|
-
height: external_exports.number().int().min(240).max(2160),
|
|
14534
|
-
frames: external_exports.array(external_exports.object({ timestamp: external_exports.number().nonnegative().finite(), size: external_exports.number().int().positive().max(ENCODING_PART_BYTES) }).strict()).min(1).max(36e3)
|
|
14535
|
-
}).strict().superRefine((m, ctx) => {
|
|
14536
|
-
if (m.frames.reduce((n, f) => n + f.size, 0) > ENCODING_MAX_BYTES) ctx.addIssue({ code: "custom", message: "Frame upload exceeds the trial byte limit." });
|
|
14537
|
-
for (let i = 1; i < m.frames.length; i++) if (m.frames[i].timestamp < m.frames[i - 1].timestamp) ctx.addIssue({ code: "custom", message: "Frame timestamps must be ordered." });
|
|
14538
|
-
if (m.frames.at(-1).timestamp - m.frames[0].timestamp > m.durationSeconds + 1) ctx.addIssue({ code: "custom", message: "Frame timestamps exceed capture duration." });
|
|
14539
|
-
});
|
|
14540
|
-
|
|
14541
|
-
// src/control-plane.ts
|
|
14542
|
-
import { createReadStream } from "fs";
|
|
14543
|
-
import { readFile, stat } from "fs/promises";
|
|
14544
|
-
import { Upload } from "tus-js-client";
|
|
14545
|
-
var ControlPlaneError = class extends Error {
|
|
14546
|
-
constructor(code, message, recovery, details, httpStatus) {
|
|
14547
|
-
super(`${code}: ${message}${recovery ? `: ${recovery}` : ""}`);
|
|
14548
|
-
this.code = code;
|
|
14549
|
-
this.recovery = recovery;
|
|
14550
|
-
this.details = details;
|
|
14551
|
-
this.httpStatus = httpStatus;
|
|
14552
|
-
this.name = "ControlPlaneError";
|
|
14553
|
-
}
|
|
14554
|
-
};
|
|
14555
|
-
var ControlPlaneHttpError = class extends Error {
|
|
14556
|
-
constructor(httpStatus) {
|
|
14557
|
-
super(`Request failed (${httpStatus}).`);
|
|
14558
|
-
this.httpStatus = httpStatus;
|
|
14559
|
-
this.name = "ControlPlaneHttpError";
|
|
14560
|
-
}
|
|
14561
|
-
};
|
|
14562
|
-
var ControlPlaneClient = class {
|
|
14563
|
-
constructor(apiUrl, token) {
|
|
14564
|
-
this.apiUrl = apiUrl;
|
|
14565
|
-
this.token = token;
|
|
14566
|
-
}
|
|
14567
|
-
encodingCapabilities() {
|
|
14568
|
-
return this.request("/api/encoding/capabilities");
|
|
14569
|
-
}
|
|
14570
|
-
beginEncoding(id, manifest) {
|
|
14571
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}`, { method: "POST", body: JSON.stringify(manifest) });
|
|
14572
|
-
}
|
|
14573
|
-
encodingStatus(id) {
|
|
14574
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}`);
|
|
14575
|
-
}
|
|
14576
|
-
uploadEncodingPart(id, part, bytes) {
|
|
14577
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}/parts/${part}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "Content-Length": String(bytes.length) }, body: new Uint8Array(bytes), signal: AbortSignal.timeout(12e4) });
|
|
14578
|
-
}
|
|
14579
|
-
completeEncoding(id) {
|
|
14580
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}/complete`, { method: "POST" });
|
|
14581
|
-
}
|
|
14582
|
-
retryEncoding(id) {
|
|
14583
|
-
return this.request(`/api/encoding/${encodeURIComponent(id)}/retry`, { method: "POST" });
|
|
14584
|
-
}
|
|
14585
|
-
createCloudRun(input) {
|
|
14586
|
-
return this.request("/api/cloud/runs", { method: "POST", body: JSON.stringify(input) });
|
|
14587
|
-
}
|
|
14588
|
-
cloudRun(id) {
|
|
14589
|
-
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}`);
|
|
14590
|
-
}
|
|
14591
|
-
cancelCloudRun(id) {
|
|
14592
|
-
return this.request(`/api/cloud/runs/${encodeURIComponent(id)}/cancel`, { method: "POST" });
|
|
14593
|
-
}
|
|
14594
|
-
health() {
|
|
14595
|
-
return this.request("/api/health", {}, false);
|
|
14596
|
-
}
|
|
14597
|
-
agentSession() {
|
|
14598
|
-
return this.request("/api/agent/session", { signal: AbortSignal.timeout(5e3) });
|
|
14599
|
-
}
|
|
14600
|
-
reportCliCommand(input) {
|
|
14601
|
-
return this.request("/api/analytics/cli-command", { method: "POST", body: JSON.stringify(input) });
|
|
14602
|
-
}
|
|
14603
|
-
createRecording(input) {
|
|
14604
|
-
return this.request("/api/recordings", { method: "POST", body: JSON.stringify(input) });
|
|
14605
|
-
}
|
|
14606
|
-
markRecordingStarted(recordingId, input) {
|
|
14607
|
-
return this.request(`/api/recordings/${recordingId}/local-started`, {
|
|
14608
|
-
method: "POST",
|
|
14609
|
-
body: JSON.stringify(input)
|
|
14610
|
-
});
|
|
14611
|
-
}
|
|
14612
|
-
markRecordingStopped(recordingId, input) {
|
|
14613
|
-
return this.request(`/api/recordings/${recordingId}/local-stopped`, {
|
|
14614
|
-
method: "POST",
|
|
14615
|
-
body: JSON.stringify(input)
|
|
14616
|
-
});
|
|
14617
|
-
}
|
|
14618
|
-
submitRecordingFeedback(recordingId, input) {
|
|
14619
|
-
return this.request(recordingId ? `/api/recordings/${recordingId}/feedback` : "/api/feedback", {
|
|
14620
|
-
method: "POST",
|
|
14621
|
-
body: JSON.stringify(input)
|
|
14622
|
-
});
|
|
14623
|
-
}
|
|
14624
|
-
provisionVideo(recordingId, input) {
|
|
14625
|
-
return this.request(`/api/recordings/${recordingId}/video-upload`, { method: "POST", body: JSON.stringify(input) });
|
|
14626
|
-
}
|
|
14627
|
-
recordingStatus(recordingId) {
|
|
14628
|
-
return this.request(`/api/recordings/${recordingId}`);
|
|
14629
|
-
}
|
|
14630
|
-
cancelRecording(recordingId, reason = "cancelled_by_agent") {
|
|
14631
|
-
return this.request(`/api/recordings/${recordingId}/cancel`, {
|
|
14632
|
-
method: "POST",
|
|
14633
|
-
body: JSON.stringify({ reason })
|
|
14634
|
-
});
|
|
14635
|
-
}
|
|
14636
|
-
async waitUntilPlayable(recordingId, timeoutMs = 10 * 6e4) {
|
|
14637
|
-
const deadline = Date.now() + timeoutMs;
|
|
14638
|
-
let intervalMs = 1e3;
|
|
14639
|
-
while (Date.now() < deadline) {
|
|
14640
|
-
const recording = await this.recordingStatus(recordingId);
|
|
14641
|
-
if (recording.status === "ready" && recording.shareUrl) {
|
|
14642
|
-
return { ...recording, shareUrl: new URL(recording.shareUrl, this.apiUrl).toString() };
|
|
14643
|
-
}
|
|
14644
|
-
if (recording.status === "failed") throw new Error(`processing_failed: ${recording.failure_message ?? "Cloudflare Stream could not process the recording."}`);
|
|
14645
|
-
process.stderr.write(`\rprocessing ${recording.status.padEnd(12)}`);
|
|
14646
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
14647
|
-
intervalMs = Math.min(5e3, Math.round(intervalMs * 1.4));
|
|
14648
|
-
}
|
|
14649
|
-
throw new Error("processing_failed: Timed out waiting for the recording to become playable. Run `rill record status <recording-id>` to continue checking.");
|
|
14650
|
-
}
|
|
14651
|
-
async uploadVideo(uploadUrl, filePath) {
|
|
14652
|
-
const file2 = await stat(filePath);
|
|
14653
|
-
if (uploadUrl.startsWith(this.apiUrl)) {
|
|
14654
|
-
const response = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Length": String(file2.size) }, body: createReadStream(filePath), duplex: "half" });
|
|
14655
|
-
if (!response.ok) throw new Error(`Video upload failed (${response.status}).`);
|
|
14656
|
-
return;
|
|
14657
|
-
}
|
|
14658
|
-
await new Promise((resolve, reject) => {
|
|
14659
|
-
const upload = new Upload(createReadStream(filePath), {
|
|
14660
|
-
uploadUrl,
|
|
14661
|
-
uploadSize: file2.size,
|
|
14662
|
-
retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
|
|
14663
|
-
removeFingerprintOnSuccess: true,
|
|
14664
|
-
onError: reject,
|
|
14665
|
-
onSuccess: () => resolve(),
|
|
14666
|
-
onProgress: (sent, total) => process.stderr.write(`\ruploading ${Math.round(sent / total * 100)}%`)
|
|
14667
|
-
});
|
|
14668
|
-
upload.start();
|
|
14669
|
-
});
|
|
14670
|
-
process.stderr.write("\n");
|
|
14671
|
-
}
|
|
14672
|
-
async uploadDiagnostics(recordingId, filePath, summary) {
|
|
14673
|
-
const body = await readFile(filePath);
|
|
14674
|
-
return this.request(`/api/recordings/${recordingId}/diagnostics`, { method: "PUT", headers: { "Content-Encoding": "gzip", "Content-Type": "application/x-ndjson", "Content-Length": String(body.length), "X-Rill-Summary": JSON.stringify(summary) }, body });
|
|
14675
|
-
}
|
|
14676
|
-
listRecordings() {
|
|
14677
|
-
return this.request("/api/recordings");
|
|
14678
|
-
}
|
|
14679
|
-
createDeviceAuthorization(input) {
|
|
14680
|
-
return this.request("/api/device/authorizations", { method: "POST", body: JSON.stringify(input) }, false);
|
|
14681
|
-
}
|
|
14682
|
-
pollDeviceAuthorization(deviceCode) {
|
|
14683
|
-
return this.request("/api/device/token", { method: "POST", body: JSON.stringify({ deviceCode }) }, false);
|
|
14684
|
-
}
|
|
14685
|
-
inspect(shareUrl) {
|
|
14686
|
-
if (/^rec_[a-zA-Z0-9_-]+$/.test(shareUrl)) return this.request(`/api/recordings/${encodeURIComponent(shareUrl)}`);
|
|
14687
|
-
const url2 = new URL(shareUrl);
|
|
14688
|
-
const slug = url2.pathname.split("/").filter(Boolean).at(-1);
|
|
14689
|
-
if (!slug) throw new Error("The share URL does not contain a slug.");
|
|
14690
|
-
return fetch(`${url2.origin}/api/public/shares/${encodeURIComponent(slug)}/context`, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
14691
|
-
if (!response.ok) throw new Error(`Could not inspect the share link (${response.status}).`);
|
|
14692
|
-
return response.json();
|
|
14693
|
-
});
|
|
14694
|
-
}
|
|
14695
|
-
async request(path, init = {}, authenticated = true) {
|
|
14696
|
-
const headers = new Headers(init.headers);
|
|
14697
|
-
if (init.body && !headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
14698
|
-
if (authenticated) {
|
|
14699
|
-
const token = typeof this.token === "function" ? await this.token() : this.token;
|
|
14700
|
-
if (!token) throw new Error("authentication_required: Run `rill login` or set RILL_TOKEN.");
|
|
14701
|
-
headers.set("Authorization", `Bearer ${token}`);
|
|
14702
|
-
}
|
|
14703
|
-
const response = await fetch(`${this.apiUrl}${path}`, { ...init, headers });
|
|
14704
|
-
const value = await response.json().catch(() => null);
|
|
14705
|
-
if (!response.ok && value?.error?.code && value.error.message) {
|
|
14706
|
-
throw new ControlPlaneError(value.error.code, value.error.message, value.error.recovery, value.error.details, response.status);
|
|
14707
|
-
}
|
|
14708
|
-
if (!response.ok) throw new ControlPlaneHttpError(response.status);
|
|
14709
|
-
return value;
|
|
14961
|
+
if (Object.keys(extraMeta).length > 0) {
|
|
14962
|
+
ctx.registry.add(baseSchema, extraMeta);
|
|
14710
14963
|
}
|
|
14711
|
-
|
|
14964
|
+
if (schema.description) {
|
|
14965
|
+
baseSchema = baseSchema.describe(schema.description);
|
|
14966
|
+
}
|
|
14967
|
+
return baseSchema;
|
|
14968
|
+
}
|
|
14969
|
+
function fromJSONSchema(schema, params) {
|
|
14970
|
+
if (typeof schema === "boolean") {
|
|
14971
|
+
return schema ? z.any() : z.never();
|
|
14972
|
+
}
|
|
14973
|
+
let normalized;
|
|
14974
|
+
try {
|
|
14975
|
+
normalized = JSON.parse(JSON.stringify(schema));
|
|
14976
|
+
} catch {
|
|
14977
|
+
throw new Error("fromJSONSchema input is not valid JSON (possibly cyclic); use $defs/$ref for recursive schemas");
|
|
14978
|
+
}
|
|
14979
|
+
const version2 = detectVersion(normalized, params?.defaultTarget);
|
|
14980
|
+
const defs = normalized.$defs || normalized.definitions || {};
|
|
14981
|
+
const ctx = {
|
|
14982
|
+
version: version2,
|
|
14983
|
+
defs,
|
|
14984
|
+
refs: /* @__PURE__ */ new Map(),
|
|
14985
|
+
processing: /* @__PURE__ */ new Set(),
|
|
14986
|
+
rootSchema: normalized,
|
|
14987
|
+
registry: params?.registry ?? globalRegistry
|
|
14988
|
+
};
|
|
14989
|
+
return convertSchema(normalized, ctx);
|
|
14990
|
+
}
|
|
14991
|
+
|
|
14992
|
+
// ../../node_modules/zod/v4/classic/coerce.js
|
|
14993
|
+
var coerce_exports = {};
|
|
14994
|
+
__export(coerce_exports, {
|
|
14995
|
+
bigint: () => bigint3,
|
|
14996
|
+
boolean: () => boolean3,
|
|
14997
|
+
date: () => date4,
|
|
14998
|
+
number: () => number3,
|
|
14999
|
+
string: () => string3
|
|
15000
|
+
});
|
|
15001
|
+
function string3(params) {
|
|
15002
|
+
return _coercedString(ZodString, params);
|
|
15003
|
+
}
|
|
15004
|
+
function number3(params) {
|
|
15005
|
+
return _coercedNumber(ZodNumber, params);
|
|
15006
|
+
}
|
|
15007
|
+
function boolean3(params) {
|
|
15008
|
+
return _coercedBoolean(ZodBoolean, params);
|
|
15009
|
+
}
|
|
15010
|
+
function bigint3(params) {
|
|
15011
|
+
return _coercedBigint(ZodBigInt, params);
|
|
15012
|
+
}
|
|
15013
|
+
function date4(params) {
|
|
15014
|
+
return _coercedDate(ZodDate, params);
|
|
15015
|
+
}
|
|
15016
|
+
|
|
15017
|
+
// ../../node_modules/zod/v4/classic/external.js
|
|
15018
|
+
config(en_default());
|
|
15019
|
+
|
|
15020
|
+
// ../contracts/src/encoding.ts
|
|
15021
|
+
var ENCODING_PART_BYTES = 4 * 1024 * 1024;
|
|
15022
|
+
var ENCODING_MAX_BYTES = 1024 * 1024 * 1024;
|
|
15023
|
+
var frameManifestSchema = external_exports.object({
|
|
15024
|
+
version: external_exports.literal(1),
|
|
15025
|
+
durationSeconds: external_exports.number().positive().max(600),
|
|
15026
|
+
width: external_exports.number().int().min(320).max(3840),
|
|
15027
|
+
height: external_exports.number().int().min(240).max(2160),
|
|
15028
|
+
frames: external_exports.array(external_exports.object({ timestamp: external_exports.number().nonnegative().finite(), size: external_exports.number().int().positive().max(ENCODING_PART_BYTES) }).strict()).min(1).max(36e3)
|
|
15029
|
+
}).strict().superRefine((m, ctx) => {
|
|
15030
|
+
if (m.frames.reduce((n, f) => n + f.size, 0) > ENCODING_MAX_BYTES) ctx.addIssue({ code: "custom", message: "Frame upload exceeds the trial byte limit." });
|
|
15031
|
+
for (let i = 1; i < m.frames.length; i++) if (m.frames[i].timestamp < m.frames[i - 1].timestamp) ctx.addIssue({ code: "custom", message: "Frame timestamps must be ordered." });
|
|
15032
|
+
if (m.frames.at(-1).timestamp - m.frames[0].timestamp > m.durationSeconds + 1) ctx.addIssue({ code: "custom", message: "Frame timestamps exceed capture duration." });
|
|
15033
|
+
});
|
|
14712
15034
|
|
|
14713
15035
|
// src/remote-encoding.ts
|
|
14714
15036
|
async function withFrameUploadRetry(upload, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
@@ -14728,14 +15050,14 @@ Frame upload interrupted; retrying in ${delay / 1e3}s.
|
|
|
14728
15050
|
}
|
|
14729
15051
|
}
|
|
14730
15052
|
async function submitFrameCapture(client, id, directory) {
|
|
14731
|
-
const manifest = frameManifestSchema.parse(JSON.parse(await readFile2(
|
|
15053
|
+
const manifest = frameManifestSchema.parse(JSON.parse(await readFile2(join2(directory, "capture-manifest.json"), "utf8")));
|
|
14732
15054
|
let status = await client.beginEncoding(id, manifest);
|
|
14733
15055
|
if (status.status === "failed") status = await client.retryEncoding(id);
|
|
14734
15056
|
if (status.status !== "uploading") {
|
|
14735
15057
|
if (["expired", "cancelled"].includes(status.status)) throw new Error(`Capture ${status.status}; it cannot be retried.`);
|
|
14736
15058
|
return status;
|
|
14737
15059
|
}
|
|
14738
|
-
const file2 = await open(
|
|
15060
|
+
const file2 = await open(join2(directory, "frames.bin"), "r");
|
|
14739
15061
|
try {
|
|
14740
15062
|
const total = manifest.frames.reduce((n, f) => n + f.size, 0);
|
|
14741
15063
|
if ((await file2.stat()).size !== total) throw new Error("Capture bytes do not match the manifest.");
|
|
@@ -14755,6 +15077,7 @@ async function submitFrameCapture(client, id, directory) {
|
|
|
14755
15077
|
} finally {
|
|
14756
15078
|
await file2.close();
|
|
14757
15079
|
}
|
|
15080
|
+
process.stderr.write("\nFrame upload complete; submitting for encoding.\n");
|
|
14758
15081
|
return client.completeEncoding(id);
|
|
14759
15082
|
}
|
|
14760
15083
|
async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
|
|
@@ -14766,7 +15089,7 @@ async function waitForEncoding(client, id, timeoutMs = 30 * 6e4) {
|
|
|
14766
15089
|
process.stderr.write(`\r${status.status.padEnd(20)}`);
|
|
14767
15090
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
14768
15091
|
}
|
|
14769
|
-
throw new
|
|
15092
|
+
throw new ControlPlaneError("processing_pending", "Encoding continues remotely.", `Run ${client.recordingStatusCommand(id)}.`, { recordingId: id, nextCommand: client.recordingStatusCommand(id) });
|
|
14770
15093
|
}
|
|
14771
15094
|
|
|
14772
15095
|
// src/cli.ts
|
|
@@ -14777,16 +15100,16 @@ import { basename, dirname as dirname2 } from "path";
|
|
|
14777
15100
|
import { readdir as readdir2, rm as rm5, stat as stat4 } from "fs/promises";
|
|
14778
15101
|
|
|
14779
15102
|
// ../recorder/src/client.ts
|
|
14780
|
-
import { request } from "http";
|
|
14781
|
-
import { spawn } from "child_process";
|
|
15103
|
+
import { request as request2 } from "http";
|
|
15104
|
+
import { spawn as spawn2 } from "child_process";
|
|
14782
15105
|
|
|
14783
15106
|
// ../recorder/src/paths.ts
|
|
14784
|
-
import { homedir, tmpdir } from "os";
|
|
14785
|
-
import { join as
|
|
14786
|
-
var rillDirectory = () => process.env.RILL_HOME ??
|
|
14787
|
-
var daemonSocketPath = () => process.env.RILL_SOCKET ??
|
|
14788
|
-
var daemonStatePath = () =>
|
|
14789
|
-
var recordingTempRoot = () => process.env.RILL_TEMP ??
|
|
15107
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
15108
|
+
import { join as join3 } from "path";
|
|
15109
|
+
var rillDirectory = () => process.env.RILL_HOME ?? join3(homedir2(), ".rill");
|
|
15110
|
+
var daemonSocketPath = () => process.env.RILL_SOCKET ?? join3(rillDirectory(), "recorder-v2.sock");
|
|
15111
|
+
var daemonStatePath = () => join3(rillDirectory(), "sessions-v2.json");
|
|
15112
|
+
var recordingTempRoot = () => process.env.RILL_TEMP ?? join3(tmpdir(), "rill-recordings");
|
|
14790
15113
|
|
|
14791
15114
|
// ../recorder/src/client.ts
|
|
14792
15115
|
async function ensureDaemon(cliEntryPath) {
|
|
@@ -14795,7 +15118,7 @@ async function ensureDaemon(cliEntryPath) {
|
|
|
14795
15118
|
return;
|
|
14796
15119
|
} catch {
|
|
14797
15120
|
}
|
|
14798
|
-
const child =
|
|
15121
|
+
const child = spawn2(process.execPath, [cliEntryPath, "daemon"], { detached: true, stdio: "ignore", env: process.env });
|
|
14799
15122
|
child.unref();
|
|
14800
15123
|
const deadline = Date.now() + 1e4;
|
|
14801
15124
|
while (Date.now() < deadline) {
|
|
@@ -14820,7 +15143,7 @@ function recordingStatus(recordingId) {
|
|
|
14820
15143
|
function callDaemon(method, path, body) {
|
|
14821
15144
|
return new Promise((resolve, reject) => {
|
|
14822
15145
|
const payload = body === void 0 ? void 0 : Buffer.from(JSON.stringify(body));
|
|
14823
|
-
const outgoing =
|
|
15146
|
+
const outgoing = request2({ socketPath: daemonSocketPath(), method, path, headers: payload ? { "Content-Type": "application/json", "Content-Length": payload.length } : void 0 }, (incoming) => {
|
|
14824
15147
|
const chunks = [];
|
|
14825
15148
|
incoming.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
14826
15149
|
incoming.on("end", () => {
|
|
@@ -14840,17 +15163,17 @@ function callDaemon(method, path, body) {
|
|
|
14840
15163
|
}
|
|
14841
15164
|
|
|
14842
15165
|
// ../recorder/src/retention.ts
|
|
14843
|
-
import { readdir, readFile as readFile3, rm } from "fs/promises";
|
|
14844
|
-
import { join as
|
|
15166
|
+
import { readdir, readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
15167
|
+
import { join as join4 } from "path";
|
|
14845
15168
|
async function purgeExpiredRemoteCaptures(root, now = Date.now()) {
|
|
14846
15169
|
const removed = [];
|
|
14847
15170
|
for (const id of await readdir(root).catch(() => [])) {
|
|
14848
15171
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
14849
|
-
const directory =
|
|
15172
|
+
const directory = join4(root, id);
|
|
14850
15173
|
try {
|
|
14851
|
-
const marker = JSON.parse(await readFile3(
|
|
15174
|
+
const marker = JSON.parse(await readFile3(join4(directory, "remote-capture.json"), "utf8"));
|
|
14852
15175
|
if (typeof marker.expiresAt !== "number" || marker.expiresAt > now) continue;
|
|
14853
|
-
await
|
|
15176
|
+
await rm2(directory, { recursive: true, force: true });
|
|
14854
15177
|
removed.push(id);
|
|
14855
15178
|
} catch {
|
|
14856
15179
|
}
|
|
@@ -14861,7 +15184,7 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
14861
15184
|
for (const id of await readdir(root).catch(() => [])) {
|
|
14862
15185
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(id)) continue;
|
|
14863
15186
|
try {
|
|
14864
|
-
const marker = JSON.parse(await readFile3(
|
|
15187
|
+
const marker = JSON.parse(await readFile3(join4(root, id, "remote-capture.json"), "utf8"));
|
|
14865
15188
|
if (typeof marker.expiresAt === "number" && Number.isFinite(marker.expiresAt)) return true;
|
|
14866
15189
|
} catch {
|
|
14867
15190
|
}
|
|
@@ -14871,8 +15194,8 @@ async function hasRetainedRemoteCaptures(root) {
|
|
|
14871
15194
|
|
|
14872
15195
|
// ../recorder/src/daemon.ts
|
|
14873
15196
|
import { createServer as createServer2 } from "http";
|
|
14874
|
-
import { mkdir as
|
|
14875
|
-
import { dirname, join as
|
|
15197
|
+
import { mkdir as mkdir5, readFile as readFile5, unlink, writeFile as writeFile4, rename, stat as stat3 } from "fs/promises";
|
|
15198
|
+
import { dirname, join as join8 } from "path";
|
|
14876
15199
|
|
|
14877
15200
|
// ../contracts/src/recording.ts
|
|
14878
15201
|
var cursorAppearanceSchema = external_exports.object({
|
|
@@ -15033,10 +15356,10 @@ async function attachCursorOverlay(page, cdp, input) {
|
|
|
15033
15356
|
|
|
15034
15357
|
// ../recorder/src/session.ts
|
|
15035
15358
|
import { chromium } from "playwright-core";
|
|
15036
|
-
import { spawn as
|
|
15359
|
+
import { spawn as spawn4 } from "child_process";
|
|
15037
15360
|
import { createServer } from "net";
|
|
15038
|
-
import { mkdir as
|
|
15039
|
-
import { join as
|
|
15361
|
+
import { mkdir as mkdir4, mkdtemp, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
15362
|
+
import { join as join7 } from "path";
|
|
15040
15363
|
import { tmpdir as tmpdir2 } from "os";
|
|
15041
15364
|
|
|
15042
15365
|
// ../recorder/src/bundle.ts
|
|
@@ -15116,9 +15439,9 @@ function redactJson(value, depth = 0) {
|
|
|
15116
15439
|
}
|
|
15117
15440
|
|
|
15118
15441
|
// ../recorder/src/encoding.ts
|
|
15119
|
-
import { open as open2, writeFile, mkdir } from "fs/promises";
|
|
15120
|
-
import { spawn as
|
|
15121
|
-
import { join as
|
|
15442
|
+
import { open as open2, writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
15443
|
+
import { spawn as spawn3 } from "child_process";
|
|
15444
|
+
import { join as join5 } from "path";
|
|
15122
15445
|
var active = 0;
|
|
15123
15446
|
var waiting = [];
|
|
15124
15447
|
function encodingConcurrency() {
|
|
@@ -15148,11 +15471,11 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15148
15471
|
lines.push(`file 'frame-${String(i).padStart(6, "0")}.jpg'`, `duration ${Math.max(1 / 60, duration3).toFixed(6)}`);
|
|
15149
15472
|
}
|
|
15150
15473
|
lines.push(`file 'frame-${String(manifest.frames.length - 1).padStart(6, "0")}.jpg'`);
|
|
15151
|
-
await writeFile(
|
|
15474
|
+
await writeFile(join5(directory, "frames.ffconcat"), lines.join("\n") + "\n", { mode: 384 });
|
|
15152
15475
|
const threads = Number(process.env.RILL_FFMPEG_THREADS ?? 1);
|
|
15153
15476
|
if (!Number.isInteger(threads) || threads < 1 || threads > 16) throw new Error("Invalid RILL_FFMPEG_THREADS");
|
|
15154
15477
|
await new Promise((resolve, reject) => {
|
|
15155
|
-
const child =
|
|
15478
|
+
const child = spawn3(process.env.RILL_FFMPEG ?? "ffmpeg", ["-hide_banner", "-loglevel", "error", "-protocol_whitelist", "file", "-f", "concat", "-safe", "1", "-i", "frames.ffconcat", "-vsync", "vfr", "-pix_fmt", "yuv420p", "-c:v", "libvpx-vp9", "-deadline", "good", "-cpu-used", "4", "-threads", String(threads), "-b:v", "0", "-crf", "32", "-y", output], { cwd: directory, stdio: ["ignore", "ignore", "pipe"] });
|
|
15156
15479
|
let stderr = "";
|
|
15157
15480
|
child.stderr.on("data", (chunk) => {
|
|
15158
15481
|
stderr = (stderr + String(chunk)).slice(-2e3);
|
|
@@ -15177,8 +15500,8 @@ async function encodeFrames(directory, manifest, output, signal) {
|
|
|
15177
15500
|
}
|
|
15178
15501
|
|
|
15179
15502
|
// ../recorder/src/screencast.ts
|
|
15180
|
-
import { mkdir as
|
|
15181
|
-
import { join as
|
|
15503
|
+
import { mkdir as mkdir3, writeFile as writeFile2, readFile as readFile4, open as open3 } from "fs/promises";
|
|
15504
|
+
import { join as join6 } from "path";
|
|
15182
15505
|
var ScreencastRecorder = class {
|
|
15183
15506
|
constructor(session, directory, width, height) {
|
|
15184
15507
|
this.session = session;
|
|
@@ -15193,7 +15516,7 @@ var ScreencastRecorder = class {
|
|
|
15193
15516
|
frameBytes = 0;
|
|
15194
15517
|
captureError;
|
|
15195
15518
|
async start() {
|
|
15196
|
-
await
|
|
15519
|
+
await mkdir3(this.directory, { recursive: true, mode: 448 });
|
|
15197
15520
|
this.session.on("Page.screencastFrame", (payload) => {
|
|
15198
15521
|
const bytes = Buffer.from(payload.data, "base64");
|
|
15199
15522
|
if (this.frameCounter >= 36e3 || this.frameBytes + bytes.length > ENCODING_MAX_BYTES || bytes.length > ENCODING_PART_BYTES) {
|
|
@@ -15202,7 +15525,7 @@ var ScreencastRecorder = class {
|
|
|
15202
15525
|
return;
|
|
15203
15526
|
}
|
|
15204
15527
|
this.frameBytes += bytes.length;
|
|
15205
|
-
const path =
|
|
15528
|
+
const path = join6(this.directory, `frame-${String(this.frameCounter).padStart(6, "0")}.jpg`);
|
|
15206
15529
|
this.frameCounter += 1;
|
|
15207
15530
|
const write = writeFile2(path, bytes, { mode: 384 }).then(() => {
|
|
15208
15531
|
this.frames.push({ path, timestamp: payload.metadata.timestamp ?? Date.now() / 1e3 });
|
|
@@ -15226,7 +15549,7 @@ var ScreencastRecorder = class {
|
|
|
15226
15549
|
if (!this.frames.length) throw new Error("No video frames were captured.");
|
|
15227
15550
|
const ordered = this.frames.toSorted((a, b) => a.timestamp - b.timestamp);
|
|
15228
15551
|
const frames = [];
|
|
15229
|
-
const bundle = await open3(
|
|
15552
|
+
const bundle = await open3(join6(this.directory, "frames.bin"), "w", 384);
|
|
15230
15553
|
try {
|
|
15231
15554
|
for (const frame of ordered) {
|
|
15232
15555
|
const bytes = await readFile4(frame.path);
|
|
@@ -15237,7 +15560,7 @@ var ScreencastRecorder = class {
|
|
|
15237
15560
|
await bundle.close();
|
|
15238
15561
|
}
|
|
15239
15562
|
const manifest = frameManifestSchema.parse({ version: 1, durationSeconds: Math.min(600, expectedDurationSeconds), width: this.width, height: this.height, frames });
|
|
15240
|
-
await writeFile2(
|
|
15563
|
+
await writeFile2(join6(this.directory, "capture-manifest.json"), JSON.stringify(manifest), { mode: 384 });
|
|
15241
15564
|
if (!captureOnly) await encodeFrames(this.directory, manifest, outputPath);
|
|
15242
15565
|
return { frameCount: this.frames.length, outputPath };
|
|
15243
15566
|
}
|
|
@@ -15286,11 +15609,11 @@ var RecordingSession = class _RecordingSession {
|
|
|
15286
15609
|
static async start(options) {
|
|
15287
15610
|
const width = options.width ?? 1280;
|
|
15288
15611
|
const height = options.height ?? 720;
|
|
15289
|
-
await
|
|
15290
|
-
if (options.encoding === "remote") await writeFile3(
|
|
15612
|
+
await mkdir4(options.outputDirectory, { recursive: true, mode: 448 });
|
|
15613
|
+
if (options.encoding === "remote") await writeFile3(join7(options.outputDirectory, "remote-capture.json"), JSON.stringify({ expiresAt: Date.now() + 864e5 }), { mode: 384 });
|
|
15291
15614
|
const runtime = options.cdpUrl ? await connectAttached(options.cdpUrl, options.outputDirectory) : await launchManaged({ headed: options.headed ?? false, width, height, outputDirectory: options.outputDirectory, proxyServer: options.proxyServer });
|
|
15292
15615
|
const cdpSession = await runtime.context.newCDPSession(runtime.page);
|
|
15293
|
-
const screencast = new ScreencastRecorder(cdpSession,
|
|
15616
|
+
const screencast = new ScreencastRecorder(cdpSession, join7(options.outputDirectory, "frames"), width, height);
|
|
15294
15617
|
const session = new _RecordingSession(options, { ...runtime, screencast });
|
|
15295
15618
|
try {
|
|
15296
15619
|
await session.attachDiagnostics(options.captureNetworkBodies ?? false);
|
|
@@ -15322,8 +15645,8 @@ var RecordingSession = class _RecordingSession {
|
|
|
15322
15645
|
if (this.maxTimer) clearTimeout(this.maxTimer);
|
|
15323
15646
|
const durationSeconds = Math.max(0.1, (Date.now() - this.startedAt.getTime()) / 1e3);
|
|
15324
15647
|
const outputDirectory = this.outputDirectory;
|
|
15325
|
-
const videoPath =
|
|
15326
|
-
const diagnosticsPath =
|
|
15648
|
+
const videoPath = join7(outputDirectory, "recording.webm");
|
|
15649
|
+
const diagnosticsPath = join7(outputDirectory, "diagnostics-v1.ndjson.gz");
|
|
15327
15650
|
const bundle = {
|
|
15328
15651
|
schemaVersion: 1,
|
|
15329
15652
|
capturePolicyVersion: "safe-default-v1",
|
|
@@ -15346,13 +15669,13 @@ var RecordingSession = class _RecordingSession {
|
|
|
15346
15669
|
if (this.browserProcess) {
|
|
15347
15670
|
await this.browser.close().catch(() => void 0);
|
|
15348
15671
|
await terminateBrowser(this.browserProcess);
|
|
15349
|
-
if (this.profileDirectory) await
|
|
15672
|
+
if (this.profileDirectory) await rm3(this.profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15350
15673
|
}
|
|
15351
15674
|
}
|
|
15352
15675
|
const consoleErrors = this.consoleEvents.filter((event) => event.severity === "error").length;
|
|
15353
15676
|
const failedRequests = this.networkEvents.filter((event) => event.failureReason).length;
|
|
15354
15677
|
const httpErrors = this.networkEvents.filter((event) => (event.status ?? 0) >= 400).length;
|
|
15355
|
-
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory:
|
|
15678
|
+
return { recordingId: this.recordingId, videoPath, ...this.encoding === "remote" ? { frameDirectory: join7(outputDirectory, "frames") } : {}, diagnosticsPath, durationSeconds, stoppedReason: reason, diagnostics: { consoleErrors, failedRequests, httpErrors, truncated: this.truncated, bodyCaptureEnabled: this.captureNetworkBodies } };
|
|
15356
15679
|
}
|
|
15357
15680
|
captureNetworkBodies = false;
|
|
15358
15681
|
encoding = "local";
|
|
@@ -15464,7 +15787,7 @@ var RecordingSession = class _RecordingSession {
|
|
|
15464
15787
|
};
|
|
15465
15788
|
async function launchManaged(options) {
|
|
15466
15789
|
const port = await getAvailablePort();
|
|
15467
|
-
const profileDirectory = await mkdtemp(
|
|
15790
|
+
const profileDirectory = await mkdtemp(join7(tmpdir2(), "rill-chrome-"));
|
|
15468
15791
|
const executable = findChromeExecutable();
|
|
15469
15792
|
const args = [
|
|
15470
15793
|
`--remote-debugging-port=${port}`,
|
|
@@ -15482,7 +15805,7 @@ async function launchManaged(options) {
|
|
|
15482
15805
|
"about:blank"
|
|
15483
15806
|
];
|
|
15484
15807
|
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;
|
|
15485
|
-
const browserProcess =
|
|
15808
|
+
const browserProcess = spawn4(executable, args, { stdio: ["ignore", "ignore", "pipe"], detached: false, env });
|
|
15486
15809
|
const cdpUrl = `http://127.0.0.1:${port}`;
|
|
15487
15810
|
try {
|
|
15488
15811
|
await waitForCdp(cdpUrl, browserProcess);
|
|
@@ -15493,7 +15816,7 @@ async function launchManaged(options) {
|
|
|
15493
15816
|
return { cdpUrl, browser, context, page, browserProcess, profileDirectory };
|
|
15494
15817
|
} catch (error51) {
|
|
15495
15818
|
await terminateBrowser(browserProcess);
|
|
15496
|
-
await
|
|
15819
|
+
await rm3(profileDirectory, { recursive: true, force: true }).catch(() => void 0);
|
|
15497
15820
|
throw error51;
|
|
15498
15821
|
}
|
|
15499
15822
|
}
|
|
@@ -15544,8 +15867,8 @@ var sessions = /* @__PURE__ */ new Map();
|
|
|
15544
15867
|
var lastActivity = Date.now();
|
|
15545
15868
|
async function runDaemon() {
|
|
15546
15869
|
const socketPath = daemonSocketPath();
|
|
15547
|
-
await
|
|
15548
|
-
await
|
|
15870
|
+
await mkdir5(dirname(socketPath), { recursive: true, mode: 448 });
|
|
15871
|
+
await mkdir5(recordingTempRoot(), { recursive: true, mode: 448 });
|
|
15549
15872
|
await restoreRecoverableSessions();
|
|
15550
15873
|
await purgeExpiredCaptures();
|
|
15551
15874
|
await unlink(socketPath).catch((error51) => {
|
|
@@ -15579,7 +15902,7 @@ async function route(request3, response) {
|
|
|
15579
15902
|
try {
|
|
15580
15903
|
const input = await readJson(request3);
|
|
15581
15904
|
if (!/^rec_[a-zA-Z0-9_-]+$/.test(input.recordingId)) return json2(response, 400, { error: { code: "validation_failed", message: "recordingId is required." } });
|
|
15582
|
-
const outputDirectory =
|
|
15905
|
+
const outputDirectory = join8(recordingTempRoot(), input.recordingId);
|
|
15583
15906
|
const session = await RecordingSession.start({ ...input, outputDirectory });
|
|
15584
15907
|
const status = session.status();
|
|
15585
15908
|
const entry = { session, base: { recordingId: status.recordingId, cdpUrl: status.cdpUrl, startedAt: status.startedAt }, state: "recording" };
|
|
@@ -15657,7 +15980,7 @@ async function purgeExpiredCaptures() {
|
|
|
15657
15980
|
const removed = await purgeExpiredRemoteCaptures(recordingTempRoot());
|
|
15658
15981
|
for (const id of removed) sessions.delete(id);
|
|
15659
15982
|
let changed = removed.length > 0;
|
|
15660
|
-
for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await
|
|
15983
|
+
for (const [id, entry] of sessions) if (entry.result?.frameDirectory && !await stat3(entry.result.frameDirectory).catch(() => null)) {
|
|
15661
15984
|
sessions.delete(id);
|
|
15662
15985
|
changed = true;
|
|
15663
15986
|
}
|
|
@@ -15694,196 +16017,6 @@ function cliFailureCode(error51) {
|
|
|
15694
16017
|
return message.match(/^([a-z][a-z0-9_]{0,79}):/)?.[1] ?? "unexpected_error";
|
|
15695
16018
|
}
|
|
15696
16019
|
|
|
15697
|
-
// src/agent-session.ts
|
|
15698
|
-
import { hostname as hostname3, homedir as homedir2 } from "os";
|
|
15699
|
-
import { mkdir as mkdir5, rm as rm4, stat as stat3 } from "fs/promises";
|
|
15700
|
-
import { join as join8 } from "path";
|
|
15701
|
-
import { createHash, randomBytes } from "crypto";
|
|
15702
|
-
var JWT_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
15703
|
-
var CLAIM_GRANT = "urn:workos:agent-auth:grant-type:claim";
|
|
15704
|
-
var AgentAuthError = class extends Error {
|
|
15705
|
-
constructor(code, message) {
|
|
15706
|
-
super(`${code}: ${message}`);
|
|
15707
|
-
this.code = code;
|
|
15708
|
-
}
|
|
15709
|
-
};
|
|
15710
|
-
function parseAuthorization(value) {
|
|
15711
|
-
if (!value?.startsWith("{")) return null;
|
|
15712
|
-
const saved = JSON.parse(value);
|
|
15713
|
-
if (saved.version !== 1 || typeof saved.anonymous !== "boolean") throw new Error("Saved Rill authorization is invalid. Run rill connect --email <your-email>.");
|
|
15714
|
-
return saved;
|
|
15715
|
-
}
|
|
15716
|
-
async function request2(apiUrl, path, body, form = false) {
|
|
15717
|
-
const url2 = new URL(apiUrl);
|
|
15718
|
-
if (url2.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url2.hostname)) throw new Error("Agent authentication requires HTTPS.");
|
|
15719
|
-
const response = await fetch(`${apiUrl}${path}`, {
|
|
15720
|
-
method: "POST",
|
|
15721
|
-
redirect: "error",
|
|
15722
|
-
signal: AbortSignal.timeout(15e3),
|
|
15723
|
-
headers: { "Content-Type": form ? "application/x-www-form-urlencoded" : "application/json" },
|
|
15724
|
-
body: form ? new URLSearchParams(Object.entries(body).map(([key, value2]) => [key, String(value2)])) : JSON.stringify(body)
|
|
15725
|
-
});
|
|
15726
|
-
const value = await response.json();
|
|
15727
|
-
if (!response.ok) throw new AgentAuthError(value.error ?? "authentication_failed", value.error_description ?? "Could not authorize Rill.");
|
|
15728
|
-
return value;
|
|
15729
|
-
}
|
|
15730
|
-
function withTokens(saved, tokens) {
|
|
15731
|
-
return {
|
|
15732
|
-
...saved,
|
|
15733
|
-
accessToken: tokens.access_token,
|
|
15734
|
-
accessExpiresAt: Date.now() + tokens.expires_in * 1e3,
|
|
15735
|
-
...tokens.identity_assertion ? { identityAssertion: tokens.identity_assertion, assertionExpires: tokens.assertion_expires } : {}
|
|
15736
|
-
};
|
|
15737
|
-
}
|
|
15738
|
-
var save = (apiUrl, value) => saveCredential(apiUrl, JSON.stringify(value));
|
|
15739
|
-
var pending = /* @__PURE__ */ new Map();
|
|
15740
|
-
async function loadAgentCredential(apiUrl) {
|
|
15741
|
-
if (process.env.RILL_TOKEN) return process.env.RILL_TOKEN;
|
|
15742
|
-
const inFlight = pending.get(apiUrl);
|
|
15743
|
-
if (inFlight) return inFlight;
|
|
15744
|
-
const promise2 = (async () => {
|
|
15745
|
-
const raw = await loadStoredCredential(apiUrl);
|
|
15746
|
-
const saved = parseAuthorization(raw);
|
|
15747
|
-
if (!saved) return raw;
|
|
15748
|
-
if (saved.accessToken && (saved.accessExpiresAt ?? 0) > Date.now() + 6e4) return saved.accessToken;
|
|
15749
|
-
if (!saved.identityAssertion && saved.anonymous && saved.registrationToken) return null;
|
|
15750
|
-
if (!saved.identityAssertion) throw new Error("Connection is awaiting approval. Run rill connect --email <your-email>.");
|
|
15751
|
-
const tokens = await request2(apiUrl, "/oauth2/token", { grant_type: JWT_GRANT, assertion: saved.identityAssertion }, true);
|
|
15752
|
-
await save(apiUrl, withTokens(saved, tokens));
|
|
15753
|
-
return tokens.access_token;
|
|
15754
|
-
})();
|
|
15755
|
-
pending.set(apiUrl, promise2);
|
|
15756
|
-
try {
|
|
15757
|
-
return await promise2;
|
|
15758
|
-
} finally {
|
|
15759
|
-
pending.delete(apiUrl);
|
|
15760
|
-
}
|
|
15761
|
-
}
|
|
15762
|
-
async function ensureAnonymousCredential(apiUrl) {
|
|
15763
|
-
const root = join8(homedir2(), ".rill", "auth-locks");
|
|
15764
|
-
await mkdir5(root, { recursive: true, mode: 448 });
|
|
15765
|
-
const lock = join8(root, createHash("sha256").update(apiUrl).digest("hex"));
|
|
15766
|
-
const deadline = Date.now() + 3e4;
|
|
15767
|
-
while (true) {
|
|
15768
|
-
try {
|
|
15769
|
-
await mkdir5(lock, { mode: 448 });
|
|
15770
|
-
break;
|
|
15771
|
-
} catch (error51) {
|
|
15772
|
-
if (error51.code !== "EEXIST") throw error51;
|
|
15773
|
-
const info = await stat3(lock).catch(() => null);
|
|
15774
|
-
if (info && Date.now() - info.mtimeMs > 12e4) {
|
|
15775
|
-
await rm4(lock, { recursive: true, force: true });
|
|
15776
|
-
continue;
|
|
15777
|
-
}
|
|
15778
|
-
if (Date.now() > deadline) throw new Error("Another Rill process is setting up authorization. Retry shortly.");
|
|
15779
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
15780
|
-
}
|
|
15781
|
-
}
|
|
15782
|
-
try {
|
|
15783
|
-
return await createAnonymousCredential(apiUrl);
|
|
15784
|
-
} finally {
|
|
15785
|
-
await rm4(lock, { recursive: true, force: true });
|
|
15786
|
-
}
|
|
15787
|
-
}
|
|
15788
|
-
async function createAnonymousCredential(apiUrl) {
|
|
15789
|
-
const existing = await loadAgentCredential(apiUrl);
|
|
15790
|
-
if (existing) return existing;
|
|
15791
|
-
const stored = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
15792
|
-
const initial = stored?.registrationToken ? stored : { version: 1, anonymous: true, registrationToken: randomBytes(32).toString("hex") };
|
|
15793
|
-
await save(apiUrl, initial);
|
|
15794
|
-
const registered = await request2(apiUrl, "/agent/identity", { type: "anonymous", analytics_enabled: process.env.RILL_ANONYMOUS_ANALYTICS !== "0" && process.env.DO_NOT_TRACK !== "1", registration_token: initial.registrationToken, name: `${hostname3()} \xB7 ${process.platform}` });
|
|
15795
|
-
const saved = { ...initial, registrationToken: void 0, identityAssertion: registered.identity_assertion, assertionExpires: registered.assertion_expires, claimToken: registered.claim_token };
|
|
15796
|
-
await save(apiUrl, saved);
|
|
15797
|
-
process.stderr.write("Recording anonymously: three recordings, up to five minutes each. Recordings and share links expire after seven days. Run rill connect --email <your-email> to keep them.\n");
|
|
15798
|
-
return await loadAgentCredential(apiUrl);
|
|
15799
|
-
}
|
|
15800
|
-
async function connectAgent(apiUrl, email3) {
|
|
15801
|
-
if (process.env.RILL_TOKEN) throw new Error("Unset RILL_TOKEN before connecting a local identity.");
|
|
15802
|
-
let saved = parseAuthorization(await loadStoredCredential(apiUrl));
|
|
15803
|
-
let claim;
|
|
15804
|
-
if (saved?.claimToken && saved.anonymous) {
|
|
15805
|
-
try {
|
|
15806
|
-
const result = await request2(apiUrl, "/agent/identity/claim", { claim_token: saved.claimToken, email: email3 });
|
|
15807
|
-
claim = result.claim_attempt;
|
|
15808
|
-
} catch (error51) {
|
|
15809
|
-
if (!(error51 instanceof AgentAuthError) || error51.code !== "claim_expired") throw error51;
|
|
15810
|
-
}
|
|
15811
|
-
}
|
|
15812
|
-
if (!claim) {
|
|
15813
|
-
await save(apiUrl, saved ?? { version: 1, anonymous: true });
|
|
15814
|
-
const result = await request2(apiUrl, "/agent/identity", { type: "service_auth", login_hint: email3, name: `${hostname3()} \xB7 ${process.platform}` });
|
|
15815
|
-
saved = { version: 1, anonymous: true, claimToken: result.claim_token };
|
|
15816
|
-
await save(apiUrl, saved);
|
|
15817
|
-
claim = result.claim;
|
|
15818
|
-
}
|
|
15819
|
-
process.stderr.write(`Open ${claim.verification_uri}
|
|
15820
|
-
Sign in as ${email3} and enter ${claim.user_code}. Approve only the request you started.
|
|
15821
|
-
`);
|
|
15822
|
-
const deadline = Date.now() + claim.expires_in * 1e3;
|
|
15823
|
-
let interval = claim.interval;
|
|
15824
|
-
while (Date.now() < deadline) {
|
|
15825
|
-
await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
|
|
15826
|
-
try {
|
|
15827
|
-
const tokens = await request2(apiUrl, "/oauth2/token", { grant_type: CLAIM_GRANT, claim_token: saved.claimToken }, true);
|
|
15828
|
-
const connected = withTokens({ version: 1, anonymous: false }, tokens);
|
|
15829
|
-
await save(apiUrl, connected);
|
|
15830
|
-
return { status: "authenticated", expiresAt: connected.assertionExpires, apiUrl };
|
|
15831
|
-
} catch (error51) {
|
|
15832
|
-
if (error51 instanceof AgentAuthError && error51.code === "expired_token") throw new AgentAuthError("expired_token", "The approval attempt expired. Run rill connect again to request a fresh code using the same saved claim token.");
|
|
15833
|
-
if (error51 instanceof AgentAuthError && error51.code === "authorization_pending") continue;
|
|
15834
|
-
if (error51 instanceof AgentAuthError && error51.code === "slow_down") {
|
|
15835
|
-
interval += 5;
|
|
15836
|
-
continue;
|
|
15837
|
-
}
|
|
15838
|
-
throw error51;
|
|
15839
|
-
}
|
|
15840
|
-
}
|
|
15841
|
-
throw new AgentAuthError("expired_token", "Connection request expired. Run rill connect again to restart the approval attempt using the same saved claim token.");
|
|
15842
|
-
}
|
|
15843
|
-
|
|
15844
|
-
// src/keychain.ts
|
|
15845
|
-
import { spawn as spawn4 } from "child_process";
|
|
15846
|
-
var service = "rill";
|
|
15847
|
-
async function saveCredential(apiUrl, token) {
|
|
15848
|
-
if (process.platform === "darwin") {
|
|
15849
|
-
await run("security", ["add-generic-password", "-U", "-a", apiUrl, "-s", service, "-w", token]);
|
|
15850
|
-
return;
|
|
15851
|
-
}
|
|
15852
|
-
if (process.platform === "linux") {
|
|
15853
|
-
await run("secret-tool", ["store", "--label=Rill agent credential", "service", service, "api-url", apiUrl], token);
|
|
15854
|
-
return;
|
|
15855
|
-
}
|
|
15856
|
-
throw new Error("OS keychain storage is not supported on this platform yet. Use RILL_TOKEN.");
|
|
15857
|
-
}
|
|
15858
|
-
function loadCredential(apiUrl) {
|
|
15859
|
-
return loadAgentCredential(apiUrl);
|
|
15860
|
-
}
|
|
15861
|
-
async function loadStoredCredential(apiUrl) {
|
|
15862
|
-
try {
|
|
15863
|
-
if (process.platform === "darwin") return (await run("security", ["find-generic-password", "-a", apiUrl, "-s", service, "-w"])).trim() || null;
|
|
15864
|
-
if (process.platform === "linux") return (await run("secret-tool", ["lookup", "service", service, "api-url", apiUrl])).trim() || null;
|
|
15865
|
-
} catch (error51) {
|
|
15866
|
-
const code = error51.exitCode;
|
|
15867
|
-
if (process.platform === "darwin" && code === 44) return null;
|
|
15868
|
-
if (process.platform === "linux" && code === 1 && !error51.message.trim()) return null;
|
|
15869
|
-
throw new Error("Cannot read the OS credential store. Unlock your keychain or enable Secret Service; your identity has not been replaced.");
|
|
15870
|
-
}
|
|
15871
|
-
throw new Error("Local authorization requires macOS Keychain or Linux Secret Service.");
|
|
15872
|
-
}
|
|
15873
|
-
function run(command, args, input) {
|
|
15874
|
-
return new Promise((resolve, reject) => {
|
|
15875
|
-
const child = spawn4(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
15876
|
-
const stdout = [];
|
|
15877
|
-
const stderr = [];
|
|
15878
|
-
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
15879
|
-
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
15880
|
-
child.once("error", reject);
|
|
15881
|
-
child.once("exit", (code) => code === 0 ? resolve(Buffer.concat(stdout).toString("utf8")) : reject(Object.assign(new Error(Buffer.concat(stderr).toString("utf8").trim()), { exitCode: code })));
|
|
15882
|
-
if (input) child.stdin.end(input);
|
|
15883
|
-
else child.stdin.end();
|
|
15884
|
-
});
|
|
15885
|
-
}
|
|
15886
|
-
|
|
15887
16020
|
// src/output.ts
|
|
15888
16021
|
function emit(value, pretty = false) {
|
|
15889
16022
|
process.stdout.write(`${JSON.stringify(value, null, pretty ? 2 : void 0)}
|
|
@@ -15892,7 +16025,7 @@ function emit(value, pretty = false) {
|
|
|
15892
16025
|
function failureValue(error51) {
|
|
15893
16026
|
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
15894
16027
|
const [candidate] = message.split(":", 1);
|
|
15895
|
-
const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "scope_denied", "quota_exceeded", "browser_unavailable", "recording_not_found", "upload_interrupted", "processing_failed", "share_revoked", "validation_failed"]);
|
|
16028
|
+
const knownCodes = /* @__PURE__ */ new Set(["authentication_required", "authorization_not_found", "expired_token", "access_denied", "unsupported_platform", "scope_denied", "quota_exceeded", "browser_unavailable", "recording_not_found", "upload_interrupted", "processing_pending", "processing_failed", "share_revoked", "validation_failed"]);
|
|
15896
16029
|
const structured = error51 && typeof error51 === "object" ? error51 : {};
|
|
15897
16030
|
const code = structured.code && knownCodes.has(structured.code) ? structured.code : knownCodes.has(candidate) ? candidate : "internal_error";
|
|
15898
16031
|
return { error: { code, message, ...structured.recovery ? { recovery: structured.recovery } : {}, ...structured.details ? { details: structured.details } : {} } };
|
|
@@ -15968,6 +16101,7 @@ var CLI_VERSION = packageJson.version;
|
|
|
15968
16101
|
|
|
15969
16102
|
// src/doctor.ts
|
|
15970
16103
|
var defaultDependencies = {
|
|
16104
|
+
platform: process.platform,
|
|
15971
16105
|
encoding: "remote",
|
|
15972
16106
|
encodingCapabilities: (apiUrl, token) => new ControlPlaneClient(apiUrl, token).encodingCapabilities(),
|
|
15973
16107
|
nodeVersion: process.version,
|
|
@@ -15983,6 +16117,7 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
15983
16117
|
const dependencies = { ...defaultDependencies, ...overrides };
|
|
15984
16118
|
const checks = [];
|
|
15985
16119
|
let feedbackEnabled = false;
|
|
16120
|
+
checks.push(recorderTransportCheck(dependencies.platform));
|
|
15986
16121
|
if (compareVersions(normalizeVersion(dependencies.nodeVersion), "22.0.0") >= 0) {
|
|
15987
16122
|
checks.push({ name: "node", status: "pass", message: `Node.js ${dependencies.nodeVersion} is supported.`, version: dependencies.nodeVersion });
|
|
15988
16123
|
} else {
|
|
@@ -16010,8 +16145,16 @@ async function runDoctor(apiUrl, overrides = {}) {
|
|
|
16010
16145
|
if (!health) {
|
|
16011
16146
|
checks.push({ name: "authentication", status: "skip", message: "Authentication was not checked because the API is unreachable.", recovery: `Restore API reachability, then run \`rill --api-url ${apiUrl} doctor\` again.` });
|
|
16012
16147
|
} else {
|
|
16013
|
-
|
|
16014
|
-
|
|
16148
|
+
let token = null;
|
|
16149
|
+
let credentialError;
|
|
16150
|
+
try {
|
|
16151
|
+
token = await dependencies.loadCredential(apiUrl);
|
|
16152
|
+
} catch (error51) {
|
|
16153
|
+
credentialError = error51;
|
|
16154
|
+
}
|
|
16155
|
+
if (credentialError) {
|
|
16156
|
+
checks.push({ name: "authentication", status: "fail", message: errorMessage(credentialError), recovery: "Unlock the OS credential store, or use RILL_TOKEN. Windows local recording remains unsupported." });
|
|
16157
|
+
} else if (!token) {
|
|
16015
16158
|
checks.push({ name: "authentication", status: health.anonymousAccess ? "warn" : "fail", message: health.anonymousAccess ? "The first recording will create an anonymous identity in your OS credential store. Anonymous recordings expire after seven days." : "No Rill agent credential is available.", recovery: `Run \`rill --api-url ${apiUrl} login\` or set RILL_TOKEN.` });
|
|
16016
16159
|
} else {
|
|
16017
16160
|
try {
|
|
@@ -16124,16 +16267,21 @@ async function startRecordingWithCompensation(remote, dependencies) {
|
|
|
16124
16267
|
throw startupError;
|
|
16125
16268
|
}
|
|
16126
16269
|
}
|
|
16127
|
-
async function remoteAwareRecordingStatus(recordingId, dependencies) {
|
|
16270
|
+
async function remoteAwareRecordingStatus(recordingId, dependencies, apiUrl = "https://userill.dev") {
|
|
16271
|
+
const remote = await dependencies.remoteStatus(recordingId);
|
|
16272
|
+
const remoteView = {
|
|
16273
|
+
...remote,
|
|
16274
|
+
recordingId: typeof remote.recordingId === "string" ? remote.recordingId : remote.id ?? recordingId,
|
|
16275
|
+
source: "remote",
|
|
16276
|
+
...recordingNextStep(apiUrl, recordingId, String(remote.status))
|
|
16277
|
+
};
|
|
16278
|
+
if (["uploading", "processing", "ready", "failed", "expired", "cancelled"].includes(String(remote.status))) return remoteView;
|
|
16128
16279
|
try {
|
|
16129
|
-
|
|
16280
|
+
const local = await dependencies.localStatus(recordingId);
|
|
16281
|
+
const status = local.status === "ready" ? "captured" : local.status;
|
|
16282
|
+
return { ...local, status, source: "local", ...recordingNextStep(apiUrl, recordingId, String(status)) };
|
|
16130
16283
|
} catch {
|
|
16131
|
-
|
|
16132
|
-
return {
|
|
16133
|
-
...remote,
|
|
16134
|
-
recordingId: typeof remote.recordingId === "string" ? remote.recordingId : remote.id ?? recordingId,
|
|
16135
|
-
source: "remote"
|
|
16136
|
-
};
|
|
16284
|
+
return remoteView;
|
|
16137
16285
|
}
|
|
16138
16286
|
}
|
|
16139
16287
|
|
|
@@ -16187,9 +16335,22 @@ async function authorize(quiet = false) {
|
|
|
16187
16335
|
}
|
|
16188
16336
|
throw new Error("Device authorization expired.");
|
|
16189
16337
|
}
|
|
16190
|
-
program.command("
|
|
16338
|
+
program.command("whoami").description("Show the current verified agent identity without starting sign-in.").action(async () => {
|
|
16339
|
+
const options = program.opts();
|
|
16340
|
+
const identity = await whoami(options.apiUrl);
|
|
16341
|
+
emit(identity, options.pretty);
|
|
16342
|
+
if (!identity.authenticated) process.exitCode = 1;
|
|
16343
|
+
});
|
|
16344
|
+
var connect = program.command("connect").description("Connect this agent once, with renewable account access.").option("--email <email>", "Email of the Rill account approving access").option("--no-wait", "Return approval instructions immediately; poll with connect status").action(async (input) => {
|
|
16345
|
+
if (!input.email) throw new Error("validation_failed: Supply --email <email> to start a connection.");
|
|
16191
16346
|
const options = program.opts();
|
|
16192
|
-
emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email) }, options.pretty);
|
|
16347
|
+
emit({ schemaVersion: 1, ...await connectAgent(options.apiUrl, input.email, { wait: input.wait }) }, options.pretty);
|
|
16348
|
+
});
|
|
16349
|
+
connect.command("status").argument("<authorization-id>").description("Poll a saved connection once without starting another approval.").action(async (id) => {
|
|
16350
|
+
const options = program.opts();
|
|
16351
|
+
const result = await pollConnection(options.apiUrl, id);
|
|
16352
|
+
emit({ schemaVersion: 1, ...result }, options.pretty);
|
|
16353
|
+
if (result.status === "expired" || result.status === "denied") process.exitCode = 1;
|
|
16193
16354
|
});
|
|
16194
16355
|
program.command("login").description("Authorize Rill using the existing device login flow.").action(async () => {
|
|
16195
16356
|
await authorize();
|
|
@@ -16228,6 +16389,7 @@ for (const action of ["status", "cancel"]) cloudRun.command(action).argument("<r
|
|
|
16228
16389
|
});
|
|
16229
16390
|
record2.command("start").option("--encoding <mode>", "remote or local", "remote").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) => {
|
|
16230
16391
|
const options = program.opts();
|
|
16392
|
+
assertRecorderTransport();
|
|
16231
16393
|
await ensureAnonymousCredential(options.apiUrl);
|
|
16232
16394
|
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16233
16395
|
const result = await withRunFeedback(() => client.agentSession(), async (rememberRecording) => {
|
|
@@ -16252,7 +16414,7 @@ record2.command("start").option("--encoding <mode>", "remote or local", "remote"
|
|
|
16252
16414
|
headed: Boolean(commandOptions.headed),
|
|
16253
16415
|
attached: Boolean(commandOptions.cdpUrl)
|
|
16254
16416
|
}).catch(() => void 0);
|
|
16255
|
-
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
|
|
16417
|
+
return { schemaVersion: 1, recordingId: remote.recordingId, status: local.status, cdpUrl: local.cdpUrl, browserAttachment: browserAttachment(local.cdpUrl), bodyCaptureEnabled: remote.bodyCaptureEnabled, anonymous: remote.anonymous ?? false, expiresAt: remote.expiresAt ?? null, maxDurationSeconds: Math.min(Number(commandOptions.maxDuration), remote.maxDurationSeconds ?? 600) };
|
|
16256
16418
|
});
|
|
16257
16419
|
emit(result, options.pretty);
|
|
16258
16420
|
});
|
|
@@ -16272,7 +16434,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16272
16434
|
await client.uploadDiagnostics(recordingId, local.result.diagnosticsPath, local.result.diagnostics);
|
|
16273
16435
|
const accepted = await submitFrameCapture(client, recordingId, local.result.frameDirectory);
|
|
16274
16436
|
const result2 = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
16275
|
-
emit({ schemaVersion: 1, ...result2, ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
|
|
16437
|
+
emit({ schemaVersion: 1, ...result2, ...recordingNextStep(options.apiUrl, recordingId, result2.status, { resumableFrames: true }), ...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false) }, options.pretty);
|
|
16276
16438
|
if (result2.status === "ready") await rm5(dirname2(local.result.videoPath), { recursive: true, force: true });
|
|
16277
16439
|
else watchEncoding(options.apiUrl, recordingId);
|
|
16278
16440
|
return;
|
|
@@ -16291,6 +16453,7 @@ record2.command("stop").argument("<recording-id>").option("--no-wait").option("-
|
|
|
16291
16453
|
durationSeconds: local.result.durationSeconds,
|
|
16292
16454
|
stoppedReason: local.result.stoppedReason,
|
|
16293
16455
|
diagnostics: local.result.diagnostics,
|
|
16456
|
+
...recordingNextStep(options.apiUrl, recordingId, remote ? "ready" : "processing"),
|
|
16294
16457
|
...recordingFeedbackFields(recordingId, stopped?.feedbackRequested ?? false)
|
|
16295
16458
|
};
|
|
16296
16459
|
emit(result, options.pretty);
|
|
@@ -16314,7 +16477,7 @@ record2.command("retry").argument("<recording-id>").option("--no-wait").action(a
|
|
|
16314
16477
|
if (!accepted) throw new Error("Encoding job is unavailable.");
|
|
16315
16478
|
if (["expired", "cancelled"].includes(accepted.status)) throw new Error(`Recording ${accepted.status}.`);
|
|
16316
16479
|
const result = commandOptions.wait && record2.opts().wait !== false ? await waitForEncoding(client, recordingId) : accepted;
|
|
16317
|
-
emit({ schemaVersion: 1, ...result }, options.pretty);
|
|
16480
|
+
emit({ schemaVersion: 1, ...result, ...recordingNextStep(options.apiUrl, recordingId, result.status, { resumableFrames: true }) }, options.pretty);
|
|
16318
16481
|
if (result.status === "ready") await removeLocalCapture(recordingId);
|
|
16319
16482
|
else watchEncoding(options.apiUrl, recordingId);
|
|
16320
16483
|
});
|
|
@@ -16356,11 +16519,11 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
|
|
|
16356
16519
|
try {
|
|
16357
16520
|
const encoding = await client.encodingStatus(recordingId);
|
|
16358
16521
|
const remote = encoding.status === "ready" ? await client.recordingStatus(recordingId) : {};
|
|
16359
|
-
emit({ schemaVersion: 1, ...encoding, ...remote, recordingId, source: "remote" }, options.pretty);
|
|
16522
|
+
emit({ schemaVersion: 1, ...encoding, ...remote, recordingId, source: "remote", ...recordingNextStep(options.apiUrl, recordingId, encoding.status, { resumableFrames: true }) }, options.pretty);
|
|
16360
16523
|
if (encoding.status === "ready") await removeLocalCapture(recordingId);
|
|
16361
16524
|
return;
|
|
16362
16525
|
} catch (error51) {
|
|
16363
|
-
if (!(error51 instanceof
|
|
16526
|
+
if (!(error51 instanceof ControlPlaneError) || error51.code !== "recording_not_found") throw error51;
|
|
16364
16527
|
}
|
|
16365
16528
|
const status = await remoteAwareRecordingStatus(recordingId, {
|
|
16366
16529
|
localStatus: async (id) => {
|
|
@@ -16368,7 +16531,7 @@ record2.command("status").argument("<recording-id>").action(async (recordingId)
|
|
|
16368
16531
|
return recordingStatus(id);
|
|
16369
16532
|
},
|
|
16370
16533
|
remoteStatus: (id) => client.recordingStatus(id)
|
|
16371
|
-
});
|
|
16534
|
+
}, options.apiUrl);
|
|
16372
16535
|
emit({ schemaVersion: 1, ...status }, options.pretty);
|
|
16373
16536
|
});
|
|
16374
16537
|
record2.command("cancel").argument("<recording-id>").description("Cancel an active recording and release its remote quota slot.").action(async (recordingId) => {
|
|
@@ -16387,11 +16550,25 @@ program.command("upload").argument("<video-file>").option("--title <title>").opt
|
|
|
16387
16550
|
await client.uploadVideo(upload.uploadUrl, videoFile);
|
|
16388
16551
|
const remote = commandOptions.wait === false ? null : await client.waitUntilPlayable(recording.recordingId);
|
|
16389
16552
|
if (remote) process.stderr.write("\n");
|
|
16390
|
-
emit({ schemaVersion: 1, recordingId: recording.recordingId, status: remote ? "ready" : "processing", shareUrl: remote?.shareUrl ?? null, expiresAt: remote?.expires_at ?? recording.expiresAt ?? null, diagnosticsAvailable: false }, options.pretty);
|
|
16553
|
+
emit({ schemaVersion: 1, recordingId: recording.recordingId, status: remote ? "ready" : "processing", shareUrl: remote?.shareUrl ?? null, expiresAt: remote?.expires_at ?? recording.expiresAt ?? null, diagnosticsAvailable: false, ...recordingNextStep(options.apiUrl, recording.recordingId, remote ? "ready" : "processing") }, options.pretty);
|
|
16554
|
+
});
|
|
16555
|
+
program.command("inspect").argument("<share-url>").option("--cursor <cursor>", "Read the next page of discussions").action(async (shareUrl, commandOptions) => {
|
|
16556
|
+
const options = program.opts();
|
|
16557
|
+
emit(await new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl)).inspect(shareUrl, commandOptions.cursor), options.pretty);
|
|
16558
|
+
});
|
|
16559
|
+
var comments = program.command("comments").description("Reply to pinned review discussions.");
|
|
16560
|
+
comments.command("reply").argument("<share-url>").requiredOption("--discussion <id>").requiredOption("--body <text>").option("--request-id <uuid>", "Reuse on retry to prevent duplicate replies").action(async (shareUrl, input) => {
|
|
16561
|
+
const options = program.opts();
|
|
16562
|
+
const requestId = input.requestId ?? crypto.randomUUID();
|
|
16563
|
+
process.stderr.write(`Reply request ID: ${requestId} (reuse with --request-id when retrying)
|
|
16564
|
+
`);
|
|
16565
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16566
|
+
emit(await client.reply(shareUrl, input.discussion, input.body, requestId), options.pretty);
|
|
16391
16567
|
});
|
|
16392
|
-
|
|
16568
|
+
comments.command("edit").argument("<share-url>").requiredOption("--comment <id>").requiredOption("--body <text>").action(async (shareUrl, input) => {
|
|
16393
16569
|
const options = program.opts();
|
|
16394
|
-
|
|
16570
|
+
const client = new ControlPlaneClient(options.apiUrl, () => loadCredential(options.apiUrl));
|
|
16571
|
+
emit(await client.editReply(shareUrl, input.comment, input.body), options.pretty);
|
|
16395
16572
|
});
|
|
16396
16573
|
var drafts = program.command("drafts");
|
|
16397
16574
|
drafts.command("list").action(async () => {
|
|
@@ -16416,6 +16593,7 @@ drafts.command("purge").action(async () => {
|
|
|
16416
16593
|
emit({ schemaVersion: 1, purged }, options.pretty);
|
|
16417
16594
|
});
|
|
16418
16595
|
program.hook("preAction", async (_command, actionCommand) => {
|
|
16596
|
+
if (commandPath(actionCommand).join(" ") === "record start") assertRecorderTransport();
|
|
16419
16597
|
const command = cliCommandCategory(commandPath(actionCommand));
|
|
16420
16598
|
if (!command) return;
|
|
16421
16599
|
const options = program.opts();
|