@qawolf/cli 1.25.0 → 1.27.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.
@@ -6993,6 +6993,10 @@ var makeEmailResourceSchema = () => resource({
6993
6993
  replyTo: array(namedAddressSchema).optional(),
6994
6994
  text: string2().optional().describe("The plain text body.")
6995
6995
  }, { urlFieldDescription: emailUrlFieldDescription });
6996
+ var makeEmailAddressResourceSchema = () => resource({
6997
+ address: string2().describe("The inbox address. Mail sent to any plus-suffixed form, such as inbox+abc@example.com, lands in the same inbox."),
6998
+ isDefault: boolean2().describe("Whether this is the workspace's default inbox address.")
6999
+ }, { urlFieldDescription: emailUrlFieldDescription });
6996
7000
 
6997
7001
  // node_modules/@qawolf/api-contracts/dist/v1/identity/organization.js
6998
7002
  var identityOrganization = object({
@@ -7217,6 +7221,8 @@ var runnerNameSchema = _enum([
7217
7221
  "android",
7218
7222
  "ios"
7219
7223
  ]);
7224
+ var runnerWorkspaceRequirement = "Required when the credential is not bound to a single workspace, such as a user API key or an OAuth connection whose organization owns several.";
7225
+ var runnerWorkspaceIdDescription = `The workspace the runner belongs to. ${runnerWorkspaceRequirement}`;
7220
7226
  var runnerIdSchema = string2().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, "A runner id may contain only lowercase letters, digits and dashes, and must start and end with a letter or digit.");
7221
7227
  var makeRunnerSchema = () => object({
7222
7228
  gpuAccelerated: boolean2().describe("Whether the runner was placed on GPU hardware. Chosen by QA Wolf from the team's configuration and what the fleet can provide, not requested."),
@@ -7245,10 +7251,11 @@ var inspectRequestSchema = discriminatedUnion("what", [
7245
7251
  what: literal("variable")
7246
7252
  })
7247
7253
  ]);
7248
- var makeInspectOnRunnerContract = () => {
7254
+ var makeInspectOnRunnerContract = (ids) => {
7249
7255
  const input = object({
7250
7256
  id: runnerIdSchema.describe("Id of the runner to inspect."),
7251
- request: inspectRequestSchema
7257
+ request: inspectRequestSchema,
7258
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
7252
7259
  });
7253
7260
  const output = discriminatedUnion("outcome", [
7254
7261
  object({
@@ -7396,10 +7403,11 @@ var inspectMobileAnswerSchema = discriminatedUnion("what", [
7396
7403
  what: literal("elements")
7397
7404
  })
7398
7405
  ]);
7399
- var makeInspectMobileOnRunnerContract = () => {
7406
+ var makeInspectMobileOnRunnerContract = (ids) => {
7400
7407
  const input = object({
7401
7408
  id: runnerIdSchema.describe("Id of the runner to inspect."),
7402
- request: inspectMobileRequestSchema
7409
+ request: inspectMobileRequestSchema,
7410
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
7403
7411
  });
7404
7412
  const output = discriminatedUnion("outcome", [
7405
7413
  inspectMobileAnswerSchema,
@@ -7485,20 +7493,26 @@ var screenFailureReasons = [
7485
7493
  var screenNotReadyDescription = "`screen-not-ready` if the runner has a screen that cannot serve this instant. Retry in a second or two: the desktop restarts when a run changes the display size, and it serves one see-or-act request at a time, so a screenshot or action already in flight is the usual reason. Do not submit a run to clear this — a run may restart the display and discard what is on it.";
7486
7494
  var screenNeedsARunDescription = "`screen-needs-a-run` if the runner's virtual desktop has never started. Waiting will not change this and retrying is pointless — call `runner.runFlow` with a flow that opens a browser, then ask for the screen again. On a runner image with a browser that has never run anything, any `runner.performAction` also starts the browser itself. Evaluating a snippet does not start the desktop.";
7487
7495
  var runnerHasNoScreenDescription = "`runner-has-no-screen` if this runner is not one that runs a browser on a virtual desktop. Nothing about it can be seen or driven, and retrying will never help — launch a `playwright` runner instead.";
7496
+ var screenshotImageSchema = string2().min(1).describe("The screenshot, as a base64-encoded JPEG. Decode it and write the bytes to a `.jpg` file — writing this string to the file leaves you with base64 text rather than an image.");
7488
7497
 
7489
7498
  // node_modules/@qawolf/api-contracts/dist/v1/runner/performAction.js
7490
7499
  var unreachableDescription = "`runner-unreachable` if the runner could not be reached: it may still be starting, it may have terminated after inactivity, or it may have stopped answering mid-action. This does not mean the action was not performed — take a screenshot before repeating it.";
7491
7500
  var notSupportedOnMobileDescription = "`action-not-supported-on-mobile` if the runner is a mobile device and this action has no touchscreen equivalent: `double_click`, `scroll`, `move`, `keypress` and `navigate`, and a `click` whose `button` is not `left`, are all pointer-device concepts a touchscreen has nothing to offer for. `click` taps, `drag` swipes between its path's first and last point, and `type` types into whatever the last tap focused.";
7501
+ var withScreenshotDescription = "With `withScreenshot: true`, the answer also carries `imageJpegBase64`: a screenshot taken after the action, once the screen has changed from just before it or half a second has passed, whichever comes first. One call instead of `runner.performAction` followed by `runner.takeScreenshot`, with no fixed wait in between. Anything animating on the page counts as a change. The action itself is performed exactly as without the option; if the screen could serve no frame after it — a display restart, or a `navigate` on a desktop that is still starting — the action's result comes back without `imageJpegBase64`, so take a screenshot separately rather than repeating the action. On a mobile runner the screenshot is the device's own, taken right after a performed action.";
7492
7502
  var maxActionErrorMessageLength = 1000;
7493
- var makePerformActionOnRunnerContract = () => {
7503
+ var makePerformActionOnRunnerContract = (ids) => {
7494
7504
  const input = object({
7495
7505
  action: browserActionSchema.describe("The action to perform. Coordinates are pixels on the screenshot's own coordinate space."),
7496
- id: runnerIdSchema.describe("Id of the runner to act on.")
7506
+ id: runnerIdSchema.describe("Id of the runner to act on."),
7507
+ withScreenshot: boolean2().optional().describe("Also answer with a screenshot taken after the action, once the screen has changed or half a second has passed. Saves the separate `runner.takeScreenshot` call and its fixed wait."),
7508
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
7497
7509
  });
7510
+ const screenshotAfterAction = screenshotImageSchema.optional();
7498
7511
  const failure = discriminatedUnion("failureReason", [
7499
7512
  object({
7500
7513
  errorMessage: string2().max(maxActionErrorMessageLength).describe("What stopped the action from taking effect."),
7501
7514
  failureReason: literal("action-failed"),
7515
+ imageJpegBase64: screenshotAfterAction,
7502
7516
  outcome: literal("failure")
7503
7517
  }),
7504
7518
  makeRunnerFailureSchema([
@@ -7508,11 +7522,14 @@ var makePerformActionOnRunnerContract = () => {
7508
7522
  ])
7509
7523
  ]);
7510
7524
  const output = discriminatedUnion("outcome", [
7511
- object({ outcome: literal("success") }),
7525
+ object({
7526
+ imageJpegBase64: screenshotAfterAction,
7527
+ outcome: literal("success")
7528
+ }),
7512
7529
  failure
7513
7530
  ]);
7514
7531
  return {
7515
- description: `Perform one raw browser action on an interactive runner: click, double_click, move, drag, scroll, keypress, type, or navigate. Coordinates are whole pixels on the runner's virtual desktop, in the same space as \`runner.takeScreenshot\`. One action per request, and the runner serves one at a time. The action shapes follow the computer-use vocabulary, minus \`screenshot\` (use \`runner.takeScreenshot\`) and \`wait\` (delay on the caller's side). A success means the action took effect. \`action-failed\`, with a reason, if it reached the runner and did not take effect. On a runner image with a browser, the first action on a runner that has never run anything starts its browser and waits for it, so it can take up to a minute to answer — no \`runner.runFlow\` is needed before acting; if the browser is still starting when the wait runs out, the answer is \`screen-not-ready\` and retrying converges. On a mobile runner, the same \`screen-needs-a-run\` and \`screen-not-ready\` outcomes mean no Appium session has started yet, or it did not answer this instant; \`runner.runFlow\` is what starts one, same as a browser. \`screen-needs-a-run\` if the browser could not be started that way — usually a runner whose runs all finished without starting its desktop; call \`runner.runFlow\` with a flow that opens a browser. ${screenNotReadyDescription} A \`navigate\` does not go through the screen, so a screen that is not ready does not stop it. ${runnerHasNoScreenDescription} ${notSupportedOnMobileDescription} ${unreachableDescription}`,
7532
+ description: `Perform one raw browser action on an interactive runner: click, double_click, move, drag, scroll, keypress, type, or navigate. Coordinates are whole pixels on the runner's virtual desktop, in the same space as \`runner.takeScreenshot\`. One action per request, and the runner serves one at a time. The action shapes follow the computer-use vocabulary, minus \`screenshot\` (use \`runner.takeScreenshot\`) and \`wait\` (delay on the caller's side). A success means the action took effect. \`action-failed\`, with a reason, if it reached the runner and did not take effect. On a runner image with a browser, the first action on a runner that has never run anything starts its browser and waits for it, so it can take up to a minute to answer — no \`runner.runFlow\` is needed before acting; if the browser is still starting when the wait runs out, the answer is \`screen-not-ready\` and retrying converges. On a mobile runner, the same \`screen-needs-a-run\` and \`screen-not-ready\` outcomes mean no Appium session has started yet, or it did not answer this instant; \`runner.runFlow\` is what starts one, same as a browser. \`screen-needs-a-run\` if the browser could not be started that way — usually a runner whose runs all finished without starting its desktop; call \`runner.runFlow\` with a flow that opens a browser. ${screenNotReadyDescription} A \`navigate\` does not go through the screen, so a screen that is not ready does not stop it. ${runnerHasNoScreenDescription} ${notSupportedOnMobileDescription} ${withScreenshotDescription} ${unreachableDescription}`,
7516
7533
  input,
7517
7534
  kind: "write",
7518
7535
  name: "runner.performAction",
@@ -7538,9 +7555,11 @@ var makeAgentGetContract = (ids) => {
7538
7555
  replies: array(agentReplySchema).describe("Everything the AI has said so far, oldest first. A reply carrying choices is a question the work is blocked on."),
7539
7556
  sessionId: ids.chatSession,
7540
7557
  status: agentSessionStatusSchema
7541
- }, { urlFieldDescription: "Absolute URL of the session in the QA Wolf app." });
7558
+ }, {
7559
+ urlFieldDescription: "Absolute URL of the live session in the QA Wolf app."
7560
+ });
7542
7561
  return {
7543
- description: 'Read what the QA Wolf AI has said and whether it is still working. Poll this after agent.send until the status settles. A status of "waiting-for-you" means the last reply is a question the work is blocked on, and answering it with agent.send is what unblocks it. Replies accumulate, so a caller that polls repeatedly sees the earlier ones again.',
7562
+ description: 'Monitor a QA Wolf AI session by reading its status and replies. After agent.send, share the returned session URL before monitoring. Wait 30 to 60 seconds between checks; do not call this in a tight loop. Replies accumulate, so compare them with what you have already seen. Continue monitoring silently when the status and replies are unchanged; do not narrate waiting, announce the next check, or ask whether to keep monitoring. Report only substantive new progress, questions, blockers, or the final outcome. A status of "waiting-for-you" means the last reply is a question the work is blocked on, and answering it with agent.send is what unblocks it. Surface an explicit request for user input even if the status still says "working". Include the session URL when reporting a blocker or final outcome. On "completed", stop status checks and verify the requested result before claiming success. For new flows, validation, publication in the target environment, and readiness are separate checks; a Git push or final reply does not prove the flow is active. If every requested result is verified but status remains "working", report the mismatch and stop monitoring. Stop on "failed" or "cancelled" and report any confirmed partial result.',
7544
7563
  input,
7545
7564
  kind: "read",
7546
7565
  name: "agent.get",
@@ -7559,9 +7578,11 @@ var makeAgentSendContract = (ids) => {
7559
7578
  const output = resource({
7560
7579
  sessionId: ids.chatSession.describe("Follow the work by passing this to agent.get."),
7561
7580
  status: agentSessionStatusSchema
7562
- }, { urlFieldDescription: "Absolute URL of the session in the QA Wolf app." });
7581
+ }, {
7582
+ urlFieldDescription: "Absolute URL of the live session in the QA Wolf app."
7583
+ });
7563
7584
  return {
7564
- description: "Ask the QA Wolf AI to do a piece of work in plain language, such as covering a user journey, investigating a failing run, or fixing a broken flow. This is the one verb that starts work from nothing: every other write acts on a flow, run or issue that already exists. Answers as soon as the request is accepted, with the id to follow it by, because the work runs for minutes to tens of minutes. Poll agent.get for progress, and send here again to answer a question or add context to work already running.",
7585
+ description: "Start or continue work with the QA Wolf AI and return a live session URL to share with the user. Use it to cover a user journey, investigate a failing run, or fix a broken flow. This is the one verb that starts work from nothing: every other write acts on a flow, run or issue that already exists. Returns sessionId, status, and url as soon as the request is accepted; work can take minutes to tens of minutes. After each send, make the next action a normal user-visible assistant message containing the exact returned url, before any tool call or wait. Tool output and internal reasoning do not count as sharing the link. Do not run a timer or monitoring call alongside this send. Acceptance does not mean the work is complete. Then monitor the session with agent.get, reporting new progress, blockers, and the final outcome rather than unchanged status. Send here again to answer a question or add context to the same session.",
7565
7586
  input,
7566
7587
  kind: "write",
7567
7588
  name: "agent.send",
@@ -7664,10 +7685,7 @@ var makeListEmailAddressesContract = (ids) => {
7664
7685
  ...makePaginationInputFields({ defaultLimit: 20, maxLimit: 50 })
7665
7686
  });
7666
7687
  const output = object({
7667
- addresses: array(resource({
7668
- address: string2().describe("The inbox address. Mail sent to any plus-suffixed form, such as inbox+abc@example.com, lands in the same inbox."),
7669
- isDefault: boolean2().describe("Whether this is the workspace's default inbox address.")
7670
- }, { urlFieldDescription: emailUrlFieldDescription })).describe("The workspace's permanent inbox addresses, alphabetical."),
7688
+ addresses: array(makeEmailAddressResourceSchema()).describe("The workspace's permanent inbox addresses, alphabetical."),
7671
7689
  nextCursor: nextCursorSchema
7672
7690
  });
7673
7691
  return {
@@ -7679,6 +7697,28 @@ var makeListEmailAddressesContract = (ids) => {
7679
7697
  };
7680
7698
  };
7681
7699
 
7700
+ // node_modules/@qawolf/api-contracts/dist/v1/email/registerAddress.js
7701
+ var hasNoPlusSuffix = (address) => {
7702
+ const [localPart] = address.split("@");
7703
+ return localPart !== undefined && !localPart.includes("+");
7704
+ };
7705
+ var makeRegisterEmailAddressContract = (ids) => {
7706
+ const input = object({
7707
+ address: email2().refine(hasNoPlusSuffix, {
7708
+ message: "Register the base address. Mail to a plus-suffixed form lands in the base inbox."
7709
+ }).describe("The inbox address to register. Its domain must be one QA Wolf serves for the workspace."),
7710
+ workspaceId: ids.workspace.optional().describe("The workspace to register the address in. Required when authenticating with an organization or user API key.")
7711
+ });
7712
+ const output = makeEmailAddressResourceSchema();
7713
+ return {
7714
+ description: "Register an inbox address for the workspace. Registering an address the workspace already has changes nothing. A refusal names the domains the workspace can use.",
7715
+ input,
7716
+ kind: "write",
7717
+ name: "email.registerAddress",
7718
+ output
7719
+ };
7720
+ };
7721
+
7682
7722
  // node_modules/@qawolf/api-contracts/dist/v1/email/send.js
7683
7723
  var recipientsSchema = array(email2()).max(50);
7684
7724
  var maxAttachments = 10;
@@ -8319,14 +8359,31 @@ var makeReattemptRunContract = (ids) => {
8319
8359
  };
8320
8360
  };
8321
8361
 
8362
+ // node_modules/@qawolf/api-contracts/dist/v1/run/stop.js
8363
+ var makeStopRunContract = (ids) => {
8364
+ const input = object({ runId: ids.run });
8365
+ const output = resource({
8366
+ runId: ids.run,
8367
+ status: _enum(["accepted", "already-finished", "execution-not-found"]).describe('"accepted" means stopping was requested; poll run.get for the final result. "already-finished" means the run has already finished and its result is unchanged. "execution-not-found" means the run exists but no execution was found to stop; it may still be starting or may no longer be available. No stop request was accepted in that case.')
8368
+ }, { urlFieldDescription: "Absolute URL of the run page." });
8369
+ return {
8370
+ description: "Stop a run, including its queued flows and automatic retries. Stopping is asynchronous. Repeated requests are safe, and finished runs keep their results. A run that is still being created returns not found; retry once run.get returns the run. If run.get returns a different runId, use that ID. Poll run.get for results.",
8371
+ input,
8372
+ kind: "write",
8373
+ name: "run.stop",
8374
+ output
8375
+ };
8376
+ };
8377
+
8322
8378
  // node_modules/@qawolf/api-contracts/dist/v1/runner/evaluateSnippet.js
8323
8379
  var maxSnippetCodeLength = 64 * 1024;
8324
- var makeEvaluateSnippetOnRunnerContract = () => {
8380
+ var makeEvaluateSnippetOnRunnerContract = (ids) => {
8325
8381
  const input = object({
8326
8382
  code: string2().min(1).max(maxSnippetCodeLength).describe("The code to evaluate against the runner's live page, in the scope of `filePath` when one is given."),
8327
8383
  filePath: runFilePathSchema.optional().describe("Path of the file the snippet is evaluated in the scope of. Omit to evaluate a snippet that imports nothing of yours."),
8328
8384
  files: runFilesSchema.optional().describe("The file named by `filePath` and everything it imports. A runner holds no copy of your project, so a snippet's scope has to travel with it."),
8329
- id: runnerIdSchema.describe("Id of the runner to evaluate the snippet on.")
8385
+ id: runnerIdSchema.describe("Id of the runner to evaluate the snippet on."),
8386
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
8330
8387
  }).refine((request) => request.filePath === undefined || Object.hasOwn(request.files ?? {}, request.filePath), {
8331
8388
  error: "A snippet naming a file must carry that file in files.",
8332
8389
  path: ["files"]
@@ -8352,9 +8409,10 @@ var makeEvaluateSnippetOnRunnerContract = () => {
8352
8409
  };
8353
8410
 
8354
8411
  // node_modules/@qawolf/api-contracts/dist/v1/runner/get.js
8355
- var makeGetRunnerContract = () => {
8412
+ var makeGetRunnerContract = (ids) => {
8356
8413
  const input = object({
8357
- id: runnerIdSchema.describe("Id of the runner to look up.")
8414
+ id: runnerIdSchema.describe("Id of the runner to look up."),
8415
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
8358
8416
  });
8359
8417
  const output = object({
8360
8418
  id: runnerIdSchema.describe("Id of the runner the call addressed."),
@@ -8372,10 +8430,11 @@ var makeGetRunnerContract = () => {
8372
8430
 
8373
8431
  // node_modules/@qawolf/api-contracts/dist/v1/runner/highlightSelector.js
8374
8432
  var maxHighlightSelectorLength = 2000;
8375
- var makeHighlightSelectorOnRunnerContract = () => {
8433
+ var makeHighlightSelectorOnRunnerContract = (ids) => {
8376
8434
  const input = object({
8377
8435
  id: runnerIdSchema.describe("Id of the runner to highlight on."),
8378
- selector: string2().max(maxHighlightSelectorLength).describe("The selector to highlight. An empty string clears whatever is highlighted.")
8436
+ selector: string2().max(maxHighlightSelectorLength).describe("The selector to highlight. An empty string clears whatever is highlighted."),
8437
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
8379
8438
  });
8380
8439
  const output = discriminatedUnion("outcome", [
8381
8440
  object({
@@ -8410,12 +8469,13 @@ var makeHighlightSelectorOnRunnerContract = () => {
8410
8469
  // node_modules/@qawolf/api-contracts/dist/v1/runner/importPackage.js
8411
8470
  var maxPackageNameLength = 214;
8412
8471
  var maxPackageVersionLength = 256;
8413
- var makeImportPackageOnRunnerContract = () => {
8472
+ var makeImportPackageOnRunnerContract = (ids) => {
8414
8473
  const input = object({
8415
8474
  id: runnerIdSchema.describe("Id of the runner to install into."),
8416
8475
  npmDependencies: record(string2(), string2()).describe("The run's current npm dependencies, so the install resolves against them."),
8417
8476
  packageName: string2().min(1).max(maxPackageNameLength),
8418
- packageVersion: string2().min(1).max(maxPackageVersionLength)
8477
+ packageVersion: string2().min(1).max(maxPackageVersionLength),
8478
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
8419
8479
  });
8420
8480
  const output = discriminatedUnion("outcome", [
8421
8481
  object({ outcome: literal("success") }),
@@ -8435,10 +8495,11 @@ var makeImportPackageOnRunnerContract = () => {
8435
8495
  };
8436
8496
 
8437
8497
  // node_modules/@qawolf/api-contracts/dist/v1/runner/index.js
8438
- var makeLaunchRunnerContract = () => {
8498
+ var makeLaunchRunnerContract = (ids) => {
8439
8499
  const input = object({
8440
8500
  id: runnerIdSchema.describe("Id to launch the runner under, chosen by the caller. Launching the same id again returns the runner already running under it."),
8441
- runnerName: runnerNameSchema.optional().describe("The runner family to launch. Defaults to the standard web family, playwright.")
8501
+ runnerName: runnerNameSchema.optional().describe("The runner family to launch. Defaults to the standard web family, playwright."),
8502
+ workspaceId: ids.workspace.optional().describe(`The workspace to launch the runner in. ${runnerWorkspaceRequirement}`)
8442
8503
  });
8443
8504
  const output = makeRunnerSchema().extend({
8444
8505
  alreadyRunning: boolean2().describe("True when a runner was already running under this id and was returned unchanged, so no second runner was started and nothing new is billed. Not an error either way."),
@@ -8452,9 +8513,10 @@ var makeLaunchRunnerContract = () => {
8452
8513
  output
8453
8514
  };
8454
8515
  };
8455
- var makeTerminateRunnerContract = () => {
8516
+ var makeTerminateRunnerContract = (ids) => {
8456
8517
  const input = object({
8457
- id: runnerIdSchema.describe("Id of the runner to terminate.")
8518
+ id: runnerIdSchema.describe("Id of the runner to terminate."),
8519
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
8458
8520
  });
8459
8521
  const output = object({
8460
8522
  id: runnerIdSchema.describe("Id of the runner the call addressed."),
@@ -8471,14 +8533,16 @@ var makeTerminateRunnerContract = () => {
8471
8533
  };
8472
8534
 
8473
8535
  // node_modules/@qawolf/api-contracts/dist/v1/runner/list.js
8474
- var makeListRunnersContract = () => {
8475
- const input = object({});
8536
+ var makeListRunnersContract = (ids) => {
8537
+ const input = object({
8538
+ workspaceId: ids.workspace.optional().describe(`The workspace whose runners to list. ${runnerWorkspaceRequirement}`)
8539
+ });
8476
8540
  const output = object({
8477
8541
  outcome: literal("success"),
8478
8542
  runners: array(makeRunnerSchema()).describe("The runners running right now, in no particular order. Each entry carries the same fields a launch reports, so any of them can be addressed like a runner you launched yourself.")
8479
8543
  });
8480
8544
  return {
8481
- description: "List the runners running on the caller's team right now. Takes no arguments: the API key already names the team. A runner that was terminated, or that terminated on its own after inactivity, is not listed, because the run system keeps no record of a runner once it is gone. This is a lookup: it never starts a runner, and it does not reset a runner's inactivity clock the way reading its journal does. It is how to find a runner launched from another machine or in an earlier session, when the id it was launched under is no longer at hand.",
8545
+ description: "List the runners running on the caller's team right now. A team API key already names the team and sends nothing; a credential bound to no single workspace names one in `workspaceId`. A runner that was terminated, or that terminated on its own after inactivity, is not listed, because the run system keeps no record of a runner once it is gone. This is a lookup: it never starts a runner, and it does not reset a runner's inactivity clock the way reading its journal does. It is how to find a runner launched from another machine or in an earlier session, when the id it was launched under is no longer at hand.",
8482
8546
  input,
8483
8547
  kind: "read",
8484
8548
  name: "runner.list",
@@ -8488,11 +8552,12 @@ var makeListRunnersContract = () => {
8488
8552
 
8489
8553
  // node_modules/@qawolf/api-contracts/dist/v1/runner/promoteSnapshot.js
8490
8554
  var maxSnapshotPathLength = 1024;
8491
- var makePromoteSnapshotOnRunnerContract = () => {
8555
+ var makePromoteSnapshotOnRunnerContract = (ids) => {
8492
8556
  const input = object({
8493
8557
  baselinePath: string2().min(1).max(maxSnapshotPathLength).describe("The baseline to replace, as named in the image diff."),
8494
8558
  id: runnerIdSchema.describe("Id of the runner holding the screenshot."),
8495
- screenshotPath: string2().min(1).max(maxSnapshotPathLength).describe("The screenshot to promote, as named in the image diff.")
8559
+ screenshotPath: string2().min(1).max(maxSnapshotPathLength).describe("The screenshot to promote, as named in the image diff."),
8560
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
8496
8561
  });
8497
8562
  const output = discriminatedUnion("outcome", [
8498
8563
  object({ outcome: literal("success") }),
@@ -8515,10 +8580,11 @@ var makePromoteSnapshotOnRunnerContract = () => {
8515
8580
  };
8516
8581
 
8517
8582
  // node_modules/@qawolf/api-contracts/dist/v1/runner/readJournal.js
8518
- var makeReadRunnerJournalContract = () => {
8583
+ var makeReadRunnerJournalContract = (ids) => {
8519
8584
  const input = readJournalRequestSchema.extend({
8520
8585
  id: runnerIdSchema.describe("Id of the runner to read the journal of."),
8521
- stream: journalStreamSchema.describe(`The stream to read. QA Wolf writes ${knownJournalStreams.join(", ")}; an unwritten stream reads as empty rather than as an error.`)
8586
+ stream: journalStreamSchema.describe(`The stream to read. QA Wolf writes ${knownJournalStreams.join(", ")}; an unwritten stream reads as empty rather than as an error.`),
8587
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
8522
8588
  });
8523
8589
  const output = discriminatedUnion("outcome", [
8524
8590
  readJournalResponseSchema.extend({ outcome: literal("success") }),
@@ -8553,7 +8619,8 @@ var makeRunFlowOnRunnerContract = (ids) => {
8553
8619
  files: runFilesSchema.describe("Every file the run needs keyed by its path — the flow file, everything it imports, package.json and tsconfig.json. A runner holds no copy of your project, so what runs is exactly what is sent here."),
8554
8620
  id: runnerIdSchema.describe("Id of the runner to run the flow on."),
8555
8621
  selection: runSelectionSchema.optional().describe("Run only these lines, inside the page the runner is already on, instead of running the whole flow from a fresh browser. Omit to run the whole entry point."),
8556
- unchangedFiles: unchangedFilesSchema.optional().describe("Files this runner already holds from an earlier run, keyed by path with a hex SHA-256 of the content it should hold. Send this to make `files` only what changed. The runner refuses with `needs-full-sync` if it holds none of a referenced path, so it never runs something other than what was asked for. Omit it to send every file.")
8622
+ unchangedFiles: unchangedFilesSchema.optional().describe("Files this runner already holds from an earlier run, keyed by path with a hex SHA-256 of the content it should hold. Send this to make `files` only what changed. The runner refuses with `needs-full-sync` if it holds none of a referenced path, so it never runs something other than what was asked for. Omit it to send every file."),
8623
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
8557
8624
  }).refine((request) => request.env === undefined || request.environmentId === undefined, {
8558
8625
  error: "Send env or environmentId, not both. An environment's variables are taken as they are stored, so there is no order in which the two would combine.",
8559
8626
  path: ["environmentId"]
@@ -8605,9 +8672,10 @@ var makeRunFlowOnRunnerContract = (ids) => {
8605
8672
  };
8606
8673
 
8607
8674
  // node_modules/@qawolf/api-contracts/dist/v1/runner/stopRun.js
8608
- var makeStopRunOnRunnerContract = () => {
8675
+ var makeStopRunOnRunnerContract = (ids) => {
8609
8676
  const input = object({
8610
- id: runnerIdSchema.describe("Id of the runner to stop the run on.")
8677
+ id: runnerIdSchema.describe("Id of the runner to stop the run on."),
8678
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
8611
8679
  });
8612
8680
  const output = discriminatedUnion("outcome", [
8613
8681
  object({
@@ -8626,13 +8694,14 @@ var makeStopRunOnRunnerContract = () => {
8626
8694
  };
8627
8695
 
8628
8696
  // node_modules/@qawolf/api-contracts/dist/v1/runner/takeScreenshot.js
8629
- var makeTakeScreenshotOnRunnerContract = () => {
8697
+ var makeTakeScreenshotOnRunnerContract = (ids) => {
8630
8698
  const input = object({
8631
- id: runnerIdSchema.describe("Id of the runner to take a screenshot of.")
8699
+ id: runnerIdSchema.describe("Id of the runner to take a screenshot of."),
8700
+ workspaceId: ids.workspace.optional().describe(runnerWorkspaceIdDescription)
8632
8701
  });
8633
8702
  const output = discriminatedUnion("outcome", [
8634
8703
  object({
8635
- imageJpegBase64: string2().min(1).describe("The screenshot, as a base64-encoded JPEG. Decode it and write the bytes to a `.jpg` file — writing this string to the file leaves you with base64 text rather than an image."),
8704
+ imageJpegBase64: screenshotImageSchema,
8636
8705
  outcome: literal("success")
8637
8706
  }),
8638
8707
  makeRunnerFailureSchema([
@@ -8686,6 +8755,7 @@ var makeContractsV1 = (ids) => {
8686
8755
  get: makeGetEmailContract(resolvedIds),
8687
8756
  getAttachment: makeGetEmailAttachmentContract(resolvedIds),
8688
8757
  listAddresses: makeListEmailAddressesContract(resolvedIds),
8758
+ registerAddress: makeRegisterEmailAddressContract(resolvedIds),
8689
8759
  send: makeSendEmailContract(resolvedIds)
8690
8760
  },
8691
8761
  environment: {
@@ -8717,24 +8787,25 @@ var makeContractsV1 = (ids) => {
8717
8787
  diagnose: makeDiagnoseRunContract(resolvedIds),
8718
8788
  find: makeFindRunsContract(resolvedIds),
8719
8789
  get: makeGetRunContract(resolvedIds),
8720
- reattempt: makeReattemptRunContract(resolvedIds)
8790
+ reattempt: makeReattemptRunContract(resolvedIds),
8791
+ stop: makeStopRunContract(resolvedIds)
8721
8792
  },
8722
8793
  runner: {
8723
- evaluateSnippet: makeEvaluateSnippetOnRunnerContract(),
8724
- get: makeGetRunnerContract(),
8725
- highlightSelector: makeHighlightSelectorOnRunnerContract(),
8726
- importPackage: makeImportPackageOnRunnerContract(),
8727
- inspect: makeInspectOnRunnerContract(),
8728
- inspectMobile: makeInspectMobileOnRunnerContract(),
8729
- launch: makeLaunchRunnerContract(),
8730
- list: makeListRunnersContract(),
8731
- performAction: makePerformActionOnRunnerContract(),
8732
- promoteSnapshot: makePromoteSnapshotOnRunnerContract(),
8733
- readJournal: makeReadRunnerJournalContract(),
8794
+ evaluateSnippet: makeEvaluateSnippetOnRunnerContract(resolvedIds),
8795
+ get: makeGetRunnerContract(resolvedIds),
8796
+ highlightSelector: makeHighlightSelectorOnRunnerContract(resolvedIds),
8797
+ importPackage: makeImportPackageOnRunnerContract(resolvedIds),
8798
+ inspect: makeInspectOnRunnerContract(resolvedIds),
8799
+ inspectMobile: makeInspectMobileOnRunnerContract(resolvedIds),
8800
+ launch: makeLaunchRunnerContract(resolvedIds),
8801
+ list: makeListRunnersContract(resolvedIds),
8802
+ performAction: makePerformActionOnRunnerContract(resolvedIds),
8803
+ promoteSnapshot: makePromoteSnapshotOnRunnerContract(resolvedIds),
8804
+ readJournal: makeReadRunnerJournalContract(resolvedIds),
8734
8805
  runFlow: makeRunFlowOnRunnerContract(resolvedIds),
8735
- stopRun: makeStopRunOnRunnerContract(),
8736
- takeScreenshot: makeTakeScreenshotOnRunnerContract(),
8737
- terminate: makeTerminateRunnerContract()
8806
+ stopRun: makeStopRunOnRunnerContract(resolvedIds),
8807
+ takeScreenshot: makeTakeScreenshotOnRunnerContract(resolvedIds),
8808
+ terminate: makeTerminateRunnerContract(resolvedIds)
8738
8809
  },
8739
8810
  tag: {
8740
8811
  create: makeCreateTagContract(resolvedIds),
@@ -9039,7 +9110,17 @@ var interactMessages = {
9039
9110
  actionMayHaveHappened: "The runner could not be reached, which does not mean the action was not performed: it may have stopped answering mid-action. Take a screenshot before repeating it.",
9040
9111
  actionNotJson: `Stdin did not hold a JSON action. Pipe one object, for example '{"type":"click","button":"left","x":480,"y":260}'.`,
9041
9112
  actionNotSupportedOnMobile: (type) => `A mobile runner has a touchscreen, so it cannot perform ${type} as asked. It taps with a left-button click, swipes with drag, and types into whatever the last tap focused.`,
9113
+ actionFailedScreenshotToStdout: "Its screen was written to stdout as a JPEG, so look at that rather than sending the action again.",
9114
+ actionFailedScreenshotUnwritten: (detail) => `Its screen could not be written, because ${detail}, so take one with qawolf runner screenshot rather than sending the action again.`,
9115
+ actionFailedScreenshotWritten: (path) => `Its screen was written to ${path}, so look at that rather than sending the action again.`,
9116
+ actionFailedWithoutScreenshot: "The runner answered without a screen, so nothing was written; take one with qawolf runner screenshot rather than sending the action again.",
9042
9117
  actionPerformed: (type) => `Performed ${type}.`,
9118
+ actionPerformedScreenshotNotAnImage: (type) => `Performed ${type}, but the screen that came with the answer was not a JPEG, so nothing was written. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot instead, and report it if it keeps happening.`,
9119
+ actionPerformedScreenshotToStdout: (type) => `Performed ${type} and wrote the runner's screen to stdout as a JPEG. Stdout holds the image bytes alone; this line, and the JSON with --json, is on stderr.`,
9120
+ actionPerformedScreenshotStdoutUnwritable: (type, detail) => `Performed ${type}, but its screen could not be written to stdout: ${detail}. The action took effect, so do not repeat it: keep the pipe reading stdout open, or take the screen with qawolf runner screenshot --out <file>.`,
9121
+ actionPerformedScreenshotUnwritable: (type, path, detail) => `Performed ${type}, but its screen could not be written to "${path}": ${detail}. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot, giving --out a path this process can write to.`,
9122
+ actionPerformedScreenshotWritten: (type, path) => `Performed ${type} and wrote the runner's screen to ${path}.`,
9123
+ actionPerformedWithoutScreenshot: (type) => `Performed ${type}, but the runner answered without the screen it was asked for. The action took effect, so do not repeat it: take the screen with qawolf runner screenshot instead. If this keeps happening, the platform or this CLI is behind the other: upgrade with npm install -g @qawolf/cli.`,
9043
9124
  actionAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
9044
9125
  screenshotAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
9045
9126
  runnerHasNoScreen: "This runner does not run a browser on a virtual desktop, so there is nothing about it to see or drive. Retrying will never help: launch a playwright runner instead.",
@@ -9051,8 +9132,11 @@ var interactMessages = {
9051
9132
  screenNeedsARun: "This runner has not run anything yet, so its screen has never started. Waiting will not clear this and there is nothing to retry: run a flow on it with qawolf runner run, then ask again. Evaluating a snippet does not start a screen.",
9052
9133
  screenNotReady: "The runner has a screen and cannot serve this yet. Its virtual desktop restarts when a run changes the display size, and it serves one request at a time, so something already in flight is the usual reason. Retry in a second or two.",
9053
9134
  screenshotNotAnImage: "The screen was captured but did not arrive as a JPEG, so nothing was written. Nothing about the command needs changing: try it again, and report it if it keeps happening.",
9135
+ stdoutIsATerminal: (flag) => `Stdout is a terminal, so the JPEG bytes would have nowhere to go. Redirect stdout to a file or pipe it into a reader, or give ${flag} a file path instead of "-".`,
9136
+ screenshotStdoutUnwritable: (detail) => `The screen was captured but could not be written to stdout: ${detail}. Keep the pipe reading stdout open, or give --out a file path instead of "-".`,
9054
9137
  screenshotUnwritable: (path, detail) => `The screen was captured but could not be written to "${path}": ${detail}. Give --out a path this process can write to.`,
9055
9138
  screenshotWritten: (path) => `Wrote the runner's screen to ${path}.`,
9139
+ screenshotWrittenToStdout: "Wrote the runner's screen to stdout as a JPEG. Stdout holds the image bytes alone; this line, and the JSON with --json, is on stderr.",
9056
9140
  snippetEmpty: (path) => `"${path}" holds no code to evaluate.`,
9057
9141
  snippetErrored: (errorMessage) => errorMessage === undefined ? "The snippet threw and reported no message." : `The snippet threw: ${errorMessage}`,
9058
9142
  snippetFileUnreadable: (path) => `Could not read "${path}". Name a readable file, or "-" to read the snippet from stdin.`,
@@ -10482,19 +10566,38 @@ function isTimeoutError(err) {
10482
10566
 
10483
10567
  // src/shell/interactiveRunner/writeScreenshot.ts
10484
10568
  var jpegStartOfImage = [255, 216, 255];
10569
+ var stdoutPath = "-";
10485
10570
  async function writeScreenshot(options) {
10486
10571
  const bytes = Buffer.from(options.imageJpegBase64, "base64");
10487
10572
  if (jpegStartOfImage.some((byte, index) => bytes[index] !== byte)) {
10488
10573
  return { ok: false, reason: "not-a-jpeg" };
10489
10574
  }
10490
10575
  try {
10491
- await options.fs.mkdir(dirname3(options.path), { recursive: true });
10492
- await options.fs.writeFile(options.path, bytes);
10576
+ if (options.path === stdoutPath) {
10577
+ await writeToStdout(options.stdout ?? process.stdout, bytes);
10578
+ } else {
10579
+ await options.fs.mkdir(dirname3(options.path), { recursive: true });
10580
+ await options.fs.writeFile(options.path, bytes);
10581
+ }
10493
10582
  } catch (error) {
10494
10583
  return { detail: errorMessage(error), ok: false, reason: "unwritable" };
10495
10584
  }
10496
10585
  return { ok: true };
10497
10586
  }
10587
+ function writeToStdout(stdout, bytes) {
10588
+ return new Promise((resolve, reject) => {
10589
+ const onError = (error) => reject(error);
10590
+ stdout.once("error", onError);
10591
+ stdout.write(bytes, (error) => {
10592
+ if (error) {
10593
+ reject(error);
10594
+ return;
10595
+ }
10596
+ stdout.off("error", onError);
10597
+ resolve();
10598
+ });
10599
+ });
10600
+ }
10498
10601
 
10499
10602
  // src/shell/stdin.ts
10500
10603
  async function readStdin() {
@@ -12323,6 +12426,34 @@ async function writeTeamStorageAssets(args, deps) {
12323
12426
  }
12324
12427
  }
12325
12428
 
12429
+ // src/shell/platform/teamStorageMethods.ts
12430
+ function createTeamStorageMethods(trpc, deps, fs, getIdentity) {
12431
+ async function list() {
12432
+ if (deps.workspaceId !== undefined) {
12433
+ return listTeamStorageFiles(trpc, { teamId: deps.workspaceId }, deps);
12434
+ }
12435
+ const identity = await getIdentity();
12436
+ if (!identity.ok)
12437
+ return identity;
12438
+ if (!("team" in identity.value)) {
12439
+ return {
12440
+ ok: false,
12441
+ error: flowsMessages.pull.teamStorageRequiresTeamKey
12442
+ };
12443
+ }
12444
+ return listTeamStorageFiles(trpc, { teamId: identity.value.team.id }, deps);
12445
+ }
12446
+ return {
12447
+ listTeamStorageFiles: list,
12448
+ async syncTeamStorageAssets(assetsAbs, opts) {
12449
+ const files = await list();
12450
+ if (!files.ok)
12451
+ return files;
12452
+ return downloadTeamStorageAssets({ assetsAbs, files: files.value }, { fetch: deps.fetch, fs, onProgress: opts?.onProgress });
12453
+ }
12454
+ };
12455
+ }
12456
+
12326
12457
  // src/shell/platform/createPlatformClient.ts
12327
12458
  var requestBackoffMs2 = [500, 1500];
12328
12459
  function createPlatformClient(apiKey, deps) {
@@ -12339,8 +12470,10 @@ function createPlatformClient(apiKey, deps) {
12339
12470
  return result;
12340
12471
  return { ok: true, value: { signedUrl: result.value.url } };
12341
12472
  }
12473
+ const identityMethods = createIdentityMethods(apiKey, deps, requestBackoffMs2);
12342
12474
  return {
12343
- ...createIdentityMethods(apiKey, deps, requestBackoffMs2),
12475
+ ...identityMethods,
12476
+ ...createTeamStorageMethods(trpc, deps, fs, identityMethods.getIdentity),
12344
12477
  getFlowsBundleUrl: getFlowsBundleUrlImpl,
12345
12478
  callPublicApi: makeCallPublicApiMethod(trpc, deps, requestBackoffMs2),
12346
12479
  async getEnvVars(envId) {
@@ -12354,24 +12487,6 @@ function createPlatformClient(apiKey, deps) {
12354
12487
  return result;
12355
12488
  return { ok: true, value: result.value.environmentVariables };
12356
12489
  },
12357
- async listTeamStorageFiles() {
12358
- const identity = await this.getIdentity();
12359
- if (!identity.ok)
12360
- return identity;
12361
- if (!("team" in identity.value)) {
12362
- return {
12363
- ok: false,
12364
- error: flowsMessages.pull.teamStorageRequiresTeamKey
12365
- };
12366
- }
12367
- return listTeamStorageFiles(trpc, { teamId: identity.value.team.id }, deps);
12368
- },
12369
- async syncTeamStorageAssets(assetsAbs, opts) {
12370
- const files = await this.listTeamStorageFiles();
12371
- if (!files.ok)
12372
- return files;
12373
- return downloadTeamStorageAssets({ assetsAbs, files: files.value }, { fetch: deps.fetch, fs, onProgress: opts?.onProgress });
12374
- },
12375
12490
  async downloadBundle(envId) {
12376
12491
  const urlResult = await getFlowsBundleUrlImpl(envId);
12377
12492
  if (!urlResult.ok)
@@ -12492,9 +12607,14 @@ function createPageVerbs({ platformClient }) {
12492
12607
  return {
12493
12608
  async act({
12494
12609
  action,
12495
- runnerId
12610
+ runnerId,
12611
+ withScreenshot
12496
12612
  }) {
12497
- return toSdkResult(await platformClient.callPublicApi(performAction, { action, id: runnerId }, runnerCallOptions));
12613
+ return toSdkResult(await platformClient.callPublicApi(performAction, {
12614
+ action,
12615
+ id: runnerId,
12616
+ ...withScreenshot === undefined ? {} : { withScreenshot }
12617
+ }, runnerCallOptions));
12498
12618
  },
12499
12619
  async highlightSelector({
12500
12620
  highlight,
@@ -13094,4 +13214,4 @@ export {
13094
13214
  createRunnerSdk
13095
13215
  };
13096
13216
 
13097
- //# debugId=DD6DEC33C9C6693F64756E2164756E21
13217
+ //# debugId=37E313924A61E7F564756E2164756E21
@@ -10,7 +10,7 @@ export declare function createRunnerSdk(options: RunnerSdkOptions): {
10
10
  list(): Promise<SdkResult<ListedRunner[]>>;
11
11
  evaluateSnippet({ runnerId, scope, source, }: import("./types.js").EvaluateSnippetRequest): Promise<SdkResult<import("./types.js").EvaluatedSnippet>>;
12
12
  importPackage({ name, runnerId, version, }: import("./types.js").ImportPackageRequest): Promise<SdkResult<import("./types.js").ImportedPackage>>;
13
- act({ action, runnerId, }: import("./types.js").ActRequest): Promise<SdkResult<import("./types.js").PerformedAction>>;
13
+ act({ action, runnerId, withScreenshot, }: import("./types.js").ActRequest): Promise<SdkResult<import("./types.js").PerformedAction>>;
14
14
  highlightSelector({ highlight, runnerId, }: import("./types.js").HighlightSelectorRequest): Promise<SdkResult<import("./types.js").HighlightedSelector>>;
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>>;
@@ -66,6 +66,13 @@ export type EventsRequest = RunnerRequest & {
66
66
  };
67
67
  export type ActRequest = RunnerRequest & {
68
68
  action: BrowserAction;
69
+ /**
70
+ * Ask the runner to answer with a screenshot taken after the action, on
71
+ * `imageJpegBase64`. One call instead of an act and a screenshot, with no
72
+ * fixed wait between them. An action that did not take effect answers with
73
+ * one too, and the screen can be absent even when asked for.
74
+ */
75
+ withScreenshot?: boolean;
69
76
  };
70
77
  export type EvaluateSnippetRequest = RunnerRequest & {
71
78
  scope: SnippetScope;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qawolf/cli",
3
- "version": "1.25.0",
3
+ "version": "1.27.0",
4
4
  "description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
5
5
  "keywords": [
6
6
  "automation",
@@ -71,7 +71,7 @@
71
71
  "@clack/prompts": "1.5.1",
72
72
  "@napi-rs/keyring": "1.3.0",
73
73
  "@oxc-node/core": "0.1.0",
74
- "@qawolf/api-contracts": "0.47.0",
74
+ "@qawolf/api-contracts": "0.52.0",
75
75
  "@qawolf/emails": "1.1.1",
76
76
  "@qawolf/flow-targets": "1.0.0",
77
77
  "@qawolf/flows": "0.1.4",