@flowriki/agent 0.1.0-beta.27 → 0.1.0-beta.29

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.
@@ -6,7 +6,7 @@ import os from "os";
6
6
  import path from "path";
7
7
  import { createRequire } from "module";
8
8
  import { spawnSync } from "child_process";
9
- var AGENT_VERSION = "0.1.0-beta.27";
9
+ var AGENT_VERSION = "0.1.0-beta.29";
10
10
  async function prepareBrowsers() {
11
11
  if (browsersReady()) {
12
12
  return { ok: true, already: true, message: "chromium is already installed" };
@@ -133,9 +133,9 @@ function forgetConnection(service, env = process.env) {
133
133
  fs3.renameSync(temporary, file);
134
134
  return true;
135
135
  }
136
- var fingerprint = (secret) => crypto2.createHash("sha256").update(secret).digest("hex").slice(0, 12);
136
+ var fingerprint = (secret2) => crypto2.createHash("sha256").update(secret2).digest("hex").slice(0, 12);
137
137
 
138
- // ../contracts/dist/chunk-MQTBBCDK.js
138
+ // ../contracts/dist/chunk-UFMRFPRU.js
139
139
  import { z } from "zod";
140
140
  var PROTOCOL_VERSION = 1;
141
141
  var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
@@ -293,7 +293,10 @@ var AGENT_FEATURES = /* @__PURE__ */ new Set([
293
293
  "actors.v1",
294
294
  /** Replaying other flows' steps before a recording starts capturing, so a
295
295
  * flow can be recorded from where its prerequisites leave the browser. */
296
- "prerequisites.v1"
296
+ "prerequisites.v1",
297
+ /** Running a helper — an email code, an authenticator, a text, an API
298
+ * call — as a step, from the helpers a run is sent. */
299
+ "helpers.v1"
297
300
  ]);
298
301
  var AgentMessageSchema = z.discriminatedUnion("type", [
299
302
  AckSchema,
@@ -461,7 +464,7 @@ import path7 from "path";
461
464
  // ../engine-playwright/dist/index.js
462
465
  import path3 from "path";
463
466
 
464
- // ../test-model/dist/chunk-TSFRTYLG.js
467
+ // ../test-model/dist/chunk-GALEMMJW.js
465
468
  import { z as z3 } from "zod";
466
469
  var LocatorOverrideSchema = z3.object({
467
470
  strategy: z3.enum(["css", "xpath", "id", "testId", "text", "label", "placeholder", "altText", "title"]),
@@ -667,7 +670,7 @@ var DialogAnswerSchema = z3.object({
667
670
  }).strict();
668
671
  var RecordedStepSchema = RecordedTargetSchema.extend({
669
672
  id: z3.string().min(1),
670
- type: z3.enum(["fill", "select", "click", "dblclick", "rightclick", "drag", "check", "press", "hover", "upload", "scroll", "back", "forward", "navigate", "closePage", "unsupported", "setRange", "assert", "signIn", "signOut", "useActor", "resetActor", "capture"]),
673
+ type: z3.enum(["fill", "select", "click", "dblclick", "rightclick", "drag", "check", "press", "hover", "upload", "scroll", "back", "forward", "navigate", "closePage", "unsupported", "setRange", "assert", "signIn", "signOut", "useActor", "resetActor", "capture", "useHelper"]),
671
674
  scope: z3.string(),
672
675
  value: z3.string(),
673
676
  secret: z3.boolean(),
@@ -743,7 +746,21 @@ var RecordedStepSchema = RecordedTargetSchema.extend({
743
746
  * participants cost a login each time, which is the cost this removes. */
744
747
  actorId: z3.string().optional(),
745
748
  navigationBase: z3.enum(["page", "application"]).optional(),
746
- capture: z3.object({ name: z3.string().regex(/^[a-zA-Z][a-zA-Z0-9_]*$/), source: z3.enum(["text", "value", "url"]) }).strict().optional()
749
+ capture: z3.object({ name: z3.string().regex(/^[a-zA-Z][a-zA-Z0-9_]*$/), source: z3.enum(["text", "value", "url"]) }).strict().optional(),
750
+ /** WHICH HELPER, for `useHelper`: the email code, the authenticator, the
751
+ * text, the API call — set up on the Helpers screen and referred to here by
752
+ * id, never copied, so fixing a helper fixes every test using it.
753
+ *
754
+ * `inputs` fills the helper's `{{names}}` and may itself use a value read
755
+ * earlier in the run (`{{email}}`). What comes back is kept under `saveAs`
756
+ * for later steps, exactly as a captured value is; `then: "open"` also
757
+ * takes the browser to it, which is what a verification link is for. */
758
+ helper: z3.object({
759
+ helperId: z3.string(),
760
+ inputs: z3.record(z3.string(), z3.string()).default({}),
761
+ saveAs: z3.string().regex(/^([a-zA-Z][a-zA-Z0-9_]*)?$/),
762
+ then: z3.enum(["remember", "open"]).default("remember")
763
+ }).strict().optional()
747
764
  }).strict();
748
765
  var DraftStepSchema = RecordedStepSchema.extend({
749
766
  role: z3.string().default(""),
@@ -940,6 +957,7 @@ function liftSignInFirst(raw) {
940
957
  import { z as z4 } from "zod";
941
958
  import { z as z22 } from "zod";
942
959
  import { z as z32 } from "zod";
960
+ import { z as z42 } from "zod";
943
961
  var EngineSchema = z4.enum(["chromium", "firefox", "webkit"]);
944
962
  var CapabilitySchema = z4.object({
945
963
  id: z4.string(),
@@ -1590,6 +1608,115 @@ function expandForRun(flow, lookup) {
1590
1608
  const { signIn, steps } = preparationFor(flow, lookup);
1591
1609
  return { ...rest, steps: [...signIn ? [signIn] : [], ...steps, ...flow.steps] };
1592
1610
  }
1611
+ var HELPER_SCHEMA_VERSION = 1;
1612
+ var SecretRef = z42.string().regex(/^[a-z][a-z0-9_]*$/i);
1613
+ var PatternSchema = z42.string().max(500).refine((source) => {
1614
+ try {
1615
+ new RegExp(source);
1616
+ return true;
1617
+ } catch {
1618
+ return false;
1619
+ }
1620
+ }, "that is not a valid pattern");
1621
+ var MailboxHelperSchema = z42.object({
1622
+ kind: z42.literal("mailbox"),
1623
+ /** IMAP, which every provider offers: Gmail, Outlook, Zoho, Fastmail… */
1624
+ host: z42.string().min(1),
1625
+ port: z42.number().int().positive().default(993),
1626
+ secure: z42.boolean().default(true),
1627
+ username: z42.string().min(1),
1628
+ passwordRef: SecretRef,
1629
+ folder: z42.string().min(1).default("INBOX"),
1630
+ /** Which email. All optional; the newest that matches every one given. */
1631
+ match: z42.object({
1632
+ to: z42.string().default(""),
1633
+ from: z42.string().default(""),
1634
+ subject: z42.string().default("")
1635
+ }).strict().default({}),
1636
+ /** What to take out of it. */
1637
+ extract: z42.object({
1638
+ take: z42.enum(["code", "link", "subject", "body"]).default("code"),
1639
+ /** For a code: the pattern, first group if it has one. Default: 4–8 digits.
1640
+ * For a link: only links containing this text. */
1641
+ pattern: z42.string().max(500).default("")
1642
+ }).strict().default({}),
1643
+ /** How long to wait for the email to arrive. */
1644
+ waitSeconds: z42.number().int().min(0).max(300).default(60)
1645
+ }).strict();
1646
+ var TotpHelperSchema = z42.object({
1647
+ kind: z42.literal("totp"),
1648
+ /** The authenticator secret (base32, from the QR code's "manual entry"). */
1649
+ secretRef: SecretRef,
1650
+ digits: z42.union([z42.literal(6), z42.literal(8)]).default(6),
1651
+ period: z42.number().int().min(10).max(120).default(30),
1652
+ algorithm: z42.enum(["SHA1", "SHA256", "SHA512"]).default("SHA1")
1653
+ }).strict();
1654
+ var SmsHelperSchema = z42.object({
1655
+ kind: z42.literal("sms"),
1656
+ provider: z42.literal("twilio").default("twilio"),
1657
+ accountSid: z42.string().regex(/^AC[0-9a-f]{32}$/i, "a Twilio account SID starts with AC followed by 32 characters"),
1658
+ authTokenRef: SecretRef,
1659
+ /** The Twilio number the application texts. */
1660
+ to: z42.string().min(3),
1661
+ match: z42.object({
1662
+ from: z42.string().default(""),
1663
+ contains: z42.string().default("")
1664
+ }).strict().default({}),
1665
+ extract: z42.object({ pattern: z42.string().max(500).default("") }).strict().default({}),
1666
+ waitSeconds: z42.number().int().min(0).max(300).default(60)
1667
+ }).strict();
1668
+ var HttpHelperSchema = z42.object({
1669
+ kind: z42.literal("http"),
1670
+ method: z42.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("POST"),
1671
+ url: z42.string().min(1),
1672
+ /** Plain headers, in the repository. */
1673
+ headers: z42.record(z42.string(), z42.string()).default({}),
1674
+ /** Header name → vault reference, for tokens and keys. */
1675
+ secretHeaders: z42.record(z42.string(), SecretRef).default({}),
1676
+ body: z42.string().max(2e4).default(""),
1677
+ /** Refused unless the response has this status. 0 means any 2xx. */
1678
+ expectStatus: z42.number().int().min(0).max(599).default(0),
1679
+ extract: z42.object({
1680
+ take: z42.enum(["json", "pattern", "status", "nothing"]).default("nothing"),
1681
+ /** For json: a dot path — `data.order.id`, `items.0.code`. */
1682
+ path: z42.string().max(300).default(""),
1683
+ pattern: z42.string().max(500).default("")
1684
+ }).strict().default({})
1685
+ }).strict();
1686
+ var HelperConfigSchema = z42.discriminatedUnion("kind", [
1687
+ MailboxHelperSchema,
1688
+ TotpHelperSchema,
1689
+ SmsHelperSchema,
1690
+ HttpHelperSchema
1691
+ ]);
1692
+ var HelperSchema = z42.object({
1693
+ schemaVersion: z42.literal(HELPER_SCHEMA_VERSION),
1694
+ id: z42.string().min(1),
1695
+ slug: z42.string().regex(/^[a-z0-9][a-z0-9-]*$/),
1696
+ name: z42.string().min(1).max(120),
1697
+ description: z42.string().max(1e3).default(""),
1698
+ config: HelperConfigSchema,
1699
+ createdAt: z42.string(),
1700
+ createdBy: z42.string().default(""),
1701
+ updatedAt: z42.string(),
1702
+ revision: z42.number().int().nonnegative().default(0)
1703
+ }).strict().superRefine((helper, ctx) => {
1704
+ const patterns = [
1705
+ helper.config.kind === "mailbox" && helper.config.extract.take === "code" ? helper.config.extract.pattern : "",
1706
+ helper.config.kind === "sms" ? helper.config.extract.pattern : "",
1707
+ helper.config.kind === "http" && helper.config.extract.take === "pattern" ? helper.config.extract.pattern : ""
1708
+ ];
1709
+ for (const pattern of patterns) {
1710
+ if (pattern && !PatternSchema.safeParse(pattern).success) {
1711
+ ctx.addIssue({ code: "custom", path: ["config", "extract", "pattern"], message: "that is not a valid pattern" });
1712
+ }
1713
+ }
1714
+ });
1715
+ var DeliveredHelperSchema = z42.object({
1716
+ id: z42.string().min(1),
1717
+ name: z42.string(),
1718
+ config: HelperConfigSchema
1719
+ }).strict();
1593
1720
 
1594
1721
  // ../engine-playwright/dist/index.js
1595
1722
  import { createHmac, randomBytes } from "crypto";
@@ -3390,6 +3517,27 @@ function withRuntimeValues(step, values) {
3390
3517
  proposals: step.proposals.map((check) => ({ ...check, value: Array.isArray(check.value) ? check.value.map(expand) : expand(check.value) }))
3391
3518
  };
3392
3519
  }
3520
+ var NAME = /\{\{([a-zA-Z][a-zA-Z0-9_]*)\}\}/g;
3521
+ async function performHelperStep(page, step, options) {
3522
+ const helper = step.helper;
3523
+ if (!helper?.helperId) throw new Error("Choose which helper this step uses");
3524
+ if (!helper.saveAs) throw new Error("Name the value this step gets");
3525
+ if (!options.runHelper) {
3526
+ throw new Error("helpers run only on runs started from the dashboard \u2014 this machine was not sent any");
3527
+ }
3528
+ const inputs = Object.fromEntries(Object.entries(helper.inputs ?? {}).map(([name, text]) => [name, text.replace(NAME, (_, used) => {
3529
+ if (!options.values.has(used)) throw new Error(`Runtime value "${used}" has not been captured yet`);
3530
+ return options.values.get(used);
3531
+ })]));
3532
+ const got = await options.runHelper(helper.helperId, inputs);
3533
+ if (got.value.length > 16384) throw new Error("The helper's value is over 16 KB");
3534
+ options.values.set(helper.saveAs, got.value);
3535
+ if (helper.then === "open") {
3536
+ if (!/^https?:\/\//i.test(got.value)) throw new Error(`The helper gave "${got.value.slice(0, 80)}", which is not a link to open`);
3537
+ await page.goto(got.value, { waitUntil: "domcontentloaded", timeout: options.navigationTimeoutMs ?? 45e3 });
3538
+ }
3539
+ return got;
3540
+ }
3393
3541
  var secretsBehind = (auth) => [.../* @__PURE__ */ new Set([
3394
3542
  ...Object.values(auth.secretRefs ?? {}),
3395
3543
  ...auth.steps.flatMap((step) => step.secretRef ? [step.secretRef] : [])
@@ -3625,7 +3773,9 @@ var RunActors = class {
3625
3773
  pages: session.pages,
3626
3774
  baseUrl: this.opts.baseUrl,
3627
3775
  ...this.opts.resolveAuth ? { resolveAuth: this.opts.resolveAuth } : {},
3628
- ...this.opts.resolveSecret ? { resolveSecret: this.opts.resolveSecret } : {}
3776
+ ...this.opts.resolveSecret ? { resolveSecret: this.opts.resolveSecret } : {},
3777
+ ...this.opts.runHelper ? { runHelper: this.opts.runHelper } : {},
3778
+ values: this.values
3629
3779
  }
3630
3780
  );
3631
3781
  if (!this.preparations.has(id)) this.preparations.set(id, "fresh");
@@ -4486,7 +4636,7 @@ async function openRunSession(opts) {
4486
4636
  const consoleLines = [];
4487
4637
  if (artifacts.console) {
4488
4638
  const typed = steps.filter((step) => step.secret).map((step) => resolveSecret?.(step.secretRef || step.value)).filter((value) => Boolean(value && value.length >= 3));
4489
- const redact2 = (line) => typed.reduce((text, secret) => text.split(secret).join("[secret]"), line);
4639
+ const redact2 = (line) => typed.reduce((text, secret2) => text.split(secret2).join("[secret]"), line);
4490
4640
  const watch = (p) => p.on("console", (m) => {
4491
4641
  if (consoleLines.length < 2e3) consoleLines.push(redact2(`[${m.type()}] ${m.text()}`));
4492
4642
  });
@@ -4636,6 +4786,7 @@ async function replayInto(target, steps, opts) {
4636
4786
  startUrl: null,
4637
4787
  ...opts.resolveSecret ? { resolveSecret: opts.resolveSecret } : {},
4638
4788
  ...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {},
4789
+ ...opts.runHelper ? { runHelper: opts.runHelper } : {},
4639
4790
  ...opts.baseUrl ? { baseUrl: opts.baseUrl } : {}
4640
4791
  });
4641
4792
  return outcome.status === "passed" ? { ok: true } : { ok: false, error: outcome.error ?? "a step before recording did not complete" };
@@ -4663,6 +4814,7 @@ function stepFailure(step, detail) {
4663
4814
  async function runStepsIn(session, steps, opts) {
4664
4815
  let { page, pages, config, artifacts } = session;
4665
4816
  const { resolveSecret, resolveFile, onStepResult, evidenceDir } = opts;
4817
+ const values = opts.actors?.values ?? opts.values ?? /* @__PURE__ */ new Map();
4666
4818
  const results = [];
4667
4819
  let status2 = "passed";
4668
4820
  let error = null;
@@ -4674,7 +4826,7 @@ async function runStepsIn(session, steps, opts) {
4674
4826
  let step = recorded;
4675
4827
  if (!recorded.disabled) {
4676
4828
  try {
4677
- step = withRuntimeValues(recorded, opts.actors?.values ?? /* @__PURE__ */ new Map());
4829
+ step = withRuntimeValues(recorded, values);
4678
4830
  } catch (err) {
4679
4831
  const result2 = stepFailure(recorded, firstLine2(err));
4680
4832
  results.push(result2);
@@ -4859,6 +5011,14 @@ async function runStepsIn(session, steps, opts) {
4859
5011
  valuesResolved: true,
4860
5012
  baseUrl: opts.baseUrl,
4861
5013
  ...opts.actors ? { actors: opts.actors } : {},
5014
+ ...opts.runHelper ? { runHelper: opts.runHelper } : {},
5015
+ values,
5016
+ // What the helper got is what this row reports: the code typed next
5017
+ // is the first thing anybody checks when the application refused it.
5018
+ onHelper: (got) => {
5019
+ result.value = got.value;
5020
+ result.detail = got.detail;
5021
+ },
4862
5022
  timeoutMs: config.timeoutMs,
4863
5023
  navigationTimeoutMs: config.navigationTimeoutMs,
4864
5024
  // SAID OUT LOUD, on a step that passed. The element was found, but
@@ -4968,6 +5128,7 @@ async function replayFlow(opts) {
4968
5128
  ...opts.evidenceDir === void 0 ? {} : { evidenceDir: opts.evidenceDir },
4969
5129
  ...opts.signal === void 0 ? {} : { signal: opts.signal },
4970
5130
  ...opts.resolveAuth === void 0 ? {} : { resolveAuth: opts.resolveAuth },
5131
+ ...opts.runHelper === void 0 ? {} : { runHelper: opts.runHelper },
4971
5132
  baseUrl,
4972
5133
  startUrl: null
4973
5134
  });
@@ -4991,7 +5152,8 @@ async function replayFlow(opts) {
4991
5152
  }
4992
5153
  async function executeStep(main, input, options = {}) {
4993
5154
  const parsed = RecordedStepSchema.parse(input);
4994
- const step = options.valuesResolved ? parsed : withRuntimeValues(parsed, options.actors?.values ?? /* @__PURE__ */ new Map());
5155
+ const values = options.actors?.values ?? options.values ?? /* @__PURE__ */ new Map();
5156
+ const step = options.valuesResolved ? parsed : withRuntimeValues(parsed, values);
4995
5157
  if (options.actors) {
4996
5158
  if (step.type === "resetActor") {
4997
5159
  await options.actors.freshGuest(step.actorId ?? options.actors.currentId);
@@ -5020,6 +5182,15 @@ async function executeStep(main, input, options = {}) {
5020
5182
  if (!outcome.ok) throw new Error(outcome.detail ?? "Authentication failed");
5021
5183
  return main;
5022
5184
  }
5185
+ if (step.type === "useHelper") {
5186
+ const got = await performHelperStep(page, step, {
5187
+ values,
5188
+ ...options.runHelper ? { runHelper: options.runHelper } : {},
5189
+ ...options.navigationTimeoutMs ? { navigationTimeoutMs: options.navigationTimeoutMs } : {}
5190
+ });
5191
+ options.onHelper?.(got);
5192
+ return page;
5193
+ }
5023
5194
  if (step.type === "capture") {
5024
5195
  if (!step.capture || !options.actors) throw new Error("Configure the runtime output name and source");
5025
5196
  if (step.secret) throw new Error("Secret fields cannot be captured as shared outputs");
@@ -5243,6 +5414,7 @@ async function replayCase(opts) {
5243
5414
  actors,
5244
5415
  baseUrl: occurrence.baseUrl ?? baseUrl,
5245
5416
  ...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {},
5417
+ ...opts.runHelper ? { runHelper: opts.runHelper } : {},
5246
5418
  stepOffset: outcomes.reduce((count, previous) => count + previous.stepsTotal, 0),
5247
5419
  ...occurrence.ownSignIn ? { ownSignIn: occurrence.ownSignIn } : {},
5248
5420
  ...opts.resolveSecret === void 0 ? {} : { resolveSecret: opts.resolveSecret },
@@ -5700,6 +5872,7 @@ async function startRecordingSession(opts) {
5700
5872
  const replayed = await replayInto({ browser, context, page, config }, opts.preparation.steps, {
5701
5873
  resolveSecret: opts.preparation.resolveSecret,
5702
5874
  ...opts.preparation.resolveAuth ? { resolveAuth: opts.preparation.resolveAuth } : {},
5875
+ ...opts.preparation.runHelper ? { runHelper: opts.preparation.runHelper } : {},
5703
5876
  baseUrl: url
5704
5877
  });
5705
5878
  if (!replayed.ok) throw new Error(replayed.error);
@@ -5880,7 +6053,7 @@ async function startRecordingSession(opts) {
5880
6053
  function firstLine2(err) {
5881
6054
  return String(err?.message ?? err).split("\n")[0].trim();
5882
6055
  }
5883
- var SESSION_STEPS = /* @__PURE__ */ new Set(["signIn", "signOut", "useActor", "resetActor", "capture"]);
6056
+ var SESSION_STEPS = /* @__PURE__ */ new Set(["signIn", "signOut", "useActor", "resetActor", "capture", "useHelper"]);
5884
6057
  var usesAnOutput = (text) => Array.isArray(text) ? text.some(usesAnOutput) : String(text ?? "").includes("{{");
5885
6058
  function needsWholeRun(opts) {
5886
6059
  if (opts.actors?.length) return true;
@@ -6233,7 +6406,7 @@ async function startDebugSession(opts) {
6233
6406
  step = withRuntimeValues(step, actors?.values ?? /* @__PURE__ */ new Map());
6234
6407
  const fresh = step.secret && recordedInSession.has(step.id);
6235
6408
  const on = actors?.current ?? { page, pages };
6236
- const active = await executeStep(on.page, fresh ? { ...step, secret: false } : step, { pages: on.pages, resolveSecret, resolveFile, timeoutMs, baseUrl, valuesResolved: true, ...actors ? { actors } : {}, ...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {} });
6409
+ const active = await executeStep(on.page, fresh ? { ...step, secret: false } : step, { pages: on.pages, resolveSecret, resolveFile, timeoutMs, baseUrl, valuesResolved: true, ...actors ? { actors } : {}, ...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {}, ...opts.runHelper ? { runHelper: opts.runHelper } : {} });
6237
6410
  for (const a of step.proposals ?? []) {
6238
6411
  if (a.enabled === false) continue;
6239
6412
  const label = describeAssertion(a, step.name);
@@ -7307,7 +7480,7 @@ function toSummary(row) {
7307
7480
  };
7308
7481
  }
7309
7482
 
7310
- // ../repository-store/dist/chunk-A4JZPKZT.js
7483
+ // ../repository-store/dist/chunk-HTFXAD4R.js
7311
7484
  var FLOWRIKI_DIR = ".flowriki";
7312
7485
  var FEATURES_DIR = `${FLOWRIKI_DIR}/features`;
7313
7486
  var CONFIG_PATH = `${FLOWRIKI_DIR}/config.json`;
@@ -7318,6 +7491,7 @@ var CASES_DIR = `${FLOWRIKI_DIR}/cases`;
7318
7491
  var SUITES_DIR = `${FLOWRIKI_DIR}/suites`;
7319
7492
  var AUTH_DIR = `${FLOWRIKI_DIR}/auth`;
7320
7493
  var ACCOUNTS_DIR = `${AUTH_DIR}/accounts`;
7494
+ var HELPERS_DIR = `${FLOWRIKI_DIR}/helpers`;
7321
7495
  function assertSlug(slug) {
7322
7496
  if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) throw new Error(`invalid slug: ${slug}`);
7323
7497
  return slug;
@@ -8026,6 +8200,275 @@ function ensureRootCommit(root) {
8026
8200
  return git2(root, ["rev-list", "--max-parents=0", "HEAD"]).split("\n").pop();
8027
8201
  }
8028
8202
 
8203
+ // ../helpers/dist/index.js
8204
+ import { createHmac as createHmac2 } from "crypto";
8205
+ var HelperFailure = class extends Error {
8206
+ constructor(message) {
8207
+ super(message);
8208
+ this.name = "HelperFailure";
8209
+ }
8210
+ };
8211
+ var NAME2 = /\{\{([a-zA-Z][a-zA-Z0-9_]*)\}\}/g;
8212
+ function fill(text, inputs = {}) {
8213
+ return text.replace(NAME2, (_, name) => {
8214
+ if (inputs[name] === void 0) throw new HelperFailure(`needs a value for {{${name}}}`);
8215
+ return inputs[name];
8216
+ });
8217
+ }
8218
+ var secret = (ctx, ref, what) => {
8219
+ const value = ctx.resolveSecret(ref);
8220
+ if (!value) throw new HelperFailure(`the ${what} has not been set \u2014 save it on this helper first`);
8221
+ return value;
8222
+ };
8223
+ var DEFAULT_CODE = "\\b\\d{4,8}\\b";
8224
+ function takeCode(text, pattern) {
8225
+ const re = new RegExp(pattern || DEFAULT_CODE);
8226
+ const m = re.exec(text);
8227
+ if (!m) return null;
8228
+ return (m[1] ?? m[0]).trim();
8229
+ }
8230
+ function linksIn(html, text) {
8231
+ const out = [];
8232
+ for (const m of html.matchAll(/href\s*=\s*["']([^"']+)["']/gi)) out.push(m[1].replace(/&amp;/g, "&"));
8233
+ for (const m of text.matchAll(/https?:\/\/[^\s<>"')\]]+/g)) out.push(m[0]);
8234
+ return [...new Set(out)].filter((url) => /^https?:\/\//i.test(url));
8235
+ }
8236
+ var textOfHtml = (html) => html.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/\s+/g, " ").trim();
8237
+ var ago = (from, at) => {
8238
+ const s2 = Math.max(0, Math.round((from - at.getTime()) / 1e3));
8239
+ return s2 < 60 ? `${s2}s ago` : s2 < 3600 ? `${Math.round(s2 / 60)} min ago` : at.toISOString();
8240
+ };
8241
+ function base32(input) {
8242
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
8243
+ const clean = input.toUpperCase().replace(/[\s=-]/g, "");
8244
+ let bits = 0, value = 0;
8245
+ const out = [];
8246
+ for (const ch of clean) {
8247
+ const at = alphabet.indexOf(ch);
8248
+ if (at < 0) throw new HelperFailure(`the authenticator secret is not base32 \u2014 copy it from the "can't scan the code?" option`);
8249
+ value = value << 5 | at;
8250
+ bits += 5;
8251
+ if (bits >= 8) {
8252
+ out.push(value >>> bits - 8 & 255);
8253
+ bits -= 8;
8254
+ }
8255
+ }
8256
+ return Buffer.from(out);
8257
+ }
8258
+ function totp(secretBase32, at, options = {}) {
8259
+ const { digits = 6, period = 30, algorithm = "SHA1" } = options;
8260
+ const counter = Math.floor(at / 1e3 / period);
8261
+ const buffer = Buffer.alloc(8);
8262
+ buffer.writeBigUInt64BE(BigInt(counter));
8263
+ const hmac = createHmac2(algorithm.toLowerCase(), base32(secretBase32)).update(buffer).digest();
8264
+ const offset = hmac[hmac.length - 1] & 15;
8265
+ const binary = (hmac[offset] & 127) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3];
8266
+ const code = String(binary % 10 ** digits).padStart(digits, "0");
8267
+ const remaining = period - Math.floor(at / 1e3 % period);
8268
+ return { code, remaining };
8269
+ }
8270
+ async function imapMailbox(config, password) {
8271
+ const { ImapFlow } = await import("imapflow");
8272
+ const { simpleParser } = await import("mailparser");
8273
+ const client = new ImapFlow({
8274
+ host: config.host,
8275
+ port: config.port,
8276
+ secure: config.secure,
8277
+ auth: { user: config.username, pass: password },
8278
+ logger: false
8279
+ });
8280
+ try {
8281
+ await client.connect();
8282
+ } catch (error) {
8283
+ const text = String(error.responseText ?? error.message ?? error);
8284
+ throw new HelperFailure(/auth|credential|login|password/i.test(text) ? `the mailbox refused the username or password \u2014 for Gmail and Outlook this has to be an app password (${text.slice(0, 120)})` : `could not reach the mailbox at ${config.host}:${config.port} \u2014 ${text.slice(0, 160)}`);
8285
+ }
8286
+ return {
8287
+ async latest(query, limit) {
8288
+ const lock = await client.getMailboxLock(config.folder);
8289
+ try {
8290
+ const criteria = {};
8291
+ if (query.to) criteria["to"] = query.to;
8292
+ if (query.from) criteria["from"] = query.from;
8293
+ if (query.subject) criteria["subject"] = query.subject;
8294
+ if (query.since) criteria["since"] = new Date(query.since.getTime() - 864e5);
8295
+ const found = await client.search(Object.keys(criteria).length ? criteria : { all: true }, { uid: true });
8296
+ const uids = (Array.isArray(found) ? found : []).slice(-limit).reverse();
8297
+ const out = [];
8298
+ for (const uid of uids) {
8299
+ const message = await client.fetchOne(String(uid), { source: true, internalDate: true }, { uid: true });
8300
+ if (!message || !message.source) continue;
8301
+ const parsed = await simpleParser(message.source);
8302
+ const addresses = (v) => {
8303
+ const list = Array.isArray(v) ? v : v ? [v] : [];
8304
+ return list.map((a) => a.text ?? "").join(", ");
8305
+ };
8306
+ out.push({
8307
+ received: message.internalDate ? new Date(message.internalDate) : parsed.date ?? /* @__PURE__ */ new Date(0),
8308
+ from: addresses(parsed.from),
8309
+ to: addresses(parsed.to),
8310
+ subject: parsed.subject ?? "",
8311
+ text: parsed.text ?? "",
8312
+ html: typeof parsed.html === "string" ? parsed.html : ""
8313
+ });
8314
+ }
8315
+ return out;
8316
+ } finally {
8317
+ lock.release();
8318
+ }
8319
+ },
8320
+ async close() {
8321
+ await client.logout().catch(() => void 0);
8322
+ }
8323
+ };
8324
+ }
8325
+ async function runMailbox(config, ctx) {
8326
+ const now = ctx.now ?? Date.now;
8327
+ const query = {
8328
+ to: fill(config.match.to, ctx.inputs),
8329
+ from: fill(config.match.from, ctx.inputs),
8330
+ subject: fill(config.match.subject, ctx.inputs),
8331
+ since: ctx.since ?? null
8332
+ };
8333
+ const mailbox = await (ctx.openMailbox ?? imapMailbox)(config, secret(ctx, config.passwordRef, "mailbox password"));
8334
+ const deadline = now() + config.waitSeconds * 1e3;
8335
+ try {
8336
+ for (; ; ) {
8337
+ const messages = (await mailbox.latest(query, 20)).filter((m) => !query.since || m.received.getTime() >= query.since.getTime()).sort((a, b2) => b2.received.getTime() - a.received.getTime());
8338
+ const message = messages[0];
8339
+ if (message) {
8340
+ const evidence = { from: message.from, subject: message.subject, received: ago(now(), message.received) };
8341
+ const body = message.text || textOfHtml(message.html);
8342
+ switch (config.extract.take) {
8343
+ case "subject":
8344
+ return { value: message.subject, detail: `the subject of "${message.subject}"`, evidence };
8345
+ case "body":
8346
+ return { value: body.slice(0, 4e3), detail: `the text of "${message.subject}"`, evidence };
8347
+ case "link": {
8348
+ const links = linksIn(message.html, message.text).filter((url) => !config.extract.pattern || url.includes(config.extract.pattern));
8349
+ if (!links[0]) throw new HelperFailure(`"${message.subject}" has no link${config.extract.pattern ? ` containing "${config.extract.pattern}"` : ""}`);
8350
+ return { value: links[0], detail: `the link in "${message.subject}"`, evidence };
8351
+ }
8352
+ case "code": {
8353
+ const code = takeCode(`${message.subject}
8354
+ ${body}`, config.extract.pattern);
8355
+ if (!code) throw new HelperFailure(`"${message.subject}" has nothing matching ${config.extract.pattern || "a 4\u20138 digit code"}`);
8356
+ return { value: code, detail: `the code in "${message.subject}"`, evidence };
8357
+ }
8358
+ }
8359
+ }
8360
+ if (now() >= deadline || ctx.signal?.aborted) {
8361
+ const looked = [query.to && `to ${query.to}`, query.from && `from ${query.from}`, query.subject && `about "${query.subject}"`].filter(Boolean).join(", ");
8362
+ throw new HelperFailure(`no email${looked ? ` ${looked}` : ""}${query.since ? " since this started" : ""} arrived within ${config.waitSeconds}s`);
8363
+ }
8364
+ await new Promise((r) => setTimeout(r, ctx.pollMs ?? 3e3));
8365
+ }
8366
+ } finally {
8367
+ await mailbox.close();
8368
+ }
8369
+ }
8370
+ async function runSms(config, ctx) {
8371
+ const now = ctx.now ?? Date.now;
8372
+ const http = ctx.fetch ?? fetch;
8373
+ const to = fill(config.to, ctx.inputs);
8374
+ const from = fill(config.match.from, ctx.inputs);
8375
+ const contains = fill(config.match.contains, ctx.inputs);
8376
+ const token = secret(ctx, config.authTokenRef, "Twilio auth token");
8377
+ const auth = Buffer.from(`${config.accountSid}:${token}`).toString("base64");
8378
+ const deadline = now() + config.waitSeconds * 1e3;
8379
+ for (; ; ) {
8380
+ const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(config.accountSid)}/Messages.json?To=${encodeURIComponent(to)}&PageSize=20`;
8381
+ const response = await http(url, { headers: { authorization: `Basic ${auth}` }, ...ctx.signal ? { signal: ctx.signal } : {} });
8382
+ if (response.status === 401) throw new HelperFailure("Twilio refused the account SID or auth token");
8383
+ if (!response.ok) throw new HelperFailure(`Twilio answered ${response.status}`);
8384
+ const body = await response.json();
8385
+ const messages = (body.messages ?? []).map((m) => ({ ...m, at: new Date(m.date_sent ?? m.date_created) })).filter((m) => (!from || m.from.includes(from)) && (!contains || m.body.includes(contains))).filter((m) => !ctx.since || m.at.getTime() >= ctx.since.getTime()).sort((a, b2) => b2.at.getTime() - a.at.getTime());
8386
+ const message = messages[0];
8387
+ if (message) {
8388
+ const code = takeCode(message.body, config.extract.pattern);
8389
+ const evidence = { from: message.from, text: message.body.slice(0, 160), received: ago(now(), message.at) };
8390
+ if (!code) throw new HelperFailure(`the latest text has nothing matching ${config.extract.pattern || "a 4\u20138 digit code"}`);
8391
+ return { value: code, detail: `the code in the text from ${message.from}`, evidence };
8392
+ }
8393
+ if (now() >= deadline || ctx.signal?.aborted) {
8394
+ throw new HelperFailure(`no text to ${to}${ctx.since ? " since this started" : ""} arrived within ${config.waitSeconds}s`);
8395
+ }
8396
+ await new Promise((r) => setTimeout(r, ctx.pollMs ?? 3e3));
8397
+ }
8398
+ }
8399
+ function atPath(value, path11) {
8400
+ let at = value;
8401
+ for (const key of path11.split(".").filter(Boolean)) {
8402
+ if (at === null || at === void 0) return void 0;
8403
+ at = Array.isArray(at) && /^\d+$/.test(key) ? at[Number(key)] : at[key];
8404
+ }
8405
+ return at;
8406
+ }
8407
+ async function runHttp(config, ctx) {
8408
+ const http = ctx.fetch ?? fetch;
8409
+ const url = fill(config.url, ctx.inputs);
8410
+ try {
8411
+ new URL(url);
8412
+ } catch {
8413
+ throw new HelperFailure(`"${url}" is not an address`);
8414
+ }
8415
+ const headers = {};
8416
+ for (const [name, value] of Object.entries(config.headers)) headers[name] = fill(value, ctx.inputs);
8417
+ for (const [name, ref] of Object.entries(config.secretHeaders)) headers[name] = secret(ctx, ref, `"${name}" header`);
8418
+ const body = config.method === "GET" || !config.body ? void 0 : fill(config.body, ctx.inputs);
8419
+ if (body && !Object.keys(headers).some((h) => h.toLowerCase() === "content-type")) {
8420
+ headers["content-type"] = /^\s*[[{]/.test(body) ? "application/json" : "text/plain";
8421
+ }
8422
+ const timeout = AbortSignal.timeout(3e4);
8423
+ let response;
8424
+ try {
8425
+ response = await http(url, { method: config.method, headers, ...body !== void 0 ? { body } : {}, signal: ctx.signal ? AbortSignal.any([ctx.signal, timeout]) : timeout });
8426
+ } catch (error) {
8427
+ throw new HelperFailure(`could not reach ${new URL(url).host} \u2014 ${error.message}`);
8428
+ }
8429
+ const text = await response.text();
8430
+ const ok = config.expectStatus ? response.status === config.expectStatus : response.ok;
8431
+ if (!ok) throw new HelperFailure(`${config.method} ${new URL(url).pathname} answered ${response.status}${config.expectStatus ? `, not ${config.expectStatus}` : ""}: ${text.slice(0, 200)}`);
8432
+ const evidence = { status: String(response.status), response: text.slice(0, 300) };
8433
+ switch (config.extract.take) {
8434
+ case "nothing":
8435
+ return { value: "", detail: `${config.method} ${new URL(url).pathname} answered ${response.status}`, evidence };
8436
+ case "status":
8437
+ return { value: String(response.status), detail: "the response status", evidence };
8438
+ case "pattern": {
8439
+ const found = takeCode(text, config.extract.pattern);
8440
+ if (found === null) throw new HelperFailure(`the response has nothing matching ${config.extract.pattern}`);
8441
+ return { value: found, detail: "matched in the response", evidence };
8442
+ }
8443
+ case "json": {
8444
+ let parsed;
8445
+ try {
8446
+ parsed = JSON.parse(text);
8447
+ } catch {
8448
+ throw new HelperFailure("the response is not JSON");
8449
+ }
8450
+ const value = atPath(parsed, config.extract.path);
8451
+ if (value === void 0 || value === null) throw new HelperFailure(`the response has no "${config.extract.path}"`);
8452
+ return { value: typeof value === "string" ? value : JSON.stringify(value), detail: `"${config.extract.path}" from the response`, evidence };
8453
+ }
8454
+ }
8455
+ }
8456
+ async function runHelper(helper, ctx) {
8457
+ const config = "config" in helper ? helper.config : helper;
8458
+ switch (config.kind) {
8459
+ case "totp": {
8460
+ const { code, remaining } = totp(secret(ctx, config.secretRef, "authenticator secret"), (ctx.now ?? Date.now)(), config);
8461
+ return { value: code, detail: `valid for ${remaining} more seconds`, evidence: {} };
8462
+ }
8463
+ case "mailbox":
8464
+ return runMailbox(config, ctx);
8465
+ case "sms":
8466
+ return runSms(config, ctx);
8467
+ case "http":
8468
+ return runHttp(config, ctx);
8469
+ }
8470
+ }
8471
+
8029
8472
  // ../execution/dist/index.js
8030
8473
  import fs24 from "fs";
8031
8474
  import crypto3 from "crypto";
@@ -8101,6 +8544,7 @@ async function executeFlow(opts) {
8101
8544
  ...flow.actors ? { actors: flow.actors } : {},
8102
8545
  ...flow.defaultActorId ? { defaultActorId: flow.defaultActorId } : {},
8103
8546
  ...store.resolveAuth ? { resolveAuth: store.resolveAuth } : {},
8547
+ ...store.runHelper ? { runHelper: store.runHelper } : {},
8104
8548
  ...opts.reuse ? { reuse: opts.reuse } : {},
8105
8549
  // The flow's own origin when it has one, the project's otherwise. A flow
8106
8550
  // that carries no origin is the normal case now: the environment belongs
@@ -8321,6 +8765,28 @@ function toStepRow(ordinal, result, step) {
8321
8765
  checks
8322
8766
  };
8323
8767
  }
8768
+ var CLOCK_SLACK_MS = 1e4;
8769
+ function helperRunner(delivered, secrets, startedAt = /* @__PURE__ */ new Date()) {
8770
+ if (!Array.isArray(delivered) || delivered.length === 0) return void 0;
8771
+ const helpers = new Map(
8772
+ DeliveredHelperSchema.array().parse(delivered).map((helper) => [helper.id, helper])
8773
+ );
8774
+ const since = new Date(startedAt.getTime() - CLOCK_SLACK_MS);
8775
+ return async (helperId, inputs) => {
8776
+ const helper = helpers.get(helperId);
8777
+ if (!helper) throw new Error("that helper was deleted, or was not sent with this run \u2014 choose another");
8778
+ try {
8779
+ const { value, detail } = await runHelper(helper.config, {
8780
+ resolveSecret: (ref) => secrets?.[ref],
8781
+ inputs,
8782
+ since
8783
+ });
8784
+ return { value, detail: `${helper.name}: ${detail}` };
8785
+ } catch (error) {
8786
+ throw new Error(`${helper.name}: ${error.message}`);
8787
+ }
8788
+ };
8789
+ }
8324
8790
  var LOCAL_CHROMIUM2 = { id: "chromium", provider: "local", engine: "chromium" };
8325
8791
  async function summaryOf2(storage, runId) {
8326
8792
  const detail = await storage.getRun(runId);
@@ -8381,6 +8847,7 @@ async function executeCase(opts) {
8381
8847
  const stepsByOccurrence = new Map(testCase.occurrences.map((o) => [o.occurrenceId, new Map(o.steps.map((s2) => [s2.id, s2]))]));
8382
8848
  const ordinals = /* @__PURE__ */ new Map();
8383
8849
  const writes = [];
8850
+ const helpers = helperRunner(opts.helpers, opts.secrets);
8384
8851
  let outcome;
8385
8852
  try {
8386
8853
  outcome = await replayCase({
@@ -8388,6 +8855,7 @@ async function executeCase(opts) {
8388
8855
  ...testCase.actors ? { actors: testCase.actors } : {},
8389
8856
  ...testCase.defaultActorId ? { defaultActorId: testCase.defaultActorId } : {},
8390
8857
  resolveAuth: authenticationResolver(opts.authentications),
8858
+ ...helpers ? { runHelper: helpers } : {},
8391
8859
  ...opts.reuse ? { reuse: opts.reuse } : {},
8392
8860
  baseUrl: testCase.baseUrl ?? baseUrl,
8393
8861
  ...testCase.environment ? { environment: testCase.environment } : {},
@@ -8582,6 +9050,7 @@ var RecordingCoordinator = class {
8582
9050
  /** Everything that used to happen before `start` answered. */
8583
9051
  async open(target, opts) {
8584
9052
  const config = this.deps.store.readConfig();
9053
+ const helpers = helperRunner(opts.helpers, opts.secrets);
8585
9054
  const configuredBase = config.baseUrl;
8586
9055
  try {
8587
9056
  this.session = await startRecordingSession({
@@ -8589,12 +9058,14 @@ var RecordingCoordinator = class {
8589
9058
  ...opts.authentication ? { authentication: {
8590
9059
  step: { id: "auth-preparation", type: "signIn", auth: opts.authentication, name: "Sign in before recording", role: "", scope: "", value: "", secret: false, legacySelector: "", proposals: [] },
8591
9060
  resolveAuth: authenticationResolver(opts.authentications),
8592
- resolveSecret: (ref) => opts.secrets?.[ref]
9061
+ resolveSecret: (ref) => opts.secrets?.[ref],
9062
+ ...helpers ? { runHelper: helpers } : {}
8593
9063
  } } : {},
8594
9064
  ...opts.preparation?.length ? { preparation: {
8595
9065
  steps: opts.preparation,
8596
9066
  resolveAuth: authenticationResolver(opts.authentications),
8597
- resolveSecret: (ref) => opts.secrets?.[ref]
9067
+ resolveSecret: (ref) => opts.secrets?.[ref],
9068
+ ...helpers ? { runHelper: helpers } : {}
8598
9069
  } } : {},
8599
9070
  // The project's saved defaults, unless this recording asks for
8600
9071
  // something else. Without the fallback the settings screen would write
@@ -8788,6 +9259,7 @@ var DebugCoordinator = class {
8788
9259
  ...flow.actors ? { actors: flow.actors } : {},
8789
9260
  ...flow.defaultActorId ? { defaultActorId: flow.defaultActorId } : {},
8790
9261
  ...store.resolveAuth ? { resolveAuth: store.resolveAuth } : {},
9262
+ ...store.runHelper ? { runHelper: store.runHelper } : {},
8791
9263
  environment: store.environmentFor(flow),
8792
9264
  resolveFile: (name) => store.resolveFile(name),
8793
9265
  baseUrl,
@@ -8921,6 +9393,7 @@ async function verifyFlowSteps(deps, opts) {
8921
9393
  const secrets = store.readSecrets();
8922
9394
  return await verifySteps({
8923
9395
  ...store.resolveAuth ? { resolveAuth: store.resolveAuth } : {},
9396
+ ...store.runHelper ? { runHelper: store.runHelper } : {},
8924
9397
  environment: store.environmentFor(flow),
8925
9398
  resolveFile: (name) => store.resolveFile(name),
8926
9399
  // Streamed, so the editor fills in as the pass proceeds instead of
@@ -8945,8 +9418,10 @@ async function verifyFlowSteps(deps, opts) {
8945
9418
  }
8946
9419
  function deliveredRun(delivered) {
8947
9420
  const { flow, secrets, config } = delivered;
9421
+ const runHelper2 = helperRunner(delivered.helpers, secrets);
8948
9422
  return {
8949
9423
  resolveAuth: authenticationResolver(delivered.authentications),
9424
+ ...runHelper2 ? { runHelper: runHelper2 } : {},
8950
9425
  readConfig: () => config,
8951
9426
  // By id, and only the one that was sent. A machine serving no checkout has
8952
9427
  // no other flow to confuse it with.
@@ -9040,7 +9515,7 @@ function resolveCheckouts(dirs) {
9040
9515
  return { projects, skipped };
9041
9516
  }
9042
9517
 
9043
- // ../agent-core/dist/chunk-4MOK25KG.js
9518
+ // ../agent-core/dist/chunk-LR7LBGCT.js
9044
9519
  import { WebSocket, WebSocketServer } from "ws";
9045
9520
  var HEARTBEAT_MS = 3e4;
9046
9521
  function adapt(socket) {
@@ -9927,6 +10402,7 @@ function executionHandlers(ctx) {
9927
10402
  const source = carried ? deliveredRun({
9928
10403
  flow: carried,
9929
10404
  authentications: command.args["authentications"],
10405
+ helpers: command.args["helpers"],
9930
10406
  secrets: command.args["secrets"] ?? {},
9931
10407
  config: command.args["config"] ?? {}
9932
10408
  }) : storeFor(project.root);
@@ -10033,6 +10509,7 @@ function executionHandlers(ctx) {
10033
10509
  const { outcome, run } = await executeCase({
10034
10510
  testCase,
10035
10511
  authentications: command.args["authentications"],
10512
+ helpers: command.args["helpers"],
10036
10513
  projectId: project.id,
10037
10514
  config: command.args["config"] ?? {},
10038
10515
  secrets: command.args["secrets"] ?? {},
@@ -10115,6 +10592,7 @@ function recordingHandlers({ recorder, onActivity }) {
10115
10592
  ...Array.isArray(command.args["preparation"]) && command.args["preparation"].length > 0 ? { preparation: RecordedStepSchema.array().parse(command.args["preparation"]) } : {},
10116
10593
  ...command.args["authentication"] || command.args["preparation"] ? {
10117
10594
  authentications: command.args["authentications"],
10595
+ helpers: command.args["helpers"],
10118
10596
  secrets: command.args["secrets"] ?? {}
10119
10597
  } : {},
10120
10598
  projectId: project.id,
@@ -10201,6 +10679,7 @@ function debuggingHandlers(ctx) {
10201
10679
  ...carried ? { delivered: {
10202
10680
  flow: carried,
10203
10681
  authentications: command.args["authentications"],
10682
+ helpers: command.args["helpers"],
10204
10683
  secrets: command.args["secrets"] ?? {},
10205
10684
  config: command.args["config"] ?? {}
10206
10685
  } } : {},
@@ -10242,6 +10721,7 @@ function debuggingHandlers(ctx) {
10242
10721
  delivered: {
10243
10722
  flow: carried,
10244
10723
  authentications: command.args["authentications"],
10724
+ helpers: command.args["helpers"],
10245
10725
  secrets: command.args["secrets"] ?? {},
10246
10726
  config: command.args["config"] ?? {},
10247
10727
  scratchRoot: scratchRoot(project.id)
@@ -10372,6 +10852,7 @@ function createAgentRuntime(options) {
10372
10852
  store: deliveredRun({
10373
10853
  flow: delivered.flow,
10374
10854
  authentications: delivered.authentications,
10855
+ helpers: delivered.helpers,
10375
10856
  secrets: delivered.secrets,
10376
10857
  config: delivered.config
10377
10858
  }),
@@ -10725,7 +11206,17 @@ export {
10725
11206
  AGENT_VERSION,
10726
11207
  prepareBrowsers,
10727
11208
  browsersReady,
11209
+ connectionFor,
11210
+ resolveService,
10728
11211
  connect,
11212
+ RecordedStepSchema,
11213
+ authenticationResolver,
11214
+ checkAssertion,
11215
+ openRunSession,
11216
+ closeRunSession,
11217
+ replayFlow,
11218
+ executeStep,
11219
+ helperRunner,
10729
11220
  resolveCheckouts,
10730
11221
  start,
10731
11222
  status,