@qawolf/cli 1.33.0 → 1.34.0

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 CHANGED
@@ -113,7 +113,31 @@ const submitted = await runner.run({
113
113
  if (submitted.ok) console.log(submitted.value.runId, submitted.value.fileSync);
114
114
  ```
115
115
 
116
- `ok: false` is a transport or auth failure. A runner that refused the request comes back as `ok: true` with `value.outcome === "failure"` and a `failureReason` from the published contract, so a caller can switch on it exhaustively.
116
+ `ok: false` reports a request failure, including transport, auth, or local validation. A runner that refused the request comes back as `ok: true` with an outcome and `failureReason` from the published contract, so a caller can switch on it exhaustively. Most verbs expose `value.outcome`; recording controls expose `value.result.outcome` and a history `value.url`.
117
+
118
+ Control video recording on a Playwright runner after a flow has started its screen:
119
+
120
+ ```ts
121
+ const recordingId = crypto.randomUUID();
122
+ const recording = await runner.record({
123
+ runnerId: "agent-1",
124
+ command: { action: "start", recordingId },
125
+ });
126
+ if (recording.ok && recording.value.result.outcome === "success") {
127
+ // Drive the browser, then stop this specific capture.
128
+ await runner.record({
129
+ runnerId: "agent-1",
130
+ command: { action: "stop", recordingId },
131
+ });
132
+ }
133
+
134
+ // History remains readable after the runner terminates.
135
+ const history = await runner.listRecordings({ runnerId: "agent-1" });
136
+ if (history.ok)
137
+ console.log(history.value.recordings, history.value.nextPageToken);
138
+ ```
139
+
140
+ `record` also accepts `{ action: "status" }` and `{ action: "auto", enabled: true }` (or `false`). Keep the UUID when retrying start or stop. `listRecordings` accepts optional `recordingId` and `pageToken` filters and returns one page, with stable platform URLs and expiring video URLs. The equivalent CLI commands are `qawolf runner record start|stop|status|auto` and `qawolf runner list-recordings`; see the [runner reference](skills/qawolf-cli/references/runner.md#video-recording).
117
141
 
118
142
  ## Reference
119
143
 
package/dist/cli.js CHANGED
@@ -15210,9 +15210,10 @@ function createLoggingSystem(opts) {
15210
15210
  }
15211
15211
 
15212
15212
  // src/core/publicApi/notFoundSubject.ts
15213
- var runnerRoutesThatNameNoRunner = new Set([
15213
+ var runnerRoutesWithoutLivePod = new Set([
15214
15214
  "runner.launch",
15215
- "runner.list"
15215
+ "runner.list",
15216
+ "runner.recordings"
15216
15217
  ]);
15217
15218
  var runRoutesThatResolveOneRun = new Set([
15218
15219
  "run.diagnose",
@@ -15234,7 +15235,7 @@ function field(input, name) {
15234
15235
  return typeof value === "string" ? value : undefined;
15235
15236
  }
15236
15237
  function notFoundSubject(contractName, input) {
15237
- if (contractName.startsWith("runner.") && !runnerRoutesThatNameNoRunner.has(contractName)) {
15238
+ if (contractName.startsWith("runner.") && !runnerRoutesWithoutLivePod.has(contractName)) {
15238
15239
  return { kind: "runner", runnerId: field(input, "id") };
15239
15240
  }
15240
15241
  if (runRoutesThatResolveOneRun.has(contractName)) {
@@ -22389,7 +22390,7 @@ function startUpdateCheck(deps) {
22389
22390
  // package.json
22390
22391
  var package_default = {
22391
22392
  name: "@qawolf/cli",
22392
- version: "1.33.0",
22393
+ version: "1.34.0",
22393
22394
  description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
22394
22395
  keywords: [
22395
22396
  "automation",
@@ -36271,6 +36272,9 @@ async function handleRunnerActions(ctx, options, deps) {
36271
36272
  return framesNote === undefined ? failure : { ...failure, error: appendSentence(failure.error, framesNote) };
36272
36273
  }
36273
36274
 
36275
+ // src/domains/interactiveRunner/deps.ts
36276
+ import { randomUUID as randomUUID3 } from "node:crypto";
36277
+
36274
36278
  // src/shell/interactiveRunner/collectRunFiles.ts
36275
36279
  import { join as join53 } from "node:path";
36276
36280
 
@@ -36538,6 +36542,7 @@ function makeInteractiveRunnerDeps(options) {
36538
36542
  cwd: options.cwd,
36539
36543
  env: options.env,
36540
36544
  makeRunnerId,
36545
+ makeRecordingId: randomUUID3,
36541
36546
  readFile: (path) => options.fs.readFile(path),
36542
36547
  readStdin: readStdin2,
36543
36548
  runFilesManifest: makeRunFilesManifestStore({
@@ -38415,6 +38420,207 @@ function registerRunCommand(runner, signals) {
38415
38420
  }, runnerDeps(ctx)))(opts, command));
38416
38421
  }
38417
38422
 
38423
+ // src/core/messages/interactiveRunner/recording.ts
38424
+ var recordingMessages = {
38425
+ noRunnerIdForHistory: "Name the runner whose recordings to read with --runner <id> or QAWOLF_RUNNER_ID. History stays readable after the runner terminates.",
38426
+ unsupported: "Video recording requires a Playwright runner.",
38427
+ inProgress: "A recording is already in progress. Use runner record status to check its mode and id. Stop a manual recording by id, or wait for the run to finish an automatic recording.",
38428
+ notFound: "That recording was not found on the runner. Use runner record status for the active recording or runner list-recordings for published history.",
38429
+ idUsed: "That recording id has already been used. Start a new recording with a new UUID.",
38430
+ failed: "The recording operation failed. If this was a stop, retry it with the same recording id to finish stopping or publishing it. Check runner list-recordings for its published status.",
38431
+ failedCapture: "The recording failed. Check runner list-recordings for its published status.",
38432
+ recordingId: (id) => `Recording id: ${id}.`
38433
+ };
38434
+
38435
+ // src/domains/interactiveRunner/recordingOutput.ts
38436
+ function recordingFailure(reason) {
38437
+ switch (reason) {
38438
+ case "unsupported":
38439
+ return {
38440
+ error: recordingMessages.unsupported,
38441
+ exitCode: exitCodes.invalidArgs
38442
+ };
38443
+ case "screen-not-ready":
38444
+ return {
38445
+ error: interactiveRunnerMessages.screenNotReady,
38446
+ exitCode: exitCodes.network
38447
+ };
38448
+ case "recording-in-progress":
38449
+ return {
38450
+ error: recordingMessages.inProgress,
38451
+ exitCode: exitCodes.invalidArgs
38452
+ };
38453
+ case "recording-not-found":
38454
+ return {
38455
+ error: recordingMessages.notFound,
38456
+ exitCode: exitCodes.notFound
38457
+ };
38458
+ case "recording-id-used":
38459
+ return {
38460
+ error: recordingMessages.idUsed,
38461
+ exitCode: exitCodes.invalidArgs
38462
+ };
38463
+ case "runner-unreachable":
38464
+ return {
38465
+ error: interactiveRunnerMessages.runnerUnreachable,
38466
+ exitCode: exitCodes.network
38467
+ };
38468
+ case "recording-failed":
38469
+ return {
38470
+ error: recordingMessages.failed,
38471
+ exitCode: exitCodes.testFailure
38472
+ };
38473
+ }
38474
+ }
38475
+ function formatRecordingState(answer) {
38476
+ const { result, url } = answer;
38477
+ if (result.outcome === "failure")
38478
+ return recordingFailure(result.failureReason).error;
38479
+ const { active, auto } = result.state;
38480
+ const lines = [
38481
+ active ? `Recording ${active.id} (${active.mode}), started ${active.startedAt}.` : "No active recording.",
38482
+ `Automatic recording: ${auto}.`
38483
+ ];
38484
+ if (result.recording) {
38485
+ lines.push(`Recording ${result.recording.id}: ${result.recording.status}.`);
38486
+ }
38487
+ return [...lines, url].join(`
38488
+ `);
38489
+ }
38490
+ function formatRecordings(answer) {
38491
+ const lines = answer.recordings.map((recording) => [
38492
+ `${recording.id} (${recording.mode}, ${recording.status})`,
38493
+ ` ${recording.startedAt} to ${recording.endedAt}`,
38494
+ ` ${recording.url}`,
38495
+ ...recording.videoUrl ? [` Video: ${recording.videoUrl}`] : []
38496
+ ].join(`
38497
+ `));
38498
+ if (lines.length === 0)
38499
+ lines.push("No published recordings found.");
38500
+ if (answer.nextPageToken !== undefined) {
38501
+ lines.push(`Next page token (pass with --page-token): ${answer.nextPageToken}`);
38502
+ }
38503
+ return lines.join(`
38504
+ `);
38505
+ }
38506
+
38507
+ // src/domains/interactiveRunner/recordingRequests.ts
38508
+ async function recordOnRunner(ctx, runner, command) {
38509
+ const contract = publicContractsV1.runner.record;
38510
+ const parsed = contract.input.safeParse({ id: runner.runnerId, command });
38511
+ if (!parsed.success) {
38512
+ return {
38513
+ ok: false,
38514
+ error: "Invalid recording request. Runner ids must be valid and recording ids must be UUIDs.",
38515
+ exitCode: exitCodes.invalidArgs
38516
+ };
38517
+ }
38518
+ const result = await ctx.platformClient.callPublicApi(contract, parsed.data, runnerCallOptions);
38519
+ return result.ok ? result : { ok: false, ...runnerRequestFailure(result, runner) };
38520
+ }
38521
+ async function readRunnerRecordings(ctx, runner, query) {
38522
+ const contract = publicContractsV1.runner.recordings;
38523
+ const parsed = contract.input.safeParse({ id: runner.runnerId, ...query });
38524
+ if (!parsed.success) {
38525
+ return {
38526
+ ok: false,
38527
+ error: "Invalid recordings request. Use a valid runner id, a UUID recording id, and a page token of at most 4096 characters.",
38528
+ exitCode: exitCodes.invalidArgs
38529
+ };
38530
+ }
38531
+ const result = await ctx.platformClient.callPublicApi(contract, parsed.data);
38532
+ return result.ok ? result : { ok: false, ...runnerRequestFailure(result, runner) };
38533
+ }
38534
+
38535
+ // src/domains/interactiveRunner/recording.ts
38536
+ async function handleRunnerRecord(ctx, options, deps) {
38537
+ const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
38538
+ if (resolved.type === "failed")
38539
+ return { ...failureFields(resolved), exitCode: resolved.exitCode };
38540
+ const command = options.command.action === "start" ? {
38541
+ action: "start",
38542
+ recordingId: options.command.recordingId ?? deps.makeRecordingId()
38543
+ } : options.command;
38544
+ const response = await recordOnRunner(ctx, resolved, command);
38545
+ const idNote = "recordingId" in command ? recordingMessages.recordingId(command.recordingId) : undefined;
38546
+ if (!response.ok) {
38547
+ const fields = failureFields(response);
38548
+ return {
38549
+ ...fields,
38550
+ ...idNote ? { errorBody: [fields.errorBody, idNote].filter(Boolean).join(`
38551
+ `) } : {},
38552
+ exitCode: response.exitCode
38553
+ };
38554
+ }
38555
+ const answer = response.value;
38556
+ if (answer.result.outcome === "failure") {
38557
+ return {
38558
+ ...recordingFailure(answer.result.failureReason),
38559
+ errorBody: [idNote, answer.url].filter(Boolean).join(`
38560
+ `)
38561
+ };
38562
+ }
38563
+ ctx.ui.output(answer, formatRecordingState(answer));
38564
+ if (answer.result.recording?.status === "failed") {
38565
+ return {
38566
+ error: recordingMessages.failedCapture,
38567
+ exitCode: exitCodes.testFailure,
38568
+ errorBody: [
38569
+ recordingMessages.recordingId(answer.result.recording.id),
38570
+ answer.url
38571
+ ].join(`
38572
+ `)
38573
+ };
38574
+ }
38575
+ return;
38576
+ }
38577
+ async function handleRunnerRecordings(ctx, options, deps) {
38578
+ const resolved = await resolveRunner(ctx, {
38579
+ autoLaunch: false,
38580
+ noRunnerIdMessage: recordingMessages.noRunnerIdForHistory,
38581
+ runner: options.runner
38582
+ }, deps);
38583
+ if (resolved.type === "failed")
38584
+ return { ...failureFields(resolved), exitCode: resolved.exitCode };
38585
+ const response = await readRunnerRecordings(ctx, resolved, {
38586
+ ...options.recordingId === undefined ? {} : { recordingId: options.recordingId },
38587
+ ...options.pageToken === undefined ? {} : { pageToken: options.pageToken }
38588
+ });
38589
+ if (!response.ok)
38590
+ return { ...failureFields(response), exitCode: response.exitCode };
38591
+ ctx.ui.output(response.value, formatRecordings(response.value));
38592
+ return;
38593
+ }
38594
+
38595
+ // src/commands/runner/recording.register.ts
38596
+ function registerRunnerRecordingCommands(runner, signals) {
38597
+ const record = runner.command("record").description("Control video recording on a Playwright runner");
38598
+ declareCommandKind(record.command("start"), "write").description("Start manual video capture across runs. Requires a ready screen and suppresses automatic capture until stopped").option("--recording-id <uuid>", "Recording UUID. Generated when omitted; reuse it when retrying this start").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerRecord(ctx, {
38599
+ command: { action: "start", recordingId: opts.recordingId },
38600
+ runner: opts.runner
38601
+ }, runnerDeps(ctx)))(opts, command));
38602
+ declareCommandKind(record.command("stop <recording-id>"), "write").description("Stop the recording with this UUID and publish it. Retrying cannot stop a later recording").option("--runner <id>", runnerFlagDescription).action((recordingId, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRecord(ctx, {
38603
+ command: { action: "stop", recordingId },
38604
+ runner: opts.runner
38605
+ }, runnerDeps(ctx)))(opts, command));
38606
+ declareCommandKind(record.command("status"), "read").description("Show the active recording and automatic recording setting").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerRecord(ctx, {
38607
+ command: { action: "status" },
38608
+ runner: opts.runner
38609
+ }, runnerDeps(ctx)))(opts, command));
38610
+ declareCommandKind(record.command("auto"), "write").addArgument(new Argument2("<setting>", "Automatic recording setting").choices([
38611
+ "on",
38612
+ "off"
38613
+ ])).description("Enable or disable automatic recording for subsequent full runs").option("--runner <id>", runnerFlagDescription).action((setting, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRecord(ctx, {
38614
+ command: { action: "auto", enabled: setting === "on" },
38615
+ runner: opts.runner
38616
+ }, runnerDeps(ctx)))(opts, command));
38617
+ declareCommandKind(runner.command("list-recordings"), "read").description("Read a page of published video recordings, including after the runner terminates. Platform URLs persist; video URLs expire").option("--runner <id>", runnerFlagDescription).option("--recording-id <uuid>", "Look up one recording; an empty result means it is not published or does not exist").option("--page-token <token>", "Continue from nextPageToken returned by the previous page").action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerRecordings(ctx, {
38618
+ runner: opts.runner,
38619
+ ...opts.recordingId === undefined ? {} : { recordingId: opts.recordingId },
38620
+ ...opts.pageToken === undefined ? {} : { pageToken: opts.pageToken }
38621
+ }, runnerDeps(ctx)))(opts, command));
38622
+ }
38623
+
38418
38624
  // src/commands/runner/index.ts
38419
38625
  function registerRunnerCommand(program, signals) {
38420
38626
  const runner = program.command("runner").description("Drive an interactive runner on the QA Wolf platform");
@@ -38428,6 +38634,7 @@ function registerRunnerCommand(program, signals) {
38428
38634
  registerRunnerImportPackageCommand(runner, signals);
38429
38635
  registerRunnerHighlightSelectorCommand(runner, signals);
38430
38636
  registerRunnerPromoteSnapshotCommand(runner, signals);
38637
+ registerRunnerRecordingCommands(runner, signals);
38431
38638
  }
38432
38639
 
38433
38640
  // src/commands/program.ts
@@ -38458,4 +38665,4 @@ createProgram({ signals }).parseAsync().catch(() => {
38458
38665
  process.exitCode = 1;
38459
38666
  }).finally(() => exitWhenIdle(typeof process.exitCode === "number" ? process.exitCode : 0));
38460
38667
 
38461
- //# debugId=3E71BA5C9A67947964756E2164756E21
38668
+ //# debugId=5DFA383F9EE30BDB64756E2164756E21
@@ -10868,6 +10868,9 @@ async function listRunners(ctx, deps) {
10868
10868
  };
10869
10869
  }
10870
10870
 
10871
+ // src/domains/interactiveRunner/deps.ts
10872
+ import { randomUUID as randomUUID2 } from "node:crypto";
10873
+
10871
10874
  // src/shell/interactiveRunner/collectRunFiles.ts
10872
10875
  import { join as join2 } from "node:path";
10873
10876
 
@@ -12122,6 +12125,7 @@ function makeInteractiveRunnerDeps(options) {
12122
12125
  cwd: options.cwd,
12123
12126
  env: options.env,
12124
12127
  makeRunnerId,
12128
+ makeRecordingId: randomUUID2,
12125
12129
  readFile: (path) => options.fs.readFile(path),
12126
12130
  readStdin,
12127
12131
  runFilesManifest: makeRunFilesManifestStore({
@@ -12238,9 +12242,10 @@ function makeDefaultFs() {
12238
12242
  }
12239
12243
 
12240
12244
  // src/core/publicApi/notFoundSubject.ts
12241
- var runnerRoutesThatNameNoRunner = new Set([
12245
+ var runnerRoutesWithoutLivePod = new Set([
12242
12246
  "runner.launch",
12243
- "runner.list"
12247
+ "runner.list",
12248
+ "runner.recordings"
12244
12249
  ]);
12245
12250
  var runRoutesThatResolveOneRun = new Set([
12246
12251
  "run.diagnose",
@@ -12262,7 +12267,7 @@ function field(input, name) {
12262
12267
  return typeof value === "string" ? value : undefined;
12263
12268
  }
12264
12269
  function notFoundSubject(contractName, input) {
12265
- if (contractName.startsWith("runner.") && !runnerRoutesThatNameNoRunner.has(contractName)) {
12270
+ if (contractName.startsWith("runner.") && !runnerRoutesWithoutLivePod.has(contractName)) {
12266
12271
  return { kind: "runner", runnerId: field(input, "id") };
12267
12272
  }
12268
12273
  if (runRoutesThatResolveOneRun.has(contractName)) {
@@ -14802,12 +14807,63 @@ function createRunVerbs({ deps, platformClient }) {
14802
14807
  };
14803
14808
  }
14804
14809
 
14810
+ // src/domains/interactiveRunner/recordingRequests.ts
14811
+ async function recordOnRunner(ctx, runner, command) {
14812
+ const contract = publicContractsV1.runner.record;
14813
+ const parsed = contract.input.safeParse({ id: runner.runnerId, command });
14814
+ if (!parsed.success) {
14815
+ return {
14816
+ ok: false,
14817
+ error: "Invalid recording request. Runner ids must be valid and recording ids must be UUIDs.",
14818
+ exitCode: exitCodes.invalidArgs
14819
+ };
14820
+ }
14821
+ const result = await ctx.platformClient.callPublicApi(contract, parsed.data, runnerCallOptions);
14822
+ return result.ok ? result : { ok: false, ...runnerRequestFailure(result, runner) };
14823
+ }
14824
+ async function readRunnerRecordings(ctx, runner, query) {
14825
+ const contract = publicContractsV1.runner.recordings;
14826
+ const parsed = contract.input.safeParse({ id: runner.runnerId, ...query });
14827
+ if (!parsed.success) {
14828
+ return {
14829
+ ok: false,
14830
+ error: "Invalid recordings request. Use a valid runner id, a UUID recording id, and a page token of at most 4096 characters.",
14831
+ exitCode: exitCodes.invalidArgs
14832
+ };
14833
+ }
14834
+ const result = await ctx.platformClient.callPublicApi(contract, parsed.data);
14835
+ return result.ok ? result : { ok: false, ...runnerRequestFailure(result, runner) };
14836
+ }
14837
+
14838
+ // src/runnerSdk/recordingVerbs.ts
14839
+ function createRecordingVerbs(context) {
14840
+ return {
14841
+ async record({
14842
+ runnerId,
14843
+ command
14844
+ }) {
14845
+ return toSdkResult(await recordOnRunner(context, givenRunner(runnerId), command));
14846
+ },
14847
+ async listRecordings({
14848
+ runnerId,
14849
+ recordingId,
14850
+ pageToken
14851
+ }) {
14852
+ return toSdkResult(await readRunnerRecordings(context, givenRunner(runnerId), {
14853
+ ...recordingId === undefined ? {} : { recordingId },
14854
+ ...pageToken === undefined ? {} : { pageToken }
14855
+ }));
14856
+ }
14857
+ };
14858
+ }
14859
+
14805
14860
  // src/runnerSdk/index.ts
14806
14861
  function createRunnerSdk(options) {
14807
14862
  const context = createSdkContext(options);
14808
14863
  return {
14809
14864
  ...createLifecycleVerbs(context),
14810
14865
  ...createRunVerbs(context),
14866
+ ...createRecordingVerbs(context),
14811
14867
  ...createPageVerbs(context),
14812
14868
  ...createProjectVerbs(context),
14813
14869
  async list() {
@@ -14820,4 +14876,4 @@ export {
14820
14876
  createRunnerSdk
14821
14877
  };
14822
14878
 
14823
- //# debugId=F5992CBD2796CE7464756E2164756E21
14879
+ //# debugId=A7A37F8150999CFE64756E2164756E21
@@ -1,5 +1,5 @@
1
1
  import type { ListedRunner, RunnerSdkOptions, SdkResult } from "./types.js";
2
- export type { ActRequest, EvaluateSnippetRequest, EvaluatedSnippet, EventsRequest, FileSync, HighlightRequest, HighlightSelectorRequest, HighlightedSelector, ImportPackageRequest, ImportedPackage, InspectRequest, Inspected, Journal, JournalWindow, KeptAlive, LaunchRequest, LaunchedRunner, LinesLocation, ListedRunner, PackageVersion, PerformedAction, PromoteSnapshotRequest, PromotedSnapshot, RunEnvironment, RunFilter, RunRequest, RunSelection, RunnerFamily, RunnerRequest, RunnerSdkOptions, Screenshot, SdkResult, SnippetScope, StoppedRun, SubmittedRun, TerminatedRunner, } from "./types.js";
2
+ export type { ActRequest, EvaluateSnippetRequest, EvaluatedSnippet, EventsRequest, FileSync, HighlightRequest, HighlightSelectorRequest, HighlightedSelector, ImportPackageRequest, ImportedPackage, InspectRequest, Inspected, Journal, JournalWindow, KeptAlive, LaunchRequest, LaunchedRunner, LinesLocation, ListedRunner, PackageVersion, PerformedAction, PromoteSnapshotRequest, PromotedSnapshot, RecordRequest, RecordResponse, RecordingsRequest, Recordings, RunEnvironment, RunFilter, RunRequest, RunSelection, RunnerFamily, RunnerRequest, RunnerSdkOptions, Screenshot, SdkResult, SnippetScope, StoppedRun, SubmittedRun, TerminatedRunner, } from "./types.js";
3
3
  export type RunnerSdk = ReturnType<typeof createRunnerSdk>;
4
4
  /**
5
5
  * Drives interactive runners on the caller's team through the QA Wolf public
@@ -15,6 +15,8 @@ export declare function createRunnerSdk(options: RunnerSdkOptions): {
15
15
  inspect({ request, runnerId, }: import("./types.js").InspectRequest): Promise<SdkResult<import("./types.js").Inspected>>;
16
16
  promoteSnapshot({ baselinePath, runnerId, screenshotPath, }: import("./types.js").PromoteSnapshotRequest): Promise<SdkResult<import("./types.js").PromotedSnapshot>>;
17
17
  screenshot({ runnerId, }: import("./types.js").RunnerRequest): Promise<SdkResult<import("./types.js").Screenshot>>;
18
+ record({ runnerId, command, }: import("./types.js").RecordRequest): Promise<SdkResult<import("./types.js").RecordResponse>>;
19
+ listRecordings({ runnerId, recordingId, pageToken, }: import("./types.js").RecordingsRequest): Promise<SdkResult<import("./types.js").Recordings>>;
18
20
  events({ runFilter, runnerId, stream, window, }: import("./types.js").EventsRequest): Promise<SdkResult<import("./types.js").Journal>>;
19
21
  run({ entryPointPath, environment, runnerId, selection, }: import("./types.js").RunRequest): Promise<SdkResult<import("./types.js").SubmittedRun>>;
20
22
  keepalive({ runnerId, }: import("./types.js").RunnerRequest): Promise<SdkResult<import("./types.js").KeptAlive>>;
@@ -1,4 +1,4 @@
1
- import type { BrowserAction, InspectOnRunnerRequest, JournalStream, PublicApiOutput, ReadJournalResponse, RunnerNameForPublicApi, publicContractsV1 } from "@qawolf/api-contracts/v1";
1
+ import type { BrowserAction, InspectOnRunnerRequest, JournalStream, PublicApiInput, PublicApiOutput, ReadJournalResponse, RunnerNameForPublicApi, publicContractsV1 } from "@qawolf/api-contracts/v1";
2
2
  type Runner = typeof publicContractsV1.runner;
3
3
  export type RunnerSdkOptions = {
4
4
  /** A QA Wolf team API key. The SDK never reads the CLI's stored credentials. */
@@ -60,6 +60,12 @@ export type LaunchRequest = {
60
60
  export type RunnerRequest = {
61
61
  runnerId: string;
62
62
  };
63
+ /** Start and stop name the same UUID, so a retry cannot target a later capture. */
64
+ export type RecordRequest = RunnerRequest & Pick<PublicApiInput<Runner["record"]>, "command">;
65
+ export type RecordingsRequest = RunnerRequest & Pick<PublicApiInput<Runner["recordings"]>, "recordingId" | "pageToken">;
66
+ /** Runner refusal is carried on result.outcome, inside the platform answer. */
67
+ export type RecordResponse = PublicApiOutput<Runner["record"]>;
68
+ export type Recordings = PublicApiOutput<Runner["recordings"]>;
63
69
  export type RunRequest = RunnerRequest & {
64
70
  entryPointPath: string;
65
71
  environment: RunEnvironment;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qawolf/cli",
3
- "version": "1.33.0",
3
+ "version": "1.34.0",
4
4
  "description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
5
5
  "keywords": [
6
6
  "automation",
@@ -196,7 +196,12 @@ that `url`; never guess a route and never send a repository link in its place.
196
196
  | `qawolf runner keepalive` | read | Reset a runner's inactivity clock, for a caller that pauses between actions |
197
197
  | `qawolf runner launch` | write | Launch an interactive runner and make it this directory's default |
198
198
  | `qawolf runner list` | read | List the runners running on your team |
199
+ | `qawolf runner list-recordings` | read | Read a page of published video recordings, including after the runner terminates. Platform URLs persist; video URLs expire |
199
200
  | `qawolf runner promote-snapshot` | write | Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it |
201
+ | `qawolf runner record auto` | write | Enable or disable automatic recording for subsequent full runs |
202
+ | `qawolf runner record start` | write | Start manual video capture across runs. Requires a ready screen and suppresses automatic capture until stopped |
203
+ | `qawolf runner record status` | read | Show the active recording and automatic recording setting |
204
+ | `qawolf runner record stop` | write | Stop the recording with this UUID and publish it. Retrying cannot stop a later recording |
200
205
  | `qawolf runner run` | write | Run a flow on an interactive runner, shipping the flow and what it imports |
201
206
  | `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file, or write it to stdout with --out - |
202
207
  | `qawolf runner stop-run` | write | Stop what a runner is currently executing, leaving the runner up |
@@ -278,7 +283,8 @@ by default.
278
283
 
279
284
  The `runner` commands drive a live cloud browser: `launch` one, `screenshot` to
280
285
  see it, `act` to click and type, `run` a flow on it, `exec` a snippet against its
281
- page, `events` to read its journal (including the `recorder` stream, which turns
286
+ page, `record` to control video capture, `list-recordings` to read published video
287
+ history even after termination, `events` to read its journal (including the `recorder` stream, which turns
282
288
  your actions into Playwright locators), `keepalive` to hold it open, and
283
289
  `terminate`
284
290
  when done. Everything is a plain request to one host, so a shell with an API key
@@ -195,6 +195,44 @@ The answer holds one entry per action reached, and the field to read on each is
195
195
 
196
196
  The sequence stops at the first action that fails. `--continue-on-failure` carries on past one that reached the runner and did not take effect, which is only safe for actions that do not depend on each other: a `type` after a failed `click` goes to whatever has focus. A runner that cannot be reached, a screen that cannot serve, or running out of time ends the sequence either way, and the message names every action that did not succeed.
197
197
 
198
+ ## Video recording
199
+
200
+ Use `record` to control video capture on a Playwright runner. Start a flow first
201
+ so the runner has a screen, then start a manual capture:
202
+
203
+ ```sh
204
+ qawolf runner record start --runner ci --json
205
+ # Keep result.state.active.id from the response and use it to stop this capture:
206
+ qawolf runner record stop <recording-id> --runner ci
207
+ qawolf runner record status --runner ci
208
+ qawolf runner record auto on --runner ci
209
+ qawolf runner record auto off --runner ci
210
+ ```
211
+
212
+ Start generates a UUID unless you pass `--recording-id <uuid>`. If a start
213
+ response is lost, the error includes that UUID: check `record status` and reuse
214
+ the UUID when retrying. Stop always names a specific recording, so retrying it
215
+ cannot stop a later capture. Manual recordings span runs and suppress automatic
216
+ capture until stopped. The auto setting affects subsequent full runs.
217
+ An active automatic recording ends with its run; `record stop` cannot interrupt
218
+ it. If a stop publishes a recording with `status: "failed"`, the CLI returns
219
+ exit code 1 and still prints the manifest so callers can inspect it.
220
+
221
+ Published recordings remain accessible after the runner terminates:
222
+
223
+ ```sh
224
+ qawolf runner list-recordings --runner ci --json
225
+ qawolf runner list-recordings --runner ci --recording-id <uuid> --json
226
+ qawolf runner list-recordings --runner ci --page-token '<nextPageToken>' --json
227
+ ```
228
+
229
+ Each response is one page with `recordings` and an optional `nextPageToken`.
230
+ Follow that token for more; the order is storage-key order, not newest-first.
231
+ Entries include status, run ids, a stable platform `url`, and an expiring
232
+ `videoUrl` when video is available. An empty lookup means the recording has not
233
+ been published or does not exist. Abrupt runner loss may leave video unavailable.
234
+ Keep the runner id for history lookups after termination clears the local default.
235
+
198
236
  ## The recorder: what you cannot get from pixels
199
237
 
200
238
  `qawolf runner events recorder` is the capability that has no equivalent in a