@qawolf/cli 1.27.0 → 1.29.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/dist/cli.js CHANGED
@@ -8805,6 +8805,7 @@ var interactMessages = {
8805
8805
  nothingToInspect: (errorMessage) => `The runner had nothing to inspect${errorMessage === undefined ? "" : `: ${errorMessage}`}. There is no live page, nothing matched the selector, or no variable has that name; a runner cannot tell those apart. Run a flow on it first, or check the selector or name.`,
8806
8806
  inspectAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
8807
8807
  inspectMobileAnsweredUnknown: (failureReason) => `The runner answered "${failureReason}", which this version of the CLI does not know how to report. Upgrade with npm install -g @qawolf/cli.`,
8808
+ inspectMobileInvalidSelector: "The runner could not parse that selector under the strategy it was given. Retrying it unchanged will never help: correct the selector, or pick the strategy that matches it.",
8808
8809
  runnerIsNotMobile: "This runner is not a mobile device, so there is nothing here to inspect. Retrying will never help: launch an android or ios runner instead.",
8809
8810
  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.",
8810
8811
  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.",
@@ -16409,6 +16410,72 @@ var makeEmailAddressResourceSchema = () => resource({
16409
16410
  isDefault: boolean2().describe("Whether this is the workspace's default inbox address.")
16410
16411
  }, { urlFieldDescription: emailUrlFieldDescription });
16411
16412
 
16413
+ // node_modules/@qawolf/api-contracts/dist/v1/file/path.js
16414
+ var uploadedFilePrefix = "uploads";
16415
+ var controlOrBackslash = /[\p{Cc}\\]/u;
16416
+ var shellMetacharacter = /["'`$;|&<>]/u;
16417
+ var segmentsOf = (path) => path.split("/");
16418
+ var teamStoragePathSchema = string2().min(1).max(200).refine((path) => !controlOrBackslash.test(path), "A path cannot carry control characters or a backslash.").refine((path) => !shellMetacharacter.test(path), "A path cannot carry a quote, a backtick, or any of $ ; | & < >. A reader passes the path to a shell, where those change what runs.").refine((path) => segmentsOf(path).every((segment) => segment.length > 0), "A path cannot start or end with a slash, or carry an empty segment.").refine((path) => segmentsOf(path).every((segment) => segment !== "." && segment !== ".."), "A path cannot climb out of the team's storage.").refine((path) => segmentsOf(path).every((segment) => segment.trim() === segment), "A path cannot have leading or trailing whitespace in a segment.");
16419
+ var uploadedFilePath = (fileName) => `${uploadedFilePrefix}/${fileName}`;
16420
+ var uploadedFileNameSchema = teamStoragePathSchema.refine((fileName) => teamStoragePathSchema.safeParse(uploadedFilePath(fileName)).success, `A name has to still be a valid path once it sits under \`${uploadedFilePrefix}/\`, which every other endpoint reads it back as.`);
16421
+
16422
+ // node_modules/@qawolf/api-contracts/dist/v1/file/requestDownload.js
16423
+ var makeRequestFileDownloadContract = (ids) => {
16424
+ const input = object({
16425
+ filePath: teamStoragePathSchema.describe("The file's path in team storage, as file.requestUpload answered with."),
16426
+ workspaceId: ids.workspace.optional().describe("The workspace the file belongs to. Required when authenticating with an organization or user API key.")
16427
+ });
16428
+ const output = object({
16429
+ expiresAt: exports_iso.datetime().describe("When readUrl stops working. Request another one after it."),
16430
+ path: string2().describe("The file's path in team storage."),
16431
+ readUrl: string2().describe('Fetch the file\'s bytes from here, for example `curl -o journeys.csv "<readUrl>"`. Write it to a file and read that file, rather than printing the contents, so a large file costs one line of output instead of all of it.')
16432
+ });
16433
+ return {
16434
+ annotations: {
16435
+ destructiveHint: false,
16436
+ openWorldHint: false,
16437
+ readOnlyHint: true
16438
+ },
16439
+ description: "Get a URL for reading a file out of the caller's team storage. Answers 404 when nothing is stored at that path.",
16440
+ input,
16441
+ kind: "read",
16442
+ name: "file.requestDownload",
16443
+ output
16444
+ };
16445
+ };
16446
+
16447
+ // node_modules/@qawolf/api-contracts/dist/v1/file/requestUpload.js
16448
+ var makeRequestFileUploadContract = (ids) => {
16449
+ const input = object({
16450
+ fileName: uploadedFileNameSchema.describe("What to call the file, such as `journeys.csv` or `q3/journeys.csv`. Spaces and non-ASCII are fine; backslashes, leading slashes and `..` segments are refused. The same name replaces the file."),
16451
+ workspaceId: ids.workspace.optional().describe("The workspace to upload into. Required when authenticating with an organization or user API key.")
16452
+ });
16453
+ const output = object({
16454
+ contentType: string2().describe("Send this as the PUT's `Content-Type` header, exactly. The URL is signed for it and refuses any other value."),
16455
+ expiresAt: exports_iso.datetime().describe("When uploadUrl stops working. Request another one after it."),
16456
+ path: string2().describe("Where the file lives in team storage. Send this to agent.send as a filePath, and the AI fetches it from there."),
16457
+ uploadUrl: string2().describe(`Send the file's bytes here with a single HTTP PUT carrying the returned contentType, for example \`curl -H 'Content-Type: application/octet-stream' --upload-file "journeys.csv" "<uploadUrl>"\`. Upload the bytes from the shell rather than through this API, so a large file never has to be written out as text.`)
16458
+ });
16459
+ return {
16460
+ annotations: {
16461
+ destructiveHint: true,
16462
+ openWorldHint: false,
16463
+ readOnlyHint: false
16464
+ },
16465
+ description: "Get a URL to put a file into team storage: a spreadsheet of journeys, anything too large to paste. PUT with the returned contentType, then name the path in filePaths.",
16466
+ input,
16467
+ kind: "write",
16468
+ name: "file.requestUpload",
16469
+ output
16470
+ };
16471
+ };
16472
+
16473
+ // node_modules/@qawolf/api-contracts/dist/v1/file/index.js
16474
+ var makeFileContracts = (ids) => ({
16475
+ requestDownload: makeRequestFileDownloadContract(ids),
16476
+ requestUpload: makeRequestFileUploadContract(ids)
16477
+ });
16478
+
16412
16479
  // node_modules/@qawolf/api-contracts/dist/v1/identity/organization.js
16413
16480
  var identityOrganization = object({
16414
16481
  id: string2(),
@@ -16465,6 +16532,7 @@ var defaultIdSchemas = {
16465
16532
  flow: string2().min(1).describe("The id of the flow."),
16466
16533
  issue: string2().min(1).describe("The id of the issue."),
16467
16534
  run: string2().min(1).describe("The id of the run."),
16535
+ trigger: string2().min(1).describe("The id of the trigger."),
16468
16536
  workspace: string2().min(1).describe("The id of the workspace.")
16469
16537
  };
16470
16538
 
@@ -16500,6 +16568,11 @@ var makeCreateRunContract = (ids) => {
16500
16568
  tracking: _enum(["registered", "failed", "not-requested"]).describe('Whether the conversation named by aiTaskId or chatSessionId will receive run status updates: "registered" when it will, "failed" when the run was created but the registration failed, "not-requested" when neither id was given or the AI task has no conversation to notify.')
16501
16569
  }, { urlFieldDescription: "Absolute URL of the run page." });
16502
16570
  return {
16571
+ annotations: {
16572
+ destructiveHint: true,
16573
+ openWorldHint: true,
16574
+ readOnlyHint: false
16575
+ },
16503
16576
  description: "Create a run for the selected flows and/or tags in an environment.",
16504
16577
  input,
16505
16578
  kind: "write",
@@ -16637,7 +16710,8 @@ var runnerIdSchema = string2().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9
16637
16710
  var makeRunnerSchema = () => object({
16638
16711
  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."),
16639
16712
  id: runnerIdSchema.describe("The runner's id, as supplied when launching it."),
16640
- runnerName: runnerNameSchema.describe("The runner family requested when the runner was launched. QA Wolf may place it on faster hardware, reported in gpuAccelerated.")
16713
+ runnerName: runnerNameSchema.describe("The runner family requested when the runner was launched. QA Wolf may place it on faster hardware, reported in gpuAccelerated."),
16714
+ url: url().describe("A QA Wolf page showing what the runner is doing right now, where the person who launched it can watch and take over with their own mouse and keyboard. Opening it requires being signed in to QA Wolf.")
16641
16715
  });
16642
16716
  // node_modules/@qawolf/api-contracts/dist/v1/runner/unreachable.js
16643
16717
  var runnerUnreachableFailureReason = "runner-unreachable";
@@ -16682,7 +16756,12 @@ var makeInspectOnRunnerContract = (ids) => {
16682
16756
  })
16683
16757
  ]);
16684
16758
  return {
16685
- description: "Inspect one thing on an interactive runner: an element's HTML, the page's HTML simplified for a model, or a top-level variable's value as JSON. `nothing-to-inspect` means the runner had nothing to answer with: no live page, no element matching the selector, or no variable under that name. A runner cannot tell those apart, so it reports the one reason and puts whatever it did say in `errorMessage`. `runner-is-not-a-browser` on a mobile runner — call `runner.inspectMobile` for its equivalent surface instead. " + retryableRunnerUnreachableDescription,
16759
+ annotations: {
16760
+ destructiveHint: false,
16761
+ openWorldHint: false,
16762
+ readOnlyHint: false
16763
+ },
16764
+ description: "Inspect one thing on an interactive runner: an element's HTML, the page's HTML simplified for a model, or a top-level variable's value as JSON. This refreshes the inactivity timer and can extend the runner's billed lifetime. `nothing-to-inspect` means the runner had nothing to answer with: no live page, no element matching the selector, or no variable under that name. A runner cannot tell those apart, so it reports the one reason and puts whatever it did say in `errorMessage`. `runner-is-not-a-browser` on a mobile runner — call `runner.inspectMobile` for its equivalent surface instead. " + retryableRunnerUnreachableDescription,
16686
16765
  input,
16687
16766
  kind: "read",
16688
16767
  name: "runner.inspect",
@@ -16742,6 +16821,30 @@ function makeRunnerFailureSchema(reasons) {
16742
16821
 
16743
16822
  // node_modules/@qawolf/api-contracts/dist/v1/runner/inspectMobile.js
16744
16823
  var maxElementTextLength = 500;
16824
+ var selectorStrategySchema = _enum(["ios-predicate", "shadow", "xpath"]);
16825
+ var elementsRequestSchema = discriminatedUnion("by", [
16826
+ object({
16827
+ by: literal("point"),
16828
+ context: string2().min(1).optional(),
16829
+ what: literal("elements"),
16830
+ x: int().min(0).describe("Whole pixels on the device's own screen."),
16831
+ y: int().min(0).describe("Whole pixels on the device's own screen.")
16832
+ }),
16833
+ object({
16834
+ by: literal("text"),
16835
+ context: string2().min(1).optional(),
16836
+ partial: boolean2().optional().describe("Match text containing this, rather than exactly this."),
16837
+ text: string2().min(1).max(maxElementTextLength),
16838
+ what: literal("elements")
16839
+ }),
16840
+ object({
16841
+ by: literal("selector"),
16842
+ context: string2().min(1).optional(),
16843
+ selector: string2().min(1).max(maxSelectorLength).describe("Resolved the same way a screen object's own selector is."),
16844
+ strategy: selectorStrategySchema.default("xpath"),
16845
+ what: literal("elements")
16846
+ })
16847
+ ]).describe("Elements at a screen point, elements carrying some text, or the elements a selector resolves to.");
16745
16848
  var inspectMobileRequestSchema = discriminatedUnion("what", [
16746
16849
  object({
16747
16850
  what: literal("session").describe("The status of the Appium session driving the device: ready and what it is, or why not.")
@@ -16753,22 +16856,7 @@ var inspectMobileRequestSchema = discriminatedUnion("what", [
16753
16856
  context: string2().min(1).optional().describe("Read this context instead of whichever is current."),
16754
16857
  what: literal("page").describe("The page source of the device's current context, as a tree.")
16755
16858
  }),
16756
- discriminatedUnion("by", [
16757
- object({
16758
- by: literal("point"),
16759
- context: string2().min(1).optional(),
16760
- what: literal("elements"),
16761
- x: int().min(0).describe("Whole pixels on the device's own screen."),
16762
- y: int().min(0).describe("Whole pixels on the device's own screen.")
16763
- }),
16764
- object({
16765
- by: literal("text"),
16766
- context: string2().min(1).optional(),
16767
- partial: boolean2().optional().describe("Match text containing this, rather than exactly this."),
16768
- text: string2().min(1).max(maxElementTextLength),
16769
- what: literal("elements")
16770
- })
16771
- ]).describe("Elements at a screen point, or elements carrying some text — never both at once.")
16859
+ elementsRequestSchema
16772
16860
  ]);
16773
16861
  var inspectMobileAnswerSchema = discriminatedUnion("what", [
16774
16862
  object({
@@ -16820,6 +16908,7 @@ var makeInspectMobileOnRunnerContract = (ids) => {
16820
16908
  const output = discriminatedUnion("outcome", [
16821
16909
  inspectMobileAnswerSchema,
16822
16910
  makeRunnerFailureSchema([
16911
+ "invalid-selector",
16823
16912
  "runner-is-not-mobile",
16824
16913
  "screen-needs-a-run",
16825
16914
  "screen-not-ready",
@@ -16827,7 +16916,12 @@ var makeInspectMobileOnRunnerContract = (ids) => {
16827
16916
  ])
16828
16917
  ]);
16829
16918
  return {
16830
- description: `Inspect one thing on a mobile interactive runner: the Appium session's status, the WebView contexts available, the current context's page source, or the elements at a point or carrying some text. Mobile only — a browser runner answers \`runner-is-not-mobile\`; call \`runner.inspect\` for a browser's equivalent surface instead. \`what: "session"\` always answers with the session's own status rather than \`screen-needs-a-run\`, since that is the question it exists to answer; the other three request kinds need a live session first and answer the same \`screen-needs-a-run\` or \`screen-not-ready\` outcomes \`runner.performAction\` and \`runner.takeScreenshot\` use, instead of reading anything when there is none. \`screen-needs-a-run\` means no Appium session has started on this runner yet — call \`runner.runFlow\` with a flow that opens one, then inspect again. \`screen-not-ready\` means the runner's Appium session exists but did not answer this instant, or more than one is somehow live — retry once; if it persists, relaunch the runner. \`runner-is-not-mobile\` means there is nothing here to inspect, and retrying will never help — launch an \`android\` or \`ios\` runner instead. ${retryableRunnerUnreachableDescription}`,
16919
+ annotations: {
16920
+ destructiveHint: false,
16921
+ openWorldHint: false,
16922
+ readOnlyHint: true
16923
+ },
16924
+ description: `Inspect one thing on a mobile interactive runner: the Appium session's status, the WebView contexts available, the current context's page source, or the elements at a point, carrying some text, or matching a selector. This does not refresh the runner's inactivity timer. Mobile only — a browser runner answers \`runner-is-not-mobile\`; call \`runner.inspect\` for a browser's equivalent surface instead. \`what: "session"\` always answers with the session's own status rather than \`screen-needs-a-run\`, since that is the question it exists to answer; the other three request kinds need a live session first and answer the same \`screen-needs-a-run\` or \`screen-not-ready\` outcomes \`runner.performAction\` and \`runner.takeScreenshot\` use, instead of reading anything when there is none. \`screen-needs-a-run\` means no Appium session has started on this runner yet — call \`runner.runFlow\` with a flow that opens one, then inspect again. \`screen-not-ready\` means the runner's Appium session exists but did not answer this instant, or more than one is somehow live — retry once; if it persists, relaunch the runner. \`runner-is-not-mobile\` means there is nothing here to inspect, and retrying will never help — launch an \`android\` or \`ios\` runner instead. \`invalid-selector\` (elements by selector only) means the selector itself could not be parsed under the given \`strategy\` — distinct from a selector that parsed fine but matched nothing, which answers \`matches: []\` instead. ${retryableRunnerUnreachableDescription}`,
16831
16925
  input,
16832
16926
  kind: "read",
16833
16927
  name: "runner.inspectMobile",
@@ -16935,6 +17029,11 @@ var makePerformActionOnRunnerContract = (ids) => {
16935
17029
  failure
16936
17030
  ]);
16937
17031
  return {
17032
+ annotations: {
17033
+ destructiveHint: true,
17034
+ openWorldHint: true,
17035
+ readOnlyHint: false
17036
+ },
16938
17037
  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}`,
16939
17038
  input,
16940
17039
  kind: "write",
@@ -16952,20 +17051,90 @@ var runSelectionSchema = object({
16952
17051
  error: "A selection's startLine must not be after its endLine.",
16953
17052
  path: ["startLine"]
16954
17053
  });
17054
+ // node_modules/@qawolf/api-contracts/dist/v1/trigger/configuration.js
17055
+ var isTimezone = (value) => {
17056
+ try {
17057
+ Intl.DateTimeFormat("en-US", { timeZone: value });
17058
+ return true;
17059
+ } catch {
17060
+ return false;
17061
+ }
17062
+ };
17063
+ var triggerCadenceSchema = _enum(["hourly", "daily"]).describe("How often the schedule fires.");
17064
+ var triggerActionKindSchema = _enum(["createSuite", "generativeSuite"]).describe("createSuite runs the flows named by flowIds and tagNames. generativeSuite lets QA Wolf choose the flows from the deployment's changes, and is not available to a schedule.");
17065
+ var makeTriggerActionSchema = (ids) => object({
17066
+ flowIds: array(ids.flow).default([]).describe("Flows to run. Combined with tagNames."),
17067
+ instructions: string2().min(1).optional().describe("Guidance for QA Wolf. Applies to generativeSuite only."),
17068
+ investigateFailures: boolean2().optional().describe("Whether QA Wolf investigates failures in the resulting runs. Defaults to true."),
17069
+ kind: triggerActionKindSchema.default("createSuite"),
17070
+ tagNames: array(string2().min(1)).default([]).describe("Tags naming the flows to run. Combined with flowIds.")
17071
+ }).describe("What the trigger runs when it fires.");
17072
+ var makeTriggerScheduleFields = (ids) => ({
17073
+ action: makeTriggerActionSchema(ids),
17074
+ cadence: triggerCadenceSchema,
17075
+ environmentId: ids.environmentId.describe("The environment the scheduled runs happen in."),
17076
+ minuteOfHour: number2().int().min(0).max(59).optional().describe("Minutes past the hour an hourly schedule fires. Defaults to 0."),
17077
+ timeOfDay: string2().regex(/^([01]\d|2[0-3]):[0-5]\d$/).optional().describe('The wall clock time a daily schedule fires, as "HH:mm". Required when cadence is daily.'),
17078
+ timezoneId: string2().min(1).refine(isTimezone, "Expected an IANA timezone, such as America/New_York.").optional().describe("The IANA timezone that timeOfDay is read in, such as America/New_York. Required when cadence is daily.")
17079
+ });
17080
+ var makeTriggerDeploymentFields = (ids) => ({
17081
+ action: makeTriggerActionSchema(ids),
17082
+ branchPattern: string2().min(1).optional().describe("Only fire for deployments of a branch matching this pattern."),
17083
+ deployTargetPattern: string2().min(1).optional().describe("Only fire for deployments of an app matching this pattern. A deploy target names one app within an environment."),
17084
+ environmentIds: array(ids.environment).default([]).describe("Only fire for deployments to these environments. Mutually exclusive with environmentPattern."),
17085
+ environmentPattern: string2().min(1).optional().describe("Only fire for deployments to environments matching this pattern. Mutually exclusive with environmentIds.")
17086
+ });
17087
+
17088
+ // node_modules/@qawolf/api-contracts/dist/v1/trigger/resource.js
17089
+ var makeTriggerConfigurationSchema = (ids) => discriminatedUnion("kind", [
17090
+ object({
17091
+ ...makeTriggerScheduleFields({
17092
+ environmentId: ids.environment,
17093
+ flow: ids.flow
17094
+ }),
17095
+ kind: literal("schedule")
17096
+ }),
17097
+ object({
17098
+ ...makeTriggerDeploymentFields(ids),
17099
+ kind: literal("deployment")
17100
+ }),
17101
+ object({
17102
+ actionCount: number2().int().describe("How many actions it runs."),
17103
+ kind: literal("advanced"),
17104
+ whenCount: number2().int().describe("How many separate match conditions it has.")
17105
+ })
17106
+ ]);
17107
+ var makeTriggerResourceSchema = (ids) => resource({
17108
+ configuration: makeTriggerConfigurationSchema(ids),
17109
+ createdAt: exports_iso.datetime(),
17110
+ id: ids.trigger,
17111
+ isPaused: boolean2().describe("A paused trigger never fires, on any path."),
17112
+ name: string2(),
17113
+ nextScheduledAt: exports_iso.datetime().optional().describe("When a schedule trigger next fires. Absent for a deployment trigger, and while a schedule trigger is paused."),
17114
+ pausedAt: exports_iso.datetime().optional().describe("When it was paused."),
17115
+ updatedAt: exports_iso.datetime()
17116
+ }, { urlFieldDescription: "Absolute URL of the team's triggers page." });
16955
17117
  // node_modules/@qawolf/api-contracts/dist/v1/agent/get.js
16956
17118
  var makeAgentGetContract = (ids) => {
16957
17119
  const input = object({
17120
+ cursor: string2().min(1).optional().describe("Opaque cursor copied from this same session's previous nextCursor. Omit to read from its first reply."),
16958
17121
  sessionId: ids.chatSession.describe("The id agent.send answered with.")
16959
17122
  });
16960
17123
  const output = resource({
16961
- 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."),
17124
+ nextCursor: string2().min(1).describe("Send back as cursor on the next check to read only what follows. Always present, even with no replies. Belongs to this session alone."),
17125
+ replies: array(agentReplySchema).describe("What the AI has said since the cursor, oldest first. A reply still being written returns again, longer. A reply carrying choices is a question the work is blocked on."),
16962
17126
  sessionId: ids.chatSession,
16963
17127
  status: agentSessionStatusSchema
16964
17128
  }, {
16965
17129
  urlFieldDescription: "Absolute URL of the live session in the QA Wolf app."
16966
17130
  });
16967
17131
  return {
16968
- 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.',
17132
+ annotations: {
17133
+ destructiveHint: false,
17134
+ openWorldHint: false,
17135
+ readOnlyHint: true
17136
+ },
17137
+ 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. Pass the nextCursor from one response as the cursor on the next check; it then reads only what is new, and only for the session that minted it. Continue monitoring silently when the status is unchanged and no replies come back; 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.',
16969
17138
  input,
16970
17139
  kind: "read",
16971
17140
  name: "agent.get",
@@ -16977,6 +17146,7 @@ var makeAgentGetContract = (ids) => {
16977
17146
  var makeAgentSendContract = (ids) => {
16978
17147
  const input = object({
16979
17148
  environmentId: ids.environmentRef.optional().describe("The environment to work in. Omit to use the team's default environment. Ignored when sessionId is given, because a session keeps the environment it opened in."),
17149
+ filePaths: array(teamStoragePathSchema).max(20).optional().describe("Paths of files this request is about, from file.requestUpload. The AI reads them from storage rather than the message, so a plan of hundreds of journeys costs nothing to send."),
16980
17150
  message: string2().trim().min(1).max(1e4).describe("What you want QA Wolf to do, in plain language. Name the journey, the part of the app it covers, and anything the AI cannot discover for itself, such as a test account, a feature flag, or how to reach a staging environment. When answering a question from agent.get, send the answer here."),
16981
17151
  sessionId: ids.chatSession.optional().describe("Continue the session with this id instead of opening a new one. Omit to start fresh. Send this to answer a question or to add context to work already running."),
16982
17152
  workspaceId: ids.workspace.optional().describe("The workspace to work in. Required when authenticating with an organization or user API key.")
@@ -16988,6 +17158,11 @@ var makeAgentSendContract = (ids) => {
16988
17158
  urlFieldDescription: "Absolute URL of the live session in the QA Wolf app."
16989
17159
  });
16990
17160
  return {
17161
+ annotations: {
17162
+ destructiveHint: true,
17163
+ openWorldHint: true,
17164
+ readOnlyHint: false
17165
+ },
16991
17166
  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.",
16992
17167
  input,
16993
17168
  kind: "write",
@@ -17006,6 +17181,11 @@ var makeAutomateContract = (ids) => {
17006
17181
  });
17007
17182
  const output = resource({ automationId: ids.automation }, { urlFieldDescription: "Absolute URL of the automation review page." });
17008
17183
  return {
17184
+ annotations: {
17185
+ destructiveHint: true,
17186
+ openWorldHint: true,
17187
+ readOnlyHint: false
17188
+ },
17009
17189
  description: "Request automation for draft flows. First create a named local .flow.ts draft for every requested journey that does not already have a matching draft; never reuse a generic starter or placeholder. Each new draft must start with a JSDoc Goal: description, import flow from @qawolf/flows/web, and use export default flow(...); a comment-only file or direct test(...) call is not a valid draft. Commit and push all changes with Git to publish them, then list remote drafts to resolve every selected ID. Do not use patch to create or rename a selected flow. Finally make one automation request containing all requested flow IDs.",
17010
17190
  input,
17011
17191
  kind: "write",
@@ -17037,6 +17217,11 @@ var makeFindEmailsContract = (ids) => {
17037
17217
  nextCursor: nextCursorSchema
17038
17218
  });
17039
17219
  return {
17220
+ annotations: {
17221
+ destructiveHint: false,
17222
+ openWorldHint: false,
17223
+ readOnlyHint: true
17224
+ },
17040
17225
  description: "List the workspace's inbox, or its sent mail, newest first. Read a message body with email.get.",
17041
17226
  input,
17042
17227
  kind: "read",
@@ -17053,6 +17238,11 @@ var makeGetEmailContract = (ids) => {
17053
17238
  });
17054
17239
  const output = makeEmailResourceSchema();
17055
17240
  return {
17241
+ annotations: {
17242
+ destructiveHint: false,
17243
+ openWorldHint: false,
17244
+ readOnlyHint: true
17245
+ },
17056
17246
  description: "Read one email of the workspace, with its plain text and HTML bodies. Use it to pull a sign-in code or a verification link out of a message.",
17057
17247
  input,
17058
17248
  kind: "read",
@@ -17076,6 +17266,11 @@ var makeGetEmailAttachmentContract = (ids) => {
17076
17266
  type: string2().optional().describe("The MIME type.")
17077
17267
  }, { urlFieldDescription: emailUrlFieldDescription });
17078
17268
  return {
17269
+ annotations: {
17270
+ destructiveHint: false,
17271
+ openWorldHint: false,
17272
+ readOnlyHint: true
17273
+ },
17079
17274
  description: "Read one attachment of a workspace email as base64 content, by file name or by position. email.get lists both.",
17080
17275
  input,
17081
17276
  kind: "read",
@@ -17095,6 +17290,11 @@ var makeListEmailAddressesContract = (ids) => {
17095
17290
  nextCursor: nextCursorSchema
17096
17291
  });
17097
17292
  return {
17293
+ annotations: {
17294
+ destructiveHint: false,
17295
+ openWorldHint: false,
17296
+ readOnlyHint: true
17297
+ },
17098
17298
  description: "List the workspace's inbox addresses, alphabetical. A flow can sign up with a plus-suffixed form of any of them, and email.find reads what arrives.",
17099
17299
  input,
17100
17300
  kind: "read",
@@ -17117,6 +17317,11 @@ var makeRegisterEmailAddressContract = (ids) => {
17117
17317
  });
17118
17318
  const output = makeEmailAddressResourceSchema();
17119
17319
  return {
17320
+ annotations: {
17321
+ destructiveHint: false,
17322
+ openWorldHint: false,
17323
+ readOnlyHint: false
17324
+ },
17120
17325
  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.",
17121
17326
  input,
17122
17327
  kind: "write",
@@ -17150,6 +17355,11 @@ var makeSendEmailContract = (ids) => {
17150
17355
  urlFieldDescription: emailUrlFieldDescription
17151
17356
  });
17152
17357
  return {
17358
+ annotations: {
17359
+ destructiveHint: true,
17360
+ openWorldHint: true,
17361
+ readOnlyHint: false
17362
+ },
17153
17363
  description: "Send an email from one of the workspace's inbox addresses, for example to exercise a flow that reacts to incoming mail. Returns the sent email; read it back with email.get.",
17154
17364
  input,
17155
17365
  kind: "write",
@@ -17168,6 +17378,11 @@ var makeDeleteEnvironmentVariableContract = (ids) => {
17168
17378
  name: string2().describe("The canonicalized name that was removed. Returned even when the environment had no such variable.")
17169
17379
  });
17170
17380
  return {
17381
+ annotations: {
17382
+ destructiveHint: true,
17383
+ openWorldHint: false,
17384
+ readOnlyHint: false
17385
+ },
17171
17386
  description: "Remove one environment variable by name. Succeeds whether or not the variable existed.",
17172
17387
  input,
17173
17388
  kind: "write",
@@ -17213,6 +17428,11 @@ var makeFindEnvironmentsContract = (ids) => {
17213
17428
  nextCursor: nextCursorSchema
17214
17429
  });
17215
17430
  return {
17431
+ annotations: {
17432
+ destructiveHint: false,
17433
+ openWorldHint: false,
17434
+ readOnlyHint: true
17435
+ },
17216
17436
  description: "List the team's environments, newest first.",
17217
17437
  input,
17218
17438
  kind: "read",
@@ -17235,6 +17455,11 @@ var makeGetEnvironmentVariableContract = (ids) => {
17235
17455
  })).describe("The found variables, sorted by name.")
17236
17456
  });
17237
17457
  return {
17458
+ annotations: {
17459
+ destructiveHint: false,
17460
+ openWorldHint: false,
17461
+ readOnlyHint: true
17462
+ },
17238
17463
  description: "Read the values of named environment variables in one call. Values are secrets. Names that do not exist go to missingNames and do not fail the call.",
17239
17464
  input,
17240
17465
  kind: "read",
@@ -17250,6 +17475,11 @@ var makeGetEnvironmentContract = (ids) => {
17250
17475
  });
17251
17476
  const output = makeEnvironmentResourceSchema(ids);
17252
17477
  return {
17478
+ annotations: {
17479
+ destructiveHint: false,
17480
+ openWorldHint: false,
17481
+ readOnlyHint: true
17482
+ },
17253
17483
  description: "Read a single environment's name, kind, standing run health, flow-code branch and reconciliation state, run concurrency limit, and termination state. If flowCodeBranch exists, use its syncStatus for Git reconciliation and read lastSyncedCommitHash only when syncStatus is reconciled.",
17254
17484
  input,
17255
17485
  kind: "read",
@@ -17265,6 +17495,11 @@ var makeListEnvironmentVariableNamesContract = (ids) => {
17265
17495
  variableNames: array(string2()).describe("Names of the environment's variables, sorted alphabetically. Values are never returned.")
17266
17496
  });
17267
17497
  return {
17498
+ annotations: {
17499
+ destructiveHint: false,
17500
+ openWorldHint: false,
17501
+ readOnlyHint: true
17502
+ },
17268
17503
  description: "Use this to answer which QA Wolf environment variables are available to test code. Returns names only; values never leave the server.",
17269
17504
  input,
17270
17505
  kind: "read",
@@ -17282,6 +17517,11 @@ var makeSetEnvironmentVariableContract = (ids) => {
17282
17517
  name: string2().describe("The stored variable name after whitespace is replaced with underscores and letters are uppercased.")
17283
17518
  });
17284
17519
  return {
17520
+ annotations: {
17521
+ destructiveHint: true,
17522
+ openWorldHint: false,
17523
+ readOnlyHint: false
17524
+ },
17285
17525
  description: 'Create or replace an environment variable. If the user asks to create one for "my email" without naming it, use DEFAULT_EMAIL. The value is never returned.',
17286
17526
  input,
17287
17527
  kind: "write",
@@ -17296,7 +17536,12 @@ var makeCreateEnvironmentContract = (ids) => {
17296
17536
  });
17297
17537
  const output = makeEnvironmentResourceSchema(ids);
17298
17538
  return {
17299
- description: "Create an environment on the caller's team and return it in the environment.get shape.",
17539
+ annotations: {
17540
+ destructiveHint: false,
17541
+ openWorldHint: true,
17542
+ readOnlyHint: false
17543
+ },
17544
+ description: "Create an environment on the caller's team and return it in the environment.get shape. This can also create a branch on the team's connected Git provider.",
17300
17545
  input,
17301
17546
  kind: "write",
17302
17547
  name: "environment.create",
@@ -17317,6 +17562,11 @@ var makeUpdateEnvironmentContract = (ids) => {
17317
17562
  });
17318
17563
  const output = makeEnvironmentResourceSchema(ids);
17319
17564
  return {
17565
+ annotations: {
17566
+ destructiveHint: true,
17567
+ openWorldHint: false,
17568
+ readOnlyHint: false
17569
+ },
17320
17570
  description: "Update an environment owned by the caller's team and return it in the environment.get shape. Omitted fields remain unchanged.",
17321
17571
  input,
17322
17572
  kind: "write",
@@ -17339,6 +17589,11 @@ var makeCreateTagContract = (ids) => {
17339
17589
  });
17340
17590
  const output = makeTagResourceSchema();
17341
17591
  return {
17592
+ annotations: {
17593
+ destructiveHint: false,
17594
+ openWorldHint: false,
17595
+ readOnlyHint: false
17596
+ },
17342
17597
  description: "Create a tag on the caller's team. Tags select flows in run.create.",
17343
17598
  input,
17344
17599
  kind: "write",
@@ -17367,6 +17622,11 @@ var makeAddTagToFlowsContract = (ids) => {
17367
17622
  })
17368
17623
  });
17369
17624
  return {
17625
+ annotations: {
17626
+ destructiveHint: false,
17627
+ openWorldHint: false,
17628
+ readOnlyHint: false
17629
+ },
17370
17630
  description: "Assign an existing tag to the selected flows. Create tags with tag.create. Flows that already carry the tag are reported in skippedFlows.",
17371
17631
  input,
17372
17632
  kind: "write",
@@ -17399,6 +17659,11 @@ var makeListFlowsContract = (ids) => {
17399
17659
  }))
17400
17660
  });
17401
17661
  return {
17662
+ annotations: {
17663
+ destructiveHint: false,
17664
+ openWorldHint: false,
17665
+ readOnlyHint: true
17666
+ },
17402
17667
  description: "List the flows of an environment at its latest reconciled commit, or, when an AI task is given, the flows on that task's branch.",
17403
17668
  input,
17404
17669
  kind: "read",
@@ -17420,6 +17685,11 @@ var makeRemoveTagFromFlowsContract = (ids) => {
17420
17685
  })
17421
17686
  });
17422
17687
  return {
17688
+ annotations: {
17689
+ destructiveHint: true,
17690
+ openWorldHint: false,
17691
+ readOnlyHint: false
17692
+ },
17423
17693
  description: "Remove a tag from the selected flows. Succeeds whether or not each flow carried the tag; the flows that did not are reported in skippedFlows.",
17424
17694
  input,
17425
17695
  kind: "write",
@@ -17444,6 +17714,11 @@ var makeUpdateFlowContract = (ids) => {
17444
17714
  })
17445
17715
  });
17446
17716
  return {
17717
+ annotations: {
17718
+ destructiveHint: true,
17719
+ openWorldHint: false,
17720
+ readOnlyHint: false
17721
+ },
17447
17722
  description: "Move a flow between draft and active readiness. The other statuses shown in the app are derived and cannot be set.",
17448
17723
  input,
17449
17724
  kind: "write",
@@ -17500,6 +17775,11 @@ var makeFindIssuesContract = (ids) => {
17500
17775
  nextCursor: nextCursorSchema
17501
17776
  });
17502
17777
  return {
17778
+ annotations: {
17779
+ destructiveHint: false,
17780
+ openWorldHint: false,
17781
+ readOnlyHint: true
17782
+ },
17503
17783
  description: "List the team's bug reports, maintenance reports, or coverage requests, newest first.",
17504
17784
  input,
17505
17785
  kind: "read",
@@ -17515,6 +17795,11 @@ var makeGetIssueContract = (ids) => {
17515
17795
  issue: makeIssueResourceSchema(ids)
17516
17796
  });
17517
17797
  return {
17798
+ annotations: {
17799
+ destructiveHint: false,
17800
+ openWorldHint: false,
17801
+ readOnlyHint: true
17802
+ },
17518
17803
  description: "Get an issue by id.",
17519
17804
  input,
17520
17805
  kind: "read",
@@ -17533,6 +17818,11 @@ var makeAddFlowsToIssueContract = (ids) => {
17533
17818
  issue: makeIssueResourceSchema(ids)
17534
17819
  });
17535
17820
  return {
17821
+ annotations: {
17822
+ destructiveHint: false,
17823
+ openWorldHint: false,
17824
+ readOnlyHint: false
17825
+ },
17536
17826
  description: "Add flows to a coverage request owned by the caller's team. " + "Flows already covered stay covered. " + "Bug and maintenance reports link to flows through the runs that reproduce them; use run.diagnose to record one.",
17537
17827
  input,
17538
17828
  kind: "write",
@@ -17564,6 +17854,11 @@ var makeCreateIssueContract = (ids) => {
17564
17854
  issue: makeIssueResourceSchema(ids)
17565
17855
  });
17566
17856
  return {
17857
+ annotations: {
17858
+ destructiveHint: true,
17859
+ openWorldHint: true,
17860
+ readOnlyHint: false
17861
+ },
17567
17862
  description: "Create a bug or coverage request issue for the caller's team. " + "Maintenance issues cannot be created through the public API.",
17568
17863
  input,
17569
17864
  kind: "write",
@@ -17582,6 +17877,11 @@ var makeRemoveFlowsFromIssueContract = (ids) => {
17582
17877
  issue: makeIssueResourceSchema(ids)
17583
17878
  });
17584
17879
  return {
17880
+ annotations: {
17881
+ destructiveHint: true,
17882
+ openWorldHint: false,
17883
+ readOnlyHint: false
17884
+ },
17585
17885
  description: "Remove flows from a coverage request owned by the caller's team. " + "Flows the request does not cover are left alone. " + "Bug and maintenance reports link to flows through the runs that reproduce them, so their flows cannot be set directly.",
17586
17886
  input,
17587
17887
  kind: "write",
@@ -17607,6 +17907,11 @@ var makeUpdateIssueContract = (ids) => {
17607
17907
  issue: makeIssueResourceSchema(ids)
17608
17908
  });
17609
17909
  return {
17910
+ annotations: {
17911
+ destructiveHint: true,
17912
+ openWorldHint: true,
17913
+ readOnlyHint: false
17914
+ },
17610
17915
  description: "Update an issue owned by the caller's team. Omitted fields remain unchanged.",
17611
17916
  input,
17612
17917
  kind: "write",
@@ -17626,6 +17931,11 @@ var makeDiagnoseRunContract = (ids) => {
17626
17931
  issue: makeIssueResourceSchema(ids)
17627
17932
  });
17628
17933
  return {
17934
+ annotations: {
17935
+ destructiveHint: true,
17936
+ openWorldHint: true,
17937
+ readOnlyHint: false
17938
+ },
17629
17939
  description: "Diagnose failed flows in a run as reproductions of a bug or maintenance report owned by the caller's team. " + "The issue's type selects the diagnosis. " + "Each flow must have failed in the run. " + "A flow that is already diagnosed moves to this issue. " + "The diagnosis appears on the run, and the reproduction appears under the issue's reproductions. " + "Coverage requests cannot be diagnosed; use issue.addFlows to cover flows instead.",
17630
17940
  input,
17631
17941
  kind: "write",
@@ -17662,6 +17972,11 @@ var makeFindRunsContract = (ids) => {
17662
17972
  })).describe("The environment's runs, newest first. Per-flow results are available via run.get.")
17663
17973
  });
17664
17974
  return {
17975
+ annotations: {
17976
+ destructiveHint: false,
17977
+ openWorldHint: false,
17978
+ readOnlyHint: true
17979
+ },
17665
17980
  description: "List an environment's recent runs, newest first.",
17666
17981
  input,
17667
17982
  kind: "read",
@@ -17738,6 +18053,11 @@ var makeGetRunContract = (ids) => {
17738
18053
  runId: ids.run.describe("The run this response describes. Treat it as canonical: it can differ from the id you asked for. A deploy notification returns a run id before the run exists, and if a second notification for the same commit is folded into an earlier run, that id resolves to the earlier run instead.")
17739
18054
  }, { urlFieldDescription: "Absolute URL of the run page." });
17740
18055
  return {
18056
+ annotations: {
18057
+ destructiveHint: false,
18058
+ openWorldHint: false,
18059
+ readOnlyHint: true
18060
+ },
17741
18061
  description: "Get a run's status, per-flow results, and links.",
17742
18062
  input,
17743
18063
  kind: "read",
@@ -17757,6 +18077,11 @@ var makeReattemptRunContract = (ids) => {
17757
18077
  runId: ids.run
17758
18078
  }, { urlFieldDescription: "Absolute URL of the run page." });
17759
18079
  return {
18080
+ annotations: {
18081
+ destructiveHint: true,
18082
+ openWorldHint: true,
18083
+ readOnlyHint: false
18084
+ },
17760
18085
  description: "Request new attempts for a run's flows, in the same run. " + "A flow is eligible once its result is failed or canceled and QA Wolf's " + "automatic retries have finished. " + "A fully investigated run no longer accepts reattempts. " + "Attempts run with the latest flow code. Poll run.get for results.",
17761
18086
  input,
17762
18087
  kind: "write",
@@ -17773,7 +18098,12 @@ var makeStopRunContract = (ids) => {
17773
18098
  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.')
17774
18099
  }, { urlFieldDescription: "Absolute URL of the run page." });
17775
18100
  return {
17776
- 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.",
18101
+ annotations: {
18102
+ destructiveHint: true,
18103
+ openWorldHint: true,
18104
+ readOnlyHint: false
18105
+ },
18106
+ description: "Stop a run, including its queued flows and automatic retries. Stopping is asynchronous and can update run-status messages and commit statuses in connected integrations. 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.",
17777
18107
  input,
17778
18108
  kind: "write",
17779
18109
  name: "run.stop",
@@ -17806,6 +18136,11 @@ var makeEvaluateSnippetOnRunnerContract = (ids) => {
17806
18136
  ])
17807
18137
  ]);
17808
18138
  return {
18139
+ annotations: {
18140
+ destructiveHint: true,
18141
+ openWorldHint: true,
18142
+ readOnlyHint: false
18143
+ },
17809
18144
  description: "Evaluate a snippet against whatever the runner's browser is showing right now. Answers whether the snippet ran, and its error if it threw — not the value it evaluated to, so read anything you want back out of the runner's journal (a snippet's `console.log` lands in the `console` stream). A snippet starts no run and leaves no run-scoped journal entries of its own. `runner-cannot-evaluate-snippets` on a runner with nothing attached to serve one — which will never clear, so check that the runner you launched is one that runs a browser or app before retrying. `runner-unreachable` if the runner could not be reached: it may still be starting, it may have terminated after inactivity, or it may be too busy to answer. Neither is proof the snippet did not run: a snippet that outlives the answer window is still executing when you read this, so do not blindly resubmit one that mutates what the page is looking at.",
17810
18145
  input,
17811
18146
  kind: "write",
@@ -17826,6 +18161,11 @@ var makeGetRunnerContract = (ids) => {
17826
18161
  running: boolean2().describe("False when no runner is running under this id: it was never launched, it was terminated, or it terminated on its own after inactivity. These are not distinguished, because the run system keeps no record of a runner once it is gone.")
17827
18162
  });
17828
18163
  return {
18164
+ annotations: {
18165
+ destructiveHint: false,
18166
+ openWorldHint: false,
18167
+ readOnlyHint: true
18168
+ },
17829
18169
  description: "Report whether a runner is running under this id on the caller's team. 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. An id that is not running is a success with `running: false` rather than an error, so this is how to check a runner is still there before addressing it.",
17830
18170
  input,
17831
18171
  kind: "read",
@@ -17864,6 +18204,11 @@ var makeHighlightSelectorOnRunnerContract = (ids) => {
17864
18204
  })
17865
18205
  ]);
17866
18206
  return {
18207
+ annotations: {
18208
+ destructiveHint: false,
18209
+ openWorldHint: false,
18210
+ readOnlyHint: false
18211
+ },
17867
18212
  description: "Highlight the elements a selector matches on an interactive runner's live page, and answer how many it matched. The highlight stays until it is replaced or cleared, so it is visible in the next `runner.takeScreenshot` — which is the point, since a caller cannot see the runner's screen otherwise. Send an empty selector to clear, which answers `cleared`. `status` tells a selector that matched nothing (`empty`) from one the page could not parse (`invalid`), so a caller can tell a bad locator from a locator pointing at nothing. `no-answer` if the page did not answer in time: the highlight runs inside the page, so a page that is gone or mid-navigation does not answer slowly, it does not answer at all. `runner-cannot-highlight-selectors` on a runner with no browser to draw on. `runner-unreachable` if the runner could not be reached: it may still be starting, or it may have terminated after inactivity.",
17868
18213
  input,
17869
18214
  kind: "write",
@@ -17892,6 +18237,11 @@ var makeImportPackageOnRunnerContract = (ids) => {
17892
18237
  })
17893
18238
  ]);
17894
18239
  return {
18240
+ annotations: {
18241
+ destructiveHint: true,
18242
+ openWorldHint: true,
18243
+ readOnlyHint: false
18244
+ },
17895
18245
  description: "Install a package into an interactive runner's live run and import it, so a snippet or a selection can use it without a full run to reinstall dependencies. An `install-failed` failure carries npm's reason in `errorMessage`. `runner-unreachable` if the runner could not be reached: it may still be starting, it may have terminated after inactivity, or it may be too busy to answer. The install may still have landed, but installing the same version again does nothing, so retrying is safe.",
17896
18246
  input,
17897
18247
  kind: "write",
@@ -17912,6 +18262,11 @@ var makeLaunchRunnerContract = (ids) => {
17912
18262
  outcome: literal("success")
17913
18263
  });
17914
18264
  return {
18265
+ annotations: {
18266
+ destructiveHint: true,
18267
+ openWorldHint: false,
18268
+ readOnlyHint: false
18269
+ },
17915
18270
  description: "Launch an interactive runner on the caller's team under an id the caller chooses. Launching the same id again returns the runner already running rather than starting a second one, and the same id with a different runnerName is refused. A runner is not permanent: it terminates on its own after a period of inactivity, and launching the same id after that starts and bills a new runner — so read `alreadyRunning` to tell which happened.",
17916
18271
  input,
17917
18272
  kind: "write",
@@ -17930,6 +18285,11 @@ var makeTerminateRunnerContract = (ids) => {
17930
18285
  wasRunning: boolean2().describe("False when no running runner had this id: it was already terminated, was never launched, or terminated on its own after inactivity. These are not distinguished, because the run system keeps no record of a runner once it is gone. Not an error, and a retry needs no special handling.")
17931
18286
  });
17932
18287
  return {
18288
+ annotations: {
18289
+ destructiveHint: true,
18290
+ openWorldHint: false,
18291
+ readOnlyHint: false
18292
+ },
17933
18293
  description: "End an interactive runner on the caller's team, and the pod it runs on with it. Terminating a runner that is not running succeeds and reports `wasRunning: false`. This ends the runner: use `runner.stopRun` to stop what a runner is currently executing while leaving the runner up.",
17934
18294
  input,
17935
18295
  kind: "write",
@@ -17948,6 +18308,11 @@ var makeListRunnersContract = (ids) => {
17948
18308
  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.")
17949
18309
  });
17950
18310
  return {
18311
+ annotations: {
18312
+ destructiveHint: false,
18313
+ openWorldHint: false,
18314
+ readOnlyHint: true
18315
+ },
17951
18316
  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.",
17952
18317
  input,
17953
18318
  kind: "read",
@@ -17977,6 +18342,11 @@ var makePromoteSnapshotOnRunnerContract = (ids) => {
17977
18342
  })
17978
18343
  ]);
17979
18344
  return {
18345
+ annotations: {
18346
+ destructiveHint: true,
18347
+ openWorldHint: false,
18348
+ readOnlyHint: false
18349
+ },
17980
18350
  description: "Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it. The two paths are the ones the diff reported, which reach a caller as an `image-diff-artifact` entry on the runner's `run-events` journal stream. `snapshot-not-found` if the run wrote no screenshot at that path, which usually means the paths were not taken from a diff this run produced; nothing is changed, so correcting the path and repeating is safe. `runner-cannot-promote-snapshots` on a runner that stores no screenshots. `runner-unreachable` if the runner could not be reached: it may still be starting, or it may have terminated after inactivity. A promotion that lands is not undone by promoting again, so a retry after an unreachable answer is safe.",
17981
18351
  input,
17982
18352
  kind: "write",
@@ -17997,6 +18367,11 @@ var makeReadRunnerJournalContract = (ids) => {
17997
18367
  makeRunnerFailureSchema([runnerUnreachableFailureReason])
17998
18368
  ]);
17999
18369
  return {
18370
+ annotations: {
18371
+ destructiveHint: false,
18372
+ openWorldHint: false,
18373
+ readOnlyHint: false
18374
+ },
18000
18375
  description: `Read a window of one of an interactive runner's journal streams — the newest few, everything after a cursor, or everything belonging to one run. This is how a flow run's outcome and output are followed: \`run-status\` settles it, \`run-logs\` and \`run-events\` carry what it produced, and \`recorder\` carries the browser actions the runner recorded. A read counts as activity, so working through history does not get the runner reaped underneath you. ${retryableRunnerUnreachableDescription}`,
18001
18376
  input,
18002
18377
  kind: "read",
@@ -18069,6 +18444,11 @@ var makeRunFlowOnRunnerContract = (ids) => {
18069
18444
  failure
18070
18445
  ]);
18071
18446
  return {
18447
+ annotations: {
18448
+ destructiveHint: true,
18449
+ openWorldHint: true,
18450
+ readOnlyHint: false
18451
+ },
18072
18452
  description: "Run a flow on an interactive runner. When `selection` is given the lines run against the browser as it stands, so nothing is re-navigated and nothing is signed in again; without it the whole entry point runs from a fresh browser. A runner with no live browser starts one before a selection and says so with `bootstrappedRunner`, which means those lines ran against a fresh page rather than the one an earlier run left. A selection produces no `runStarted` event, so follow it by its run id on `run-status` like any other run. Send `unchangedFiles` to ship only what changed since an earlier run on this runner; a `needs-full-sync` failure names the paths it does not hold, and the way to recover is the same run again with every file in `files`. Answers as soon as the run is accepted, with the id to follow it by — nothing waits for the run to finish. Which browser or device the run needs is read from the flow file's own execution target, so it is not supplied here; when it does not match what the runner is, the call fails with `runner-target-mismatch` rather than failing partway through the run. `runner-unreachable` means the answer did not arrive, which is NOT the same as the run not having started: the runner may have accepted it and been too slow to say so, and resubmitting would start a second run that is billed and journalled alongside the first. Read the runner's `run-status` journal stream before resubmitting, and use the newest run id there if one appeared.",
18073
18453
  input,
18074
18454
  kind: "write",
@@ -18091,6 +18471,11 @@ var makeStopRunOnRunnerContract = (ids) => {
18091
18471
  makeRunnerFailureSchema([runnerUnreachableFailureReason])
18092
18472
  ]);
18093
18473
  return {
18474
+ annotations: {
18475
+ destructiveHint: true,
18476
+ openWorldHint: false,
18477
+ readOnlyHint: false
18478
+ },
18094
18479
  description: "Stop what a runner is currently executing, leaving the runner up and its browser on whatever page the run reached. Succeeds whether or not anything was running, and `wasRunning` says which. This is the counterpart of `runner.terminate`, which ends the runner itself. The run stops where it is, so the journal's `run-status` settles it as stopped rather than passed or failed. `runner-unreachable` if the runner could not be reached: it may still be starting, it may have terminated after inactivity, or it may be too busy to answer. The stop may still have landed, but stopping a runner that is already idle does nothing, so retrying is safe.",
18095
18480
  input,
18096
18481
  kind: "write",
@@ -18116,7 +18501,12 @@ var makeTakeScreenshotOnRunnerContract = (ids) => {
18116
18501
  ])
18117
18502
  ]);
18118
18503
  return {
18119
- description: `Take one screenshot of an interactive runner's screen. On a runner with a browser the image is the whole virtual desktop, browser window and all. On a mobile runner it is the device's own screen, re-encoded to JPEG on the pod so this contract reads one image format regardless of runner family. ${screenNeedsARunDescription} ${screenNotReadyDescription} ${runnerHasNoScreenDescription} ${retryableRunnerUnreachableDescription}`,
18504
+ annotations: {
18505
+ destructiveHint: false,
18506
+ openWorldHint: false,
18507
+ readOnlyHint: false
18508
+ },
18509
+ description: `Take one screenshot of an interactive runner's screen. On a runner with a browser the image is the whole virtual desktop, browser window and all; this refreshes the inactivity timer and can extend the runner's billed lifetime. On a mobile runner it is the device's own screen, re-encoded to JPEG on the pod so this contract reads one image format regardless of runner family. ${screenNeedsARunDescription} ${screenNotReadyDescription} ${runnerHasNoScreenDescription} ${retryableRunnerUnreachableDescription}`,
18120
18510
  input,
18121
18511
  kind: "read",
18122
18512
  name: "runner.takeScreenshot",
@@ -18139,6 +18529,11 @@ var makeListTagsContract = (ids) => {
18139
18529
  })).describe("The team's tags, alphabetical by name.")
18140
18530
  });
18141
18531
  return {
18532
+ annotations: {
18533
+ destructiveHint: false,
18534
+ openWorldHint: false,
18535
+ readOnlyHint: true
18536
+ },
18142
18537
  description: "List the team's tags, alphabetical by name. Tag names select flows in run.create.",
18143
18538
  input,
18144
18539
  kind: "read",
@@ -18147,6 +18542,250 @@ var makeListTagsContract = (ids) => {
18147
18542
  };
18148
18543
  };
18149
18544
 
18545
+ // node_modules/@qawolf/api-contracts/dist/v1/trigger/configurationRules.js
18546
+ var checkAction = (action, ctx) => {
18547
+ const namesFlows = action.flowIds.length + action.tagNames.length > 0;
18548
+ if (action.kind === "generativeSuite") {
18549
+ if (namesFlows) {
18550
+ ctx.addIssue({
18551
+ code: "custom",
18552
+ message: "flowIds and tagNames do not apply when the action is generativeSuite.",
18553
+ path: ["action", "flowIds"]
18554
+ });
18555
+ }
18556
+ return;
18557
+ }
18558
+ if (action.instructions !== undefined) {
18559
+ ctx.addIssue({
18560
+ code: "custom",
18561
+ message: "instructions apply to a generativeSuite action only.",
18562
+ path: ["action", "instructions"]
18563
+ });
18564
+ }
18565
+ if (!namesFlows) {
18566
+ ctx.addIssue({
18567
+ code: "custom",
18568
+ message: "At least one flow or tag is required.",
18569
+ path: ["action", "flowIds"]
18570
+ });
18571
+ }
18572
+ };
18573
+ var checkSchedule = (value, ctx) => {
18574
+ if (value.cadence === "daily") {
18575
+ for (const field of ["timeOfDay", "timezoneId"]) {
18576
+ if (value[field] === undefined) {
18577
+ ctx.addIssue({
18578
+ code: "custom",
18579
+ message: `${field} is required when cadence is daily.`,
18580
+ path: [field]
18581
+ });
18582
+ }
18583
+ }
18584
+ if (value.minuteOfHour !== undefined) {
18585
+ ctx.addIssue({
18586
+ code: "custom",
18587
+ message: "minuteOfHour applies to an hourly cadence only.",
18588
+ path: ["minuteOfHour"]
18589
+ });
18590
+ }
18591
+ } else {
18592
+ for (const field of ["timeOfDay", "timezoneId"]) {
18593
+ if (value[field] !== undefined) {
18594
+ ctx.addIssue({
18595
+ code: "custom",
18596
+ message: `${field} applies to a daily cadence only.`,
18597
+ path: [field]
18598
+ });
18599
+ }
18600
+ }
18601
+ }
18602
+ if (value.action.kind === "generativeSuite") {
18603
+ ctx.addIssue({
18604
+ code: "custom",
18605
+ message: "A schedule trigger cannot use a generativeSuite action.",
18606
+ path: ["action", "kind"]
18607
+ });
18608
+ return;
18609
+ }
18610
+ checkAction(value.action, ctx);
18611
+ };
18612
+ var checkDeployment = (value, ctx) => {
18613
+ if (value.environmentPattern !== undefined && value.environmentIds.length > 0) {
18614
+ ctx.addIssue({
18615
+ code: "custom",
18616
+ message: "Supply environmentIds or environmentPattern, not both.",
18617
+ path: ["environmentPattern"]
18618
+ });
18619
+ }
18620
+ checkAction(value.action, ctx);
18621
+ };
18622
+ var checkTriggerConfiguration = (value, ctx) => value.kind === "schedule" ? checkSchedule(value, ctx) : checkDeployment(value, ctx);
18623
+
18624
+ // node_modules/@qawolf/api-contracts/dist/v1/trigger/create.js
18625
+ var makeCreateTriggerContract = (ids) => {
18626
+ const commonFields = {
18627
+ name: string2().trim().min(1).max(255),
18628
+ workspaceId: ids.workspace.optional().describe("The workspace to create the trigger in. Required when authenticating with an organization or user API key.")
18629
+ };
18630
+ const input = discriminatedUnion("kind", [
18631
+ object({
18632
+ ...commonFields,
18633
+ ...makeTriggerScheduleFields({
18634
+ environmentId: ids.environmentRef,
18635
+ flow: ids.flow
18636
+ }),
18637
+ kind: literal("schedule")
18638
+ }),
18639
+ object({
18640
+ ...commonFields,
18641
+ ...makeTriggerDeploymentFields(ids),
18642
+ kind: literal("deployment")
18643
+ })
18644
+ ]).superRefine(checkTriggerConfiguration);
18645
+ const output = object({ trigger: makeTriggerResourceSchema(ids) });
18646
+ return {
18647
+ annotations: {
18648
+ destructiveHint: true,
18649
+ openWorldHint: true,
18650
+ readOnlyHint: false
18651
+ },
18652
+ description: "Create a trigger. A schedule trigger runs a named set of flows on a cadence; " + "a deployment trigger runs when a matching deployment is reported.",
18653
+ input,
18654
+ kind: "write",
18655
+ name: "trigger.create",
18656
+ output
18657
+ };
18658
+ };
18659
+
18660
+ // node_modules/@qawolf/api-contracts/dist/v1/trigger/find.js
18661
+ var makeFindTriggersContract = (ids) => {
18662
+ const input = object({
18663
+ isPaused: boolean2().optional().describe("Only list paused, or only unpaused, triggers. Omit for both."),
18664
+ workspaceId: ids.workspace.optional().describe("The workspace to list triggers from. Required when authenticating with an organization or user API key.")
18665
+ });
18666
+ const output = object({
18667
+ triggers: array(makeTriggerResourceSchema(ids)).describe("The team's triggers, newest first.")
18668
+ });
18669
+ return {
18670
+ annotations: {
18671
+ destructiveHint: false,
18672
+ openWorldHint: false,
18673
+ readOnlyHint: true
18674
+ },
18675
+ description: "List the team's triggers, newest first.",
18676
+ input,
18677
+ kind: "read",
18678
+ name: "trigger.find",
18679
+ output
18680
+ };
18681
+ };
18682
+
18683
+ // node_modules/@qawolf/api-contracts/dist/v1/trigger/get.js
18684
+ var makeGetTriggerContract = (ids) => {
18685
+ const input = object({ triggerId: ids.trigger });
18686
+ const output = object({ trigger: makeTriggerResourceSchema(ids) });
18687
+ return {
18688
+ annotations: {
18689
+ destructiveHint: false,
18690
+ openWorldHint: false,
18691
+ readOnlyHint: true
18692
+ },
18693
+ description: "Get one trigger by id.",
18694
+ input,
18695
+ kind: "read",
18696
+ name: "trigger.get",
18697
+ output
18698
+ };
18699
+ };
18700
+
18701
+ // node_modules/@qawolf/api-contracts/dist/v1/trigger/lifecycle.js
18702
+ var makePauseTriggerContract = (ids) => {
18703
+ const input = object({ triggerId: ids.trigger });
18704
+ const output = object({ trigger: makeTriggerResourceSchema(ids) });
18705
+ return {
18706
+ annotations: {
18707
+ destructiveHint: true,
18708
+ openWorldHint: false,
18709
+ readOnlyHint: false
18710
+ },
18711
+ description: "Pause a trigger so it stops firing. Pausing is idempotent. A team whose " + "triggers are all paused reports no trigger activity at all.",
18712
+ input,
18713
+ kind: "write",
18714
+ name: "trigger.pause",
18715
+ output
18716
+ };
18717
+ };
18718
+ var makeResumeTriggerContract = (ids) => {
18719
+ const input = object({ triggerId: ids.trigger });
18720
+ const output = object({ trigger: makeTriggerResourceSchema(ids) });
18721
+ return {
18722
+ annotations: {
18723
+ destructiveHint: true,
18724
+ openWorldHint: true,
18725
+ readOnlyHint: false
18726
+ },
18727
+ description: "Resume a paused trigger. A schedule trigger restarts from the next " + "upcoming slot: the slots it missed while paused do not run.",
18728
+ input,
18729
+ kind: "write",
18730
+ name: "trigger.resume",
18731
+ output
18732
+ };
18733
+ };
18734
+ var makeDeleteTriggerContract = (ids) => {
18735
+ const input = object({ triggerId: ids.trigger });
18736
+ const output = object({
18737
+ triggerId: ids.trigger.describe("The id of the deleted trigger.")
18738
+ });
18739
+ return {
18740
+ annotations: {
18741
+ destructiveHint: true,
18742
+ openWorldHint: false,
18743
+ readOnlyHint: false
18744
+ },
18745
+ description: "Delete a trigger permanently. The runs it already created are kept. " + "To stop a trigger without losing it, use trigger.pause.",
18746
+ input,
18747
+ kind: "write",
18748
+ name: "trigger.delete",
18749
+ output
18750
+ };
18751
+ };
18752
+
18753
+ // node_modules/@qawolf/api-contracts/dist/v1/trigger/update.js
18754
+ var makeUpdateTriggerContract = (ids) => {
18755
+ const commonFields = {
18756
+ name: string2().trim().min(1).max(255),
18757
+ triggerId: ids.trigger
18758
+ };
18759
+ const input = discriminatedUnion("kind", [
18760
+ object({
18761
+ ...commonFields,
18762
+ ...makeTriggerScheduleFields({
18763
+ environmentId: ids.environmentRef,
18764
+ flow: ids.flow
18765
+ }),
18766
+ kind: literal("schedule")
18767
+ }),
18768
+ object({
18769
+ ...commonFields,
18770
+ ...makeTriggerDeploymentFields(ids),
18771
+ kind: literal("deployment")
18772
+ })
18773
+ ]).superRefine(checkTriggerConfiguration);
18774
+ const output = object({ trigger: makeTriggerResourceSchema(ids) });
18775
+ return {
18776
+ annotations: {
18777
+ destructiveHint: true,
18778
+ openWorldHint: true,
18779
+ readOnlyHint: false
18780
+ },
18781
+ description: "Replace a trigger's configuration. Every field is written, so read the " + "trigger first and send its configuration back with your changes applied. " + "Pausing is separate: use trigger.pause and trigger.resume.",
18782
+ input,
18783
+ kind: "write",
18784
+ name: "trigger.update",
18785
+ output
18786
+ };
18787
+ };
18788
+
18150
18789
  // node_modules/@qawolf/api-contracts/dist/v1/index.js
18151
18790
  var makeContractsV1 = (ids) => {
18152
18791
  const resolvedIds = { ...defaultIdSchemas, ...ids };
@@ -18174,6 +18813,7 @@ var makeContractsV1 = (ids) => {
18174
18813
  setVariable: makeSetEnvironmentVariableContract(resolvedIds),
18175
18814
  update: makeUpdateEnvironmentContract(resolvedIds)
18176
18815
  },
18816
+ file: makeFileContracts(resolvedIds),
18177
18817
  flow: {
18178
18818
  addTag: makeAddTagToFlowsContract(resolvedIds),
18179
18819
  list: makeListFlowsContract(resolvedIds),
@@ -18216,6 +18856,15 @@ var makeContractsV1 = (ids) => {
18216
18856
  tag: {
18217
18857
  create: makeCreateTagContract(resolvedIds),
18218
18858
  list: makeListTagsContract(resolvedIds)
18859
+ },
18860
+ trigger: {
18861
+ create: makeCreateTriggerContract(resolvedIds),
18862
+ delete: makeDeleteTriggerContract(resolvedIds),
18863
+ find: makeFindTriggersContract(resolvedIds),
18864
+ get: makeGetTriggerContract(resolvedIds),
18865
+ pause: makePauseTriggerContract(resolvedIds),
18866
+ resume: makeResumeTriggerContract(resolvedIds),
18867
+ update: makeUpdateTriggerContract(resolvedIds)
18219
18868
  }
18220
18869
  };
18221
18870
  };
@@ -20564,7 +21213,7 @@ function startUpdateCheck(deps) {
20564
21213
  // package.json
20565
21214
  var package_default = {
20566
21215
  name: "@qawolf/cli",
20567
- version: "1.27.0",
21216
+ version: "1.29.0",
20568
21217
  description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
20569
21218
  keywords: [
20570
21219
  "automation",
@@ -20635,7 +21284,7 @@ var package_default = {
20635
21284
  "@clack/prompts": "1.5.1",
20636
21285
  "@napi-rs/keyring": "1.3.0",
20637
21286
  "@oxc-node/core": "0.1.0",
20638
- "@qawolf/api-contracts": "0.52.0",
21287
+ "@qawolf/api-contracts": "0.53.0",
20639
21288
  "@qawolf/emails": "1.1.1",
20640
21289
  "@qawolf/flow-targets": "1.0.0",
20641
21290
  "@qawolf/flows": "0.1.4",
@@ -33264,27 +33913,50 @@ async function handleRunnerInspect(ctx, options, deps) {
33264
33913
  }
33265
33914
 
33266
33915
  // src/core/interactiveRunner/inspectMobileRequest.ts
33916
+ var blankInspectMobileFlags = {
33917
+ context: undefined,
33918
+ partial: undefined,
33919
+ selector: undefined,
33920
+ strategy: undefined,
33921
+ text: undefined,
33922
+ x: undefined,
33923
+ y: undefined
33924
+ };
33267
33925
  function toNumber(value) {
33268
33926
  return value.trim() === "" ? Number.NaN : Number(value);
33269
33927
  }
33270
- function buildInspectMobileRequest(what, flags) {
33271
- if (flags.by === "point" && (flags.text !== undefined || flags.partial !== undefined)) {
33272
- return {
33273
- error: "--by point matches by pixel, so --text/--partial would be ignored rather than searching by text. Pass --by text instead, or drop --text/--partial.",
33274
- ok: false
33275
- };
33928
+ var groupsOf = (flags) => ({
33929
+ point: flags.x !== undefined || flags.y !== undefined,
33930
+ selector: flags.selector !== undefined || flags.strategy !== undefined,
33931
+ text: flags.text !== undefined || flags.partial !== undefined
33932
+ });
33933
+ function describeGroup(group) {
33934
+ switch (group) {
33935
+ case "point":
33936
+ return "--x/--y";
33937
+ case "selector":
33938
+ return "--selector/--strategy";
33939
+ case "text":
33940
+ return "--text/--partial";
33276
33941
  }
33277
- if (flags.by === "text" && (flags.x !== undefined || flags.y !== undefined)) {
33942
+ }
33943
+ function buildInspectMobileRequest(what, flags) {
33944
+ const groups = groupsOf(flags);
33945
+ const present = ["point", "text", "selector"].filter((group) => groups[group]);
33946
+ if (present.length > 1) {
33278
33947
  return {
33279
- error: "--by text matches by text, so --x/--y would be ignored rather than matching a point. Pass --by point instead, or drop --x/--y.",
33948
+ error: `${describeGroup(present[0])} and ${describeGroup(present[1])} were both passed, but elements are found by a point, by text, or by a selector — never more than one at once. Drop all but one of them.`,
33280
33949
  ok: false
33281
33950
  };
33282
33951
  }
33952
+ const by = present[0];
33283
33953
  const candidate = {
33284
33954
  what,
33285
- ...flags.by === undefined ? {} : { by: flags.by },
33955
+ ...by === undefined ? {} : { by },
33286
33956
  ...flags.context === undefined ? {} : { context: flags.context },
33287
33957
  ...flags.partial === undefined ? {} : { partial: flags.partial },
33958
+ ...flags.selector === undefined ? {} : { selector: flags.selector },
33959
+ ...flags.strategy === undefined ? {} : { strategy: flags.strategy },
33288
33960
  ...flags.text === undefined ? {} : { text: flags.text },
33289
33961
  ...flags.x === undefined ? {} : { x: toNumber(flags.x) },
33290
33962
  ...flags.y === undefined ? {} : { y: toNumber(flags.y) }
@@ -33296,7 +33968,7 @@ function buildInspectMobileRequest(what, flags) {
33296
33968
  return { ok: true, request: parsed.data };
33297
33969
  }
33298
33970
 
33299
- // src/domains/interactiveRunner/inspectMobile.ts
33971
+ // src/domains/interactiveRunner/inspectMobileAnswer.ts
33300
33972
  function describeSession(session) {
33301
33973
  switch (session.type) {
33302
33974
  case "ready":
@@ -33326,6 +33998,8 @@ function streamLine(value) {
33326
33998
  return JSON.stringify({ matches: value.matches });
33327
33999
  }
33328
34000
  }
34001
+
34002
+ // src/domains/interactiveRunner/inspectMobile.ts
33329
34003
  async function handleRunnerInspectMobile(ctx, options, deps) {
33330
34004
  const built = buildInspectMobileRequest(options.what, options.flags);
33331
34005
  if (!built.ok)
@@ -33353,6 +34027,11 @@ async function handleRunnerInspectMobile(ctx, options, deps) {
33353
34027
  }
33354
34028
  const { failureReason } = result.value;
33355
34029
  switch (failureReason) {
34030
+ case "invalid-selector":
34031
+ return {
34032
+ error: interactiveRunnerMessages.inspectMobileInvalidSelector,
34033
+ exitCode: exitCodes.invalidArgs
34034
+ };
33356
34035
  case "runner-is-not-mobile":
33357
34036
  return {
33358
34037
  error: interactiveRunnerMessages.runnerIsNotMobile,
@@ -33385,46 +34064,26 @@ async function handleRunnerInspectMobile(ctx, options, deps) {
33385
34064
  // src/commands/runner/inspectMobile.register.ts
33386
34065
  function registerRunnerInspectMobileCommands(inspect, signals) {
33387
34066
  declareCommandKind(inspect.command("session"), "read").description("Print the Appium session's status: ready, or why not").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
33388
- flags: {
33389
- by: undefined,
33390
- context: undefined,
33391
- partial: undefined,
33392
- text: undefined,
33393
- x: undefined,
33394
- y: undefined
33395
- },
34067
+ flags: blankInspectMobileFlags,
33396
34068
  runner: opts.runner,
33397
34069
  what: "session"
33398
34070
  }, runnerDeps(ctx)))(opts, command));
33399
34071
  declareCommandKind(inspect.command("contexts"), "read").description("List the WebView contexts available, and which is current").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
33400
- flags: {
33401
- by: undefined,
33402
- context: undefined,
33403
- partial: undefined,
33404
- text: undefined,
33405
- x: undefined,
33406
- y: undefined
33407
- },
34072
+ flags: blankInspectMobileFlags,
33408
34073
  runner: opts.runner,
33409
34074
  what: "contexts"
33410
34075
  }, runnerDeps(ctx)))(opts, command));
33411
34076
  declareCommandKind(inspect.command("page-source"), "read").description("Print the current context's page source, as a tree").option("--context <name>", "Read this context instead of the current one").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
33412
- flags: {
33413
- by: undefined,
33414
- context: opts.context,
33415
- partial: undefined,
33416
- text: undefined,
33417
- x: undefined,
33418
- y: undefined
33419
- },
34077
+ flags: { ...blankInspectMobileFlags, context: opts.context },
33420
34078
  runner: opts.runner,
33421
34079
  what: "page"
33422
34080
  }, runnerDeps(ctx)))(opts, command));
33423
- declareCommandKind(inspect.command("elements"), "read").description("Find elements at a screen point, or elements carrying some text").requiredOption("--by <by>", "point or text").option("--context <name>", "Read this context instead of the current one").option("--partial", "text: match text containing this, rather than exactly this").option("--runner <id>", runnerFlagDescription).option("--text <text>", "text: the text to match").option("--x <pixels>", "point: whole pixels on the device's own screen").option("--y <pixels>", "point: whole pixels on the device's own screen").action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
34081
+ declareCommandKind(inspect.command("elements"), "read").description("Find elements at a screen point, carrying some text, or matching a selector").option("--context <name>", "Read this context instead of the current one").option("--partial", "text: match text containing this, rather than exactly this").option("--runner <id>", runnerFlagDescription).option("--selector <selector>", "Resolved the same way a screen object's own selector is").option("--strategy <strategy>", "selector: xpath, ios-predicate, or shadow (defaults to xpath)").option("--text <text>", "text: the text to match").option("--x <pixels>", "point: whole pixels on the device's own screen").option("--y <pixels>", "point: whole pixels on the device's own screen").action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspectMobile(ctx, {
33424
34082
  flags: {
33425
- by: opts.by,
33426
34083
  context: opts.context,
33427
34084
  partial: opts.partial,
34085
+ selector: opts.selector,
34086
+ strategy: opts.strategy,
33428
34087
  text: opts.text,
33429
34088
  x: opts.x,
33430
34089
  y: opts.y
@@ -33443,8 +34102,9 @@ Examples:
33443
34102
  $ qawolf runner inspect session
33444
34103
  $ qawolf runner inspect contexts
33445
34104
  $ qawolf runner inspect page-source --context WEBVIEW_1
33446
- $ qawolf runner inspect elements --by point --x 200 --y 400
33447
- $ qawolf runner inspect elements --by text --text "Sign in" --partial`;
34105
+ $ qawolf runner inspect elements --x 200 --y 400
34106
+ $ qawolf runner inspect elements --text "Sign in" --partial
34107
+ $ qawolf runner inspect elements --selector "//android.widget.Button[@text='Sign in']"`;
33448
34108
  function registerRunnerInspectCommands(runner, signals) {
33449
34109
  const inspect = runner.command("inspect").description("Read one thing off a runner's live page (browser) or Appium session (mobile)").addHelpText("after", inspectExamples);
33450
34110
  declareCommandKind(inspect.command("element-html"), "read").description("Print the HTML of the first element a selector matches").requiredOption("--selector <selector>", "Playwright selector to inspect").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerInspect(ctx, {
@@ -34782,4 +35442,4 @@ createProgram({ signals }).parseAsync().catch(() => {
34782
35442
  process.exitCode = 1;
34783
35443
  }).finally(() => exitWhenIdle(typeof process.exitCode === "number" ? process.exitCode : 0));
34784
35444
 
34785
- //# debugId=90417CF807BBB8C264756E2164756E21
35445
+ //# debugId=424C032AEE2722C964756E2164756E21