@swmansion/argent 0.22.1-next.4 → 0.22.1-next.6

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-cmds.mjs CHANGED
@@ -2675,11 +2675,13 @@ async function applyClientFileDirectives(result) {
2675
2675
  var ToolInvocationError = class extends Error {
2676
2676
  errorCode;
2677
2677
  errorKind;
2678
+ issues;
2678
2679
  constructor(message, signal) {
2679
2680
  super(message);
2680
2681
  this.name = "ToolInvocationError";
2681
2682
  this.errorCode = signal?.errorCode;
2682
2683
  this.errorKind = signal?.errorKind;
2684
+ this.issues = signal?.issues;
2683
2685
  }
2684
2686
  };
2685
2687
  function authHeaders2(token) {
@@ -2727,6 +2729,10 @@ async function consumeToolStream(body, onProgress) {
2727
2729
  const { result: data } = await applyClientFileDirectives(final.data);
2728
2730
  return { data, note: final.note };
2729
2731
  }
2732
+ function errorBodyMessage(body) {
2733
+ if (Array.isArray(body.issues) && typeof body.message === "string") return body.message;
2734
+ return body.error ?? body.message;
2735
+ }
2730
2736
  function createToolsClient(options = {}) {
2731
2737
  let cached2 = null;
2732
2738
  async function baseUrl() {
@@ -2781,13 +2787,11 @@ function createToolsClient(options = {}) {
2781
2787
  }
2782
2788
  const json = await res.json().catch(() => ({}));
2783
2789
  if (!res.ok) {
2784
- throw new ToolInvocationError(
2785
- json.error ?? json.message ?? `${res.status} ${res.statusText}`,
2786
- {
2787
- errorCode: json.error_code,
2788
- errorKind: json.error_kind
2789
- }
2790
- );
2790
+ throw new ToolInvocationError(errorBodyMessage(json) ?? `${res.status} ${res.statusText}`, {
2791
+ errorCode: json.error_code,
2792
+ errorKind: json.error_kind,
2793
+ issues: Array.isArray(json.issues) ? json.issues : void 0
2794
+ });
2791
2795
  }
2792
2796
  const { result: data } = await applyClientFileDirectives(json.data);
2793
2797
  return { data, note: json.note };
@@ -2823,6 +2827,10 @@ var FLAG_REGISTRY = [
2823
2827
  name: "tool-server-event-log",
2824
2828
  description: "Write structured tool-server lifecycle events to a JSONL file."
2825
2829
  },
2830
+ {
2831
+ name: "boot-sound",
2832
+ description: "Default boot-device's `sound` argument to true so Android emulators boot with audio output instead of muted. Only the argument's default changes \u2014 an explicit `sound: false` on a call still boots muted."
2833
+ },
2826
2834
  {
2827
2835
  name: "microinteractions",
2828
2836
  description: "Amplify device actions with matching animations of the host window, so what happens on the guest is also visible on the desktop. Purely cosmetic, macOS only, and never affects whether the underlying action succeeds. Off by default."
@@ -7202,7 +7210,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
7202
7210
  var SESSION_ID2 = randomUUID5();
7203
7211
  function readCliVersion() {
7204
7212
  if (true) {
7205
- return "0.22.1-next.4";
7213
+ return "0.22.1-next.6";
7206
7214
  }
7207
7215
  return "0.0.0";
7208
7216
  }
@@ -8005,7 +8013,11 @@ function findMissingRequired(payload, schema) {
8005
8013
  }
8006
8014
  return names.filter((name) => !Object.hasOwn(payload, name));
8007
8015
  }
8008
- function describeServerValidationFailure(err, payload, schema) {
8016
+ function serverIssueList(err) {
8017
+ const carried = err?.issues;
8018
+ if (Array.isArray(carried)) {
8019
+ return carried.length > 0 && carried.every(isValidationIssue) ? carried : null;
8020
+ }
8009
8021
  const message = err instanceof Error ? err.message : typeof err === "string" ? err : null;
8010
8022
  if (message === null) return null;
8011
8023
  let parsed;
@@ -8015,7 +8027,11 @@ function describeServerValidationFailure(err, payload, schema) {
8015
8027
  return null;
8016
8028
  }
8017
8029
  if (!Array.isArray(parsed) || parsed.length === 0) return null;
8018
- if (!parsed.every(isValidationIssue)) return null;
8030
+ return parsed.every(isValidationIssue) ? parsed : null;
8031
+ }
8032
+ function describeServerValidationFailure(err, payload, schema) {
8033
+ const parsed = serverIssueList(err);
8034
+ if (parsed === null) return null;
8019
8035
  const properties = schema?.properties ?? {};
8020
8036
  const addressesThisTool = (issue) => issue.path.length === 0 || typeof issue.path[0] === "string" && Object.hasOwn(properties, issue.path[0]);
8021
8037
  if (!parsed.every(addressesThisTool)) return null;
@@ -16428,7 +16428,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
16428
16428
  var SESSION_ID = randomUUID4();
16429
16429
  function readCliVersion() {
16430
16430
  if (true) {
16431
- return "0.22.1-next.4";
16431
+ return "0.22.1-next.6";
16432
16432
  }
16433
16433
  return "0.0.0";
16434
16434
  }
@@ -16855,6 +16855,12 @@ async function applyClientFileDirectives(result) {
16855
16855
  return { result: rewritten, written };
16856
16856
  }
16857
16857
 
16858
+ // ../argent-tools-client/src/tools-client.ts
16859
+ function errorBodyMessage(body) {
16860
+ if (Array.isArray(body.issues) && typeof body.message === "string") return body.message;
16861
+ return body.error ?? body.message;
16862
+ }
16863
+
16858
16864
  // ../argent-tools-client/src/artifacts.ts
16859
16865
  import { copyFile, mkdir as mkdir4, readFile as readFile4, realpath, rm as rm3, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
16860
16866
  import { constants as fsConstants } from "node:fs";
@@ -18685,7 +18691,7 @@ async function startMcpServer(options) {
18685
18691
  fetchTimeoutMs: meta2?.longRunning ? null : FETCH_TIMEOUT_MS
18686
18692
  });
18687
18693
  const json = await res.json();
18688
- if (!res.ok) throw new Error(json.error ?? json.message ?? res.statusText);
18694
+ if (!res.ok) throw new Error(errorBodyMessage(json) ?? res.statusText);
18689
18695
  const { result: data } = await applyClientFileDirectives(json.data);
18690
18696
  return { result: data, outputHint: meta2?.outputHint, note: json.note };
18691
18697
  }
@@ -16063,6 +16063,63 @@ function terminatingSignalCause(message) {
16063
16063
  error_kind: "unknown"
16064
16064
  });
16065
16065
  }
16066
+ function valueAtPath(root, path41) {
16067
+ let current = root;
16068
+ for (const key2 of path41) {
16069
+ if (current === null || typeof current !== "object") return void 0;
16070
+ if (!Object.hasOwn(current, key2)) return void 0;
16071
+ current = current[key2];
16072
+ }
16073
+ return current;
16074
+ }
16075
+ function describeParamIssues(error52, params) {
16076
+ const allKeys = params !== null && typeof params === "object" && !Array.isArray(params) ? Object.keys(params) : [];
16077
+ const supplied = allKeys.slice(0, 24);
16078
+ const truncated = allKeys.length > supplied.length;
16079
+ const parts2 = error52.issues.map((issue2) => {
16080
+ const at = issue2.path.length > 0 ? issue2.path.join(".") : "(root)";
16081
+ if (issue2.code === "custom") {
16082
+ return issue2.path.length > 0 ? `\`${at}\`: ${issue2.message}` : issue2.message;
16083
+ }
16084
+ if (valueAtPath(params, issue2.path) === void 0) {
16085
+ const expected = issue2.expected;
16086
+ const kind = typeof expected === "string" ? ` (${expected})` : "";
16087
+ return `\`${at}\` is required${kind} and was not provided`;
16088
+ }
16089
+ if (issue2.code === "unrecognized_keys") {
16090
+ const keys = issue2.keys ?? [];
16091
+ const at2 = issue2.path.length > 0 ? `${issue2.path.join(".")}.` : "";
16092
+ return `unknown parameter${keys.length === 1 ? "" : "s"} ${keys.map((k) => `\`${at2}${k}\``).join(", ")}`;
16093
+ }
16094
+ if (issue2.code === "invalid_union") {
16095
+ const branches = issue2.errors ?? [];
16096
+ const alternatives = [];
16097
+ const seen = /* @__PURE__ */ new Set();
16098
+ let moreAlternatives = false;
16099
+ for (const branch of branches) {
16100
+ for (const inner of branch) {
16101
+ const innerAt = inner.path.length > 0 ? `${at}.${inner.path.join(".")}: ` : "";
16102
+ const text = `${innerAt}${inner.message}`;
16103
+ if (seen.has(text)) continue;
16104
+ if (alternatives.length >= MAX_UNION_ALTERNATIVES) {
16105
+ moreAlternatives = true;
16106
+ break;
16107
+ }
16108
+ seen.add(text);
16109
+ alternatives.push(text);
16110
+ }
16111
+ if (moreAlternatives) break;
16112
+ }
16113
+ if (alternatives.length > 0) {
16114
+ return `\`${at}\`: ${alternatives.join("; or ")}${moreAlternatives ? "; or \u2026" : ""}`;
16115
+ }
16116
+ }
16117
+ return `\`${at}\`: ${issue2.message}`;
16118
+ });
16119
+ const sent = supplied.length > 0 ? ` You sent: ${supplied.map((k) => `\`${k}\``).join(", ")}${truncated ? ", \u2026" : ""}.` : "";
16120
+ const body = parts2.length > 0 ? `${parts2.map((p) => p.replace(/\.$/, "")).join("; ")}.` : "";
16121
+ return `${body}${sent}`.trim() || "invalid parameters";
16122
+ }
16066
16123
  function formatInteractionMessage(format, fallback) {
16067
16124
  try {
16068
16125
  return format() ?? fallback;
@@ -16070,7 +16127,7 @@ function formatInteractionMessage(format, fallback) {
16070
16127
  return fallback;
16071
16128
  }
16072
16129
  }
16073
- var import_node_crypto2, Registry;
16130
+ var import_node_crypto2, Registry, MAX_UNION_ALTERNATIVES;
16074
16131
  var init_registry = __esm({
16075
16132
  "../registry/src/registry.ts"() {
16076
16133
  "use strict";
@@ -16151,7 +16208,15 @@ var init_registry = __esm({
16151
16208
  if (definition.zodSchema) {
16152
16209
  const parsed = definition.zodSchema.safeParse(params ?? {});
16153
16210
  if (!parsed.success) {
16154
- throw new Error(`Invalid params for tool "${id}": ${parsed.error.message}`);
16211
+ throw new FailureError(
16212
+ `Invalid params for tool "${id}": ${describeParamIssues(parsed.error, params)}`,
16213
+ {
16214
+ error_code: FAILURE_CODES.TOOL_INPUT_INVALID,
16215
+ failure_stage: "tool_params_parse",
16216
+ failure_area: "registry",
16217
+ error_kind: "validation"
16218
+ }
16219
+ );
16155
16220
  }
16156
16221
  effectiveParams = parsed.data;
16157
16222
  }
@@ -16388,6 +16453,7 @@ var init_registry = __esm({
16388
16453
  this._transition(node, cause ? "ERROR" /* ERROR */ : "IDLE" /* IDLE */, cause);
16389
16454
  }
16390
16455
  };
16456
+ MAX_UNION_ALTERNATIVES = 12;
16391
16457
  }
16392
16458
  });
16393
16459
 
@@ -91789,6 +91855,10 @@ var FLAG_REGISTRY = [
91789
91855
  name: "tool-server-event-log",
91790
91856
  description: "Write structured tool-server lifecycle events to a JSONL file."
91791
91857
  },
91858
+ {
91859
+ name: "boot-sound",
91860
+ description: "Default boot-device's `sound` argument to true so Android emulators boot with audio output instead of muted. Only the argument's default changes \u2014 an explicit `sound: false` on a call still boots muted."
91861
+ },
91792
91862
  {
91793
91863
  name: "microinteractions",
91794
91864
  description: "Amplify device actions with matching animations of the host window, so what happens on the guest is also visible on the desktop. Purely cosmetic, macOS only, and never affects whether the underlying action succeeds. Off by default."
@@ -95664,7 +95734,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
95664
95734
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
95665
95735
  function readCliVersion() {
95666
95736
  if (true) {
95667
- return "0.22.1-next.4";
95737
+ return "0.22.1-next.6";
95668
95738
  }
95669
95739
  return "0.0.0";
95670
95740
  }
@@ -113871,14 +113941,23 @@ async function resolveFileInputs(def, body, lookupUpload) {
113871
113941
  };
113872
113942
  const specs = def.fileInputs;
113873
113943
  if (!specs || specs.length === 0 || typeof body !== "object" || body === null) {
113874
- return { args: body ?? {}, fileInputs: void 0, cleanup };
113944
+ return {
113945
+ args: body ?? {},
113946
+ fileInputs: void 0,
113947
+ derivedTargets: [],
113948
+ cleanup
113949
+ };
113875
113950
  }
113876
113951
  const args = { ...body };
113877
113952
  let resolved;
113953
+ const derivedTargets = [];
113878
113954
  try {
113879
113955
  for (const spec of specs) {
113880
113956
  const value = args[spec.target];
113881
113957
  if (!isFileInputWire(value)) continue;
113958
+ if (spec.path !== `\${${spec.target}}` && !derivedTargets.includes(spec.target)) {
113959
+ derivedTargets.push(spec.target);
113960
+ }
113882
113961
  if (spec.unwrapWhenSet !== void 0 && isParamSet(args[spec.unwrapWhenSet])) {
113883
113962
  args[spec.target] = value.path;
113884
113963
  continue;
@@ -113895,7 +113974,7 @@ async function resolveFileInputs(def, body, lookupUpload) {
113895
113974
  await cleanup();
113896
113975
  throw err;
113897
113976
  }
113898
- return { args, fileInputs: resolved, cleanup };
113977
+ return { args, fileInputs: resolved, derivedTargets, cleanup };
113899
113978
  }
113900
113979
 
113901
113980
  // ../tool-server/src/utils/debugger/device-alias.ts
@@ -114186,6 +114265,14 @@ function isToolExposed(def) {
114186
114265
  function findDependencyMissing(err) {
114187
114266
  return findErrorInCauseChain(err, DependencyMissingError);
114188
114267
  }
114268
+ function omitKeys(args, keys) {
114269
+ if (keys.length === 0 || args === null || typeof args !== "object" || Array.isArray(args)) {
114270
+ return args;
114271
+ }
114272
+ const copy = { ...args };
114273
+ for (const key2 of keys) delete copy[key2];
114274
+ return copy;
114275
+ }
114189
114276
  function errorSignalFields(err) {
114190
114277
  const signal = getFailureSignal(err);
114191
114278
  return signal ? { error_code: signal.error_code, error_kind: signal.error_kind } : {};
@@ -114525,6 +114612,7 @@ function createHttpApp(registry2, options) {
114525
114612
  }
114526
114613
  let bodyArgs;
114527
114614
  let resolvedFileInputs;
114615
+ let derivedTargets;
114528
114616
  try {
114529
114617
  const resolved = await resolveFileInputs(def, req.body, (id) => {
114530
114618
  const entry = uploads.get(id);
@@ -114533,6 +114621,7 @@ function createHttpApp(registry2, options) {
114533
114621
  });
114534
114622
  bodyArgs = resolved.args;
114535
114623
  resolvedFileInputs = resolved.fileInputs;
114624
+ derivedTargets = resolved.derivedTargets;
114536
114625
  res.once("close", () => void resolved.cleanup());
114537
114626
  } catch (err) {
114538
114627
  if (err instanceof FileInputError) {
@@ -114556,7 +114645,11 @@ function createHttpApp(registry2, options) {
114556
114645
  req.body,
114557
114646
  { invalid_params: deriveInvalidParams(parseResult.error, declared) }
114558
114647
  );
114559
- res.status(400).json({ error: parseResult.error.message });
114648
+ res.status(400).json({
114649
+ error: parseResult.error.message,
114650
+ message: describeParamIssues(parseResult.error, omitKeys(bodyArgs, derivedTargets)),
114651
+ issues: parseResult.error.issues
114652
+ });
114560
114653
  return;
114561
114654
  }
114562
114655
  parsedData = parseResult.data;
@@ -114697,6 +114790,10 @@ function createHttpApp(registry2, options) {
114697
114790
  res.status(400).json({ error: invalidInputErr.message, ...errorSignalFields(err) });
114698
114791
  return;
114699
114792
  }
114793
+ if (getFailureSignal(err)?.error_code === FAILURE_CODES.TOOL_INPUT_INVALID) {
114794
+ res.status(400).json({ error: formatErrorForAgent(err), ...errorSignalFields(err) });
114795
+ return;
114796
+ }
114700
114797
  const notImplementedErr = findErrorInCauseChain(err, NotImplementedOnPlatformError);
114701
114798
  if (notImplementedErr) {
114702
114799
  res.status(501).json({
@@ -118670,6 +118767,9 @@ var zodSchema9 = external_exports.object({
118670
118767
  "Android/Vega: overall budget for the boot sequence. Default 480000 (8 min) on Android, 120000 (2 min) on Vega. Clamped to [30s, 15min]. Ignored on iOS."
118671
118768
  ),
118672
118769
  force: external_exports.boolean().optional().describe("Shut down and re-boot the device even if already running."),
118770
+ sound: external_exports.boolean().optional().describe(
118771
+ "Android only: boot the emulator with audio output enabled. Defaults to false \u2014 argent boots emulators MUTED so several agent-driven devices don't all play sound on the host machine; pass `true` when the task involves playing, hearing, or testing audio. Takes effect at boot: if the emulator is already running muted, add `force: true` to reboot it with sound. A boot snapshot saved in the other audio mode can't be reused, so the first boot after toggling is a slower cold boot. The `boot-sound` argent flag flips this default to true. Ignored on iOS/Vega/Electron, which argent never mutes."
118772
+ ),
118673
118773
  headless: external_exports.boolean().optional().describe(
118674
118774
  "iOS only: boot the simulator core WITHOUT opening the Simulator.app GUI window. The device still streams via simulator-server; used by Argent Lens. Set the `ARGENT_SIMULATOR_NO_WINDOW` env var (1/true/yes) to force this host-wide without passing the flag per call (the iOS analog of `ARGENT_EMULATOR_NO_WINDOW`). Ignored on Android/Vega/Electron, which have no equivalent GUI step."
118675
118775
  ),
@@ -118687,13 +118787,15 @@ function bootTarget(params) {
118687
118787
  return params.udid ?? params.avdName ?? params.vvdImage ?? params.electronAppPath ?? "device";
118688
118788
  }
118689
118789
  var LAUNCH_HARDENING_ARGS = [
118690
- "-noaudio",
118691
118790
  "-no-boot-anim",
118692
118791
  "-netfast",
118693
118792
  "-crash-report-mode",
118694
118793
  "never",
118695
118794
  "-no-metrics"
118696
118795
  ];
118796
+ function launchHardeningArgs(sound) {
118797
+ return sound ? [...LAUNCH_HARDENING_ARGS] : ["-noaudio", ...LAUNCH_HARDENING_ARGS];
118798
+ }
118697
118799
  var STAGE_BUDGET = {
118698
118800
  adbRegister: 6e4,
118699
118801
  // adb devices sees the serial for this AVD
@@ -119125,6 +119227,7 @@ async function bootAndroidImpl(params) {
119125
119227
  await ensureDep("emulator");
119126
119228
  const gpuMode = selectGpuMode();
119127
119229
  const extraEmulatorArgs = androidHeadlessFromEnv() ? ["-no-window"] : [];
119230
+ const hardeningArgs = launchHardeningArgs(params.sound);
119128
119231
  for (const msg of linuxBootDiagnostics(params.avdName) ?? []) {
119129
119232
  console.warn(`[boot-device:linux] ${msg}`);
119130
119233
  }
@@ -119203,7 +119306,7 @@ async function bootAndroidImpl(params) {
119203
119306
  } else {
119204
119307
  const RENDERER_ARGS = ["-gpu", gpuMode, ...extraEmulatorArgs];
119205
119308
  const probe3 = await checkSnapshotLoadable(params.avdName, "default_boot", {
119206
- extraArgs: [...RENDERER_ARGS, ...LAUNCH_HARDENING_ARGS]
119309
+ extraArgs: [...RENDERER_ARGS, ...hardeningArgs]
119207
119310
  });
119208
119311
  if (!probe3.loadable) {
119209
119312
  hotBootFailureReason = `-check-snapshot-loadable: ${probe3.reason ?? "unknown"}`;
@@ -119214,7 +119317,7 @@ async function bootAndroidImpl(params) {
119214
119317
  "-force-snapshot-load",
119215
119318
  "-no-snapshot-save",
119216
119319
  ...RENDERER_ARGS,
119217
- ...LAUNCH_HARDENING_ARGS,
119320
+ ...hardeningArgs,
119218
119321
  ...crashReportArgs
119219
119322
  ];
119220
119323
  const hotAttemptDeadline = Math.min(overallDeadline, Date.now() + HOT_BOOT_BUDGET_MS);
@@ -119258,7 +119361,7 @@ async function bootAndroidImpl(params) {
119258
119361
  "-gpu",
119259
119362
  gpuMode,
119260
119363
  ...extraEmulatorArgs,
119261
- ...LAUNCH_HARDENING_ARGS,
119364
+ ...hardeningArgs,
119262
119365
  ...crashReportArgs
119263
119366
  ];
119264
119367
  let coldResult;
@@ -119455,7 +119558,11 @@ Android boots take 2\u201310 minutes depending on machine and cold/warm state; t
119455
119558
  return bootAndroid({
119456
119559
  avdName: params.avdName,
119457
119560
  bootTimeoutMs: params.bootTimeoutMs ?? 48e4,
119458
- force: params.force
119561
+ force: params.force,
119562
+ // An explicit argument always wins; the `boot-sound` flag only moves
119563
+ // the default when the caller left `sound` unset. Read live per call
119564
+ // so `argent enable/disable boot-sound` applies without a restart.
119565
+ sound: params.sound ?? isFlagEnabled("boot-sound")
119459
119566
  });
119460
119567
  }
119461
119568
  if (hasVega) {
@@ -123240,6 +123347,7 @@ init_zod();
123240
123347
 
123241
123348
  // ../tool-server/src/utils/sub-invoke.ts
123242
123349
  var import_node_crypto7 = require("node:crypto");
123350
+ init_src();
123243
123351
  async function invokeSubTool(registry2, ctx, toolId, args) {
123244
123352
  const signal = ctx?.signal;
123245
123353
  const recordChildInvocation = ctx?.recordChildInvocation;
@@ -123258,6 +123366,14 @@ async function invokeSubTool(registry2, ctx, toolId, args) {
123258
123366
  release();
123259
123367
  }
123260
123368
  }
123369
+ function describeNestedParamError(registry2, err, toolId, dispatchedArgs, authoredArgs) {
123370
+ if (getFailureSignal(err)?.error_code !== FAILURE_CODES.TOOL_INPUT_INVALID) return void 0;
123371
+ const zodSchema76 = registry2.getTool(toolId)?.zodSchema;
123372
+ if (!zodSchema76) return void 0;
123373
+ const parsed = zodSchema76.safeParse(dispatchedArgs ?? {});
123374
+ if (parsed.success) return void 0;
123375
+ return `Invalid params for tool "${toolId}": ${describeParamIssues(parsed.error, authoredArgs)}`;
123376
+ }
123261
123377
 
123262
123378
  // ../tool-server/src/tools/await-ui-element/index.ts
123263
123379
  init_zod();
@@ -129736,8 +129852,8 @@ Stops on the first error (or unmet await-ui-element condition) and returns parti
129736
129852
  throw err;
129737
129853
  }
129738
129854
  }
129855
+ const toolArgs = { ...step.args, udid };
129739
129856
  try {
129740
- const toolArgs = { ...step.args, udid };
129741
129857
  const result = await invokeSubTool(registry2, ctx, step.tool, toolArgs);
129742
129858
  if (isUnmetUiWaitResult(step.tool, result)) {
129743
129859
  const note = result.note;
@@ -129749,9 +129865,16 @@ Stops on the first error (or unmet await-ui-element condition) and returns parti
129749
129865
  }
129750
129866
  results.push({ tool: step.tool, result });
129751
129867
  } catch (err) {
129868
+ const reframed = describeNestedParamError(
129869
+ registry2,
129870
+ err,
129871
+ step.tool,
129872
+ toolArgs,
129873
+ step.args ?? {}
129874
+ );
129752
129875
  results.push({
129753
129876
  tool: step.tool,
129754
- error: err instanceof Error ? err.message : String(err)
129877
+ error: reframed ?? (err instanceof Error ? err.message : String(err))
129755
129878
  });
129756
129879
  break;
129757
129880
  }
@@ -145856,7 +145979,9 @@ var zodSchema63 = external_exports.object({
145856
145979
  project_root: external_exports.string().describe(
145857
145980
  "Absolute path to the project root of the flow being recorded \u2014 the same value passed to flow-start-recording. Together with `name` it identifies which recording this step belongs to."
145858
145981
  ),
145859
- command: external_exports.string().describe('MCP tool name (e.g. "gesture-tap", "screenshot", "launch-app")'),
145982
+ command: external_exports.string().describe(
145983
+ 'MCP tool name (e.g. "gesture-tap", "screenshot", "launch-app") \u2014 a TOOL, not a flow directive. A flow-file directive name ("tap", "launch", "run", "type", "await", "assert", "pinch", "echo", "wait", "long-press", "scroll-to", "snapshot", "when") is answered with guidance, and nothing runs or is recorded: most name the tool that records the directive, while "wait", "long-press", "scroll-to", "snapshot" and "when" have no recording tool at all and are answered with what to do instead. A recording tool (flow-add-step, flow-add-echo, flow-start-recording, flow-finish-recording) is refused the same way, each for its own reason \u2014 nesting one would erase this flow at replay, end the take, or write the step twice.'
145984
+ ),
145860
145985
  args: external_exports.string().optional().describe(
145861
145986
  `Tool arguments as a JSON string, e.g. '{"udid": "ABC", "x": 0.5, "y": 0.3}'. Omit for tools with no arguments.`
145862
145987
  ),
@@ -146056,6 +146181,77 @@ async function captureTapSelector(registry2, session, udid, point) {
146056
146181
  };
146057
146182
  }
146058
146183
  }
146184
+ async function activeFlowState(session) {
146185
+ if (session.persist === "host") {
146186
+ try {
146187
+ session.flow = parseFlow(await fs48.readFile(session.filePath, "utf8"));
146188
+ } catch (err) {
146189
+ return {
146190
+ stepCount: session.flow.steps.length,
146191
+ note: `The persisted flow could not be read and parsed (${err instanceof Error ? err.message : String(err)}); the step count is from the last valid in-memory snapshot.`
146192
+ };
146193
+ }
146194
+ }
146195
+ return { stepCount: session.flow.steps.length };
146196
+ }
146197
+ async function recordNothing(session, guidance) {
146198
+ const { stepCount, note } = await activeFlowState(session);
146199
+ return {
146200
+ message: `${guidance} Nothing was executed and no step was recorded.${note ? ` ${note}` : ""}`,
146201
+ toolResult: void 0,
146202
+ stepCount,
146203
+ savedTo: session.filePath
146204
+ };
146205
+ }
146206
+ var DIRECTIVE_COMMAND_HINTS = {
146207
+ tap: { tool: "gesture-tap", rewritten: true },
146208
+ launch: {
146209
+ tool: "restart-app",
146210
+ rewritten: true,
146211
+ rewriteCondition: "when it carries only the bundle id (a call with an extra arg, e.g. an Android `activity`, is kept as a raw `tool: restart-app` step to convert during polish)"
146212
+ },
146213
+ run: {
146214
+ tool: "flow-execute",
146215
+ rewritten: true,
146216
+ rewriteCondition: "when the target resolves as a sibling flow in this recording's folder \u2014 a `name` that does not is kept as a raw `tool: flow-execute` step, and so is every target in a REMOTE recording (`run:` composition is host-resolved, so the host cannot validate the client's siblings); a `flow_path` that is not a sibling is refused outright and records nothing"
146217
+ },
146218
+ type: { tool: "keyboard", rewritten: false },
146219
+ await: { tool: AWAIT_UI_ELEMENT_TOOL_ID, rewritten: false },
146220
+ assert: { tool: AWAIT_UI_ELEMENT_TOOL_ID, rewritten: false },
146221
+ pinch: { tool: "gesture-pinch", rewritten: false }
146222
+ };
146223
+ var NESTED_RECORDER_TOOLS = {
146224
+ "flow-add-echo": "`flow-add-echo` records a step itself, so it must be called DIRECTLY, not through flow-add-step \u2014 nesting it would write the echo AND a `tool: flow-add-echo` step that fails on every replay.",
146225
+ "flow-add-step": "flow-add-step cannot record itself. Pass the MCP tool you want to execute as `command`.",
146226
+ "flow-start-recording": "`flow-start-recording` truncates the flow it names. Recording it as a step would erase this flow at replay; call it directly when you want to start a recording.",
146227
+ "flow-finish-recording": "`flow-finish-recording` ends the recording, so it cannot also be a step in it. Call it directly when the walkthrough is complete."
146228
+ };
146229
+ function isToolNotFound(err, command) {
146230
+ return err instanceof ToolNotFoundError && err.toolId === command;
146231
+ }
146232
+ function directiveCommandHint(command) {
146233
+ if (command === "echo") {
146234
+ return `"echo" is a flow directive, not a tool. Call \`flow-add-echo\` DIRECTLY \u2014 not through flow-add-step, which would run it as a nested tool AND record a \`tool: flow-add-echo\` step that fails on every replay.`;
146235
+ }
146236
+ if (command === "wait") {
146237
+ return `"wait" is a flow directive, not a tool, and there is no tool that records one \u2014 a fixed sleep is not a readiness signal. Record the thing you are actually waiting for with \`${AWAIT_UI_ELEMENT_TOOL_ID}\` instead.`;
146238
+ }
146239
+ if (command === "long-press") {
146240
+ return `"long-press" is a flow directive, not a tool, and no tool records one \u2014 there is no gesture-long-press. Record the rest of the path, then add the \`long-press:\` step by hand during polish and prove it with the replay.`;
146241
+ }
146242
+ if (command === "scroll-to") {
146243
+ return `"scroll-to" is a flow directive, not a tool, and no tool records one \u2014 it SEARCHES, scrolling until the target is visible, which no single recorded gesture reproduces. Record the movement with \`gesture-swipe\` (\`gesture-scroll\` on chromium) if the path needs it, then add the \`scroll-to:\` step by hand during polish and prove it with the replay.`;
146244
+ }
146245
+ if (command === "snapshot") {
146246
+ return `"snapshot" is a flow directive, not a tool, and no tool records one \u2014 it compares the screen against a stored baseline, which \`screenshot-diff\` does not manage. Add the \`snapshot:\` step by hand during polish, then adopt its baseline with a run that sets updateBaselines, and review the PNG before committing it.`;
146247
+ }
146248
+ if (command === "when") {
146249
+ return `"when" is a flow directive, not a tool, and no tool records one \u2014 it GUARDS the steps nested under it, so there is no action of its own to run. Record those steps, then wrap them in the \`when:\` block by hand during polish and prove both branches with the replay.`;
146250
+ }
146251
+ const hint = Object.hasOwn(DIRECTIVE_COMMAND_HINTS, command) ? DIRECTIVE_COMMAND_HINTS[command] : void 0;
146252
+ if (!hint) return void 0;
146253
+ return `"${command}" is a flow directive, not a tool. Record it by calling \`${hint.tool}\` through flow-add-step` + (hint.rewritten ? ` \u2014 the recorder rewrites it into the \`${command}:\` step ${hint.rewriteCondition ?? "for you"}. Where the call is recorded at all, a \`delayMs\` on it opts out of the rewrite: the step is then kept in its raw \`tool: ${hint.tool}\` form (a replay delay has no directive form), so leave \`delayMs\` off if you want the \`${command}:\` step.` : `. It is stored as a raw \`tool: ${hint.tool}\` step; converting it to \`${command}:\` is part of the polish pass.`);
146254
+ }
146059
146255
  var RUN_TARGET_COMMAND = "flow-execute";
146060
146256
  async function rewriteSiblingFlowPath(session, args) {
146061
146257
  const flowPath = args.flow_path;
@@ -146173,12 +146369,12 @@ function createFlowAddStepTool(registry2) {
146173
146369
  // Name the flow: recordings are concurrent, so several of these lines can
146174
146370
  // interleave in one log and "the recorded flow" would not say which.
146175
146371
  startedMsg: ({ params }) => `Adding ${params.command} step to flow ${params.name}`,
146176
- completedMsg: ({ params }) => `Added ${params.command} step to flow ${params.name}`,
146372
+ completedMsg: ({ params, result }) => result.recorded === void 0 ? `Recorded no ${params.command} step in flow ${params.name}` : `Added ${params.command} step to flow ${params.name}`,
146177
146373
  failedMsg: ({ params, failureSignal: failureSignal2 }) => `Failed to add ${params.command} step to flow ${params.name}: ${failureSignal2.error_code}`
146178
146374
  },
146179
146375
  description: `Execute a tool call and record it as a step in the flow named by \`name\` + \`project_root\` (the recording must already be open \u2014 see flow-start-recording). Use when recording a flow and you want to run and capture each action. A coordinate \`gesture-tap\` is recorded as a portable \`tap: { selector }\` step when the tapped element has stable text/identifier (otherwise coordinates are kept with a warning); a \`restart-app\` is recorded as a \`launch\` step (record one FIRST to make the flow a self-contained e2e flow; restart-app has no chromium support, so a chromium flow records as a fragment \u2014 add the \`launch: { chromium: <app path> }\` line to the YAML afterward, deleting the executionPrerequisite line if one was recorded: a flow that starts with a launch must not declare it).
146180
146376
  A recorded \`await-ui-element\` that PASSED is re-probed against the tree the RUNNER resolves \`await:\`/\`assert:\` directives against, which is NOT the tree the live call read; a wait that came back \`{ success: false }\` is not probed at all, and its warning says so; when the condition does not hold there the step is still recorded and \`message\` carries a warning to read before converting \u2014 whether the conversion actually breaks depends on WHY the two disagree, since a screen that moved on between the live wait and the re-probe reads the same way. If that tree could not be read at all, the warning says so instead: the conversion is UNKNOWN, not known-bad. The probe judges the selector exactly as recorded, so write the conversion in the strict map spelling (\`{ visible: { text: Continue } }\`, copying the step's \`selector:\`) \u2014 the bare-string spelling (\`{ visible: Continue }\`) re-parses as a loose selector that resolves identifier-first and falls back to text, which is a different check. \`message\` also warns when the live wait itself came back \`{ success: false }\` \u2014 that tool reports a failed wait by returning rather than throwing, so the step is recorded either way. That warning names the cause, because only one of them judges the condition: a genuine miss will stop the run at replay, while a wait whose tree source was unreadable, or one that was cancelled, observed nothing and leaves the condition UNKNOWN.
146181
- Returns { message, toolResult, stepCount, recorded, savedTo } on success \u2014 \`message\` is \`Step added to "<name>" flow\` plus any warning about what was recorded (read it; a warning never means the step was skipped). If it fails an error is returned and nothing is recorded.
146377
+ Returns { message, toolResult, stepCount, recorded, savedTo } on success \u2014 \`message\` is \`Step added to "<name>" flow\` plus any warning about what was recorded (read it; a warning never means the step was skipped). If it fails an error is returned and nothing is recorded. Two calls SUCCEED while recording nothing, and omit \`recorded\` to say so: a \`command\` naming a recording tool, and one naming a flow-file directive rather than a tool. Both answer with what to do instead \u2014 usually the call to make (the tool that records that directive, or the recording tool called directly), but \`wait\`, \`long-press\`, \`scroll-to\`, \`snapshot\` and \`when\` have no recording tool, so those name no call and say what to record or add by hand in its place. Either way nothing runs at the device and the take is left untouched \u2014 read \`recorded\`, not the status, to know whether a step was appended.
146182
146378
  If a step was recorded by mistake, remove it from the .yaml after \`flow-finish-recording\` rather than during the recording: against a remote client the in-memory copy is authoritative and every write serializes it over your edit, and in host mode a mid-recording edit renumbers the steps, which costs the finish the cross-tree verdicts anchored to them.`,
146183
146379
  // The recorded tool RUNS here, so this call lasts as long as whatever it
146184
146380
  // wraps, and the three it most often wraps declare this too. Without it the
@@ -146190,7 +146386,19 @@ If a step was recorded by mistake, remove it from the .yaml after \`flow-finish-
146190
146386
  services: () => ({}),
146191
146387
  async execute(_services, params, ctx) {
146192
146388
  const session = await requireRecordingSession(params.project_root, params.name);
146193
- const args = params.args ? JSON.parse(params.args) : {};
146389
+ const nested = Object.hasOwn(NESTED_RECORDER_TOOLS, params.command) ? NESTED_RECORDER_TOOLS[params.command] : void 0;
146390
+ if (nested) return recordNothing(session, nested);
146391
+ let args;
146392
+ try {
146393
+ args = params.args ? JSON.parse(params.args) : {};
146394
+ } catch (err) {
146395
+ if (registry2.getTool(params.command) === void 0) {
146396
+ const hint = directiveCommandHint(params.command);
146397
+ if (hint) return recordNothing(session, hint);
146398
+ }
146399
+ throw err;
146400
+ }
146401
+ const authoredArgs = { ...args };
146194
146402
  if (params.command === RUN_TARGET_COMMAND) await rewriteSiblingFlowPath(session, args);
146195
146403
  const isTap = params.command === "gesture-tap" && params.delayMs === void 0 && typeof args.udid === "string" && typeof args.x === "number" && typeof args.y === "number";
146196
146404
  let captured;
@@ -146200,7 +146408,27 @@ If a step was recorded by mistake, remove it from the .yaml after \`flow-finish-
146200
146408
  y: args.y
146201
146409
  });
146202
146410
  }
146203
- const toolResult = await invokeSubTool(registry2, ctx, params.command, args);
146411
+ let toolResult;
146412
+ try {
146413
+ toolResult = await invokeSubTool(registry2, ctx, params.command, args);
146414
+ } catch (err) {
146415
+ const hint = isToolNotFound(err, params.command) ? directiveCommandHint(params.command) : void 0;
146416
+ if (hint) return recordNothing(session, hint);
146417
+ const reframed = describeNestedParamError(
146418
+ registry2,
146419
+ err,
146420
+ params.command,
146421
+ args,
146422
+ authoredArgs
146423
+ );
146424
+ if (reframed === void 0) throw err;
146425
+ throw new FailureError(reframed, {
146426
+ error_code: FAILURE_CODES.TOOL_INPUT_INVALID,
146427
+ failure_stage: "flow_add_step_nested_params",
146428
+ failure_area: "tool_server",
146429
+ error_kind: "validation"
146430
+ });
146431
+ }
146204
146432
  let waitWarning;
146205
146433
  if (params.command === AWAIT_UI_ELEMENT_TOOL_ID) {
146206
146434
  if (isUnmetUiWaitResult(params.command, toolResult)) {
@@ -148742,8 +148970,10 @@ var zodSchema65 = external_exports.object({
148742
148970
  if (params.name === void 0 === (params.flow_path === void 0)) {
148743
148971
  ctx.addIssue({
148744
148972
  code: external_exports.ZodIssueCode.custom,
148745
- message: "Pass exactly one flow source: name or flow_path.",
148746
- path: ["flow_path"]
148973
+ message: params.name !== void 0 ? "Pass exactly one flow source: name or flow_path." : "Pass exactly one flow source: name or flow_path. flow-execute needs the flow's name in `name` \u2014 it resolves <project_root>/.argent/flows/<name>.yaml.",
148974
+ // The ROOT, not `flow_path`: the rule spans both source fields, and a
148975
+ // path would prefix the message with "`flow_path`:".
148976
+ path: []
148747
148977
  });
148748
148978
  }
148749
148979
  });
@@ -149833,7 +150063,8 @@ async function execLeafStep(state3, step, index, scope) {
149833
150063
  }
149834
150064
  return { ...base, status: "pass", tool: step.name, result, outputHint, args };
149835
150065
  } catch (err) {
149836
- return { ...base, status: "error", tool: step.name, reason: errMsg3(err) };
150066
+ const reframed = describeNestedParamError(registry2, err, step.name, args, step.args ?? {});
150067
+ return { ...base, status: "error", tool: step.name, reason: reframed ?? errMsg3(err) };
149837
150068
  }
149838
150069
  }
149839
150070
  default:
@@ -149981,8 +150212,10 @@ var zodSchema66 = external_exports.object({
149981
150212
  if (params.name === void 0 === (params.flow_path === void 0)) {
149982
150213
  ctx.addIssue({
149983
150214
  code: external_exports.ZodIssueCode.custom,
149984
- message: "Pass exactly one flow source: name or flow_path.",
149985
- path: ["flow_path"]
150215
+ message: params.name !== void 0 ? "Pass exactly one flow source: name or flow_path." : "Pass exactly one flow source: name or flow_path. flow-read-prerequisite needs the flow's name in `name` \u2014 it resolves <project_root>/.argent/flows/<name>.yaml.",
150216
+ // The ROOT, matching flow-execute: the rule spans both source fields,
150217
+ // so it must not be anchored on one of them.
150218
+ path: []
149986
150219
  });
149987
150220
  }
149988
150221
  });
@@ -150014,7 +150247,7 @@ Use when you need to check what app/simulator state is required before executing
150014
150247
  source (name or flow_path) you will pass to flow-execute, so the prerequisite you read is the contract of
150015
150248
  the flow that will actually run.
150016
150249
  Fails if the flow file does not exist.
150017
- Address the flow exactly as you will address it in flow-execute: name or flow_path, one and only one; supplying both or neither is rejected.`,
150250
+ Address the flow exactly as you will address it in flow-execute: name or flow_path, one and only one; supplying both or neither is rejected. The name goes in \`name\`, which resolves <project_root>/.argent/flows/<name>.yaml.`,
150018
150251
  zodSchema: zodSchema66,
150019
150252
  fileInputs: fileInputs4,
150020
150253
  services: () => ({}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.22.1-next.4",
3
+ "version": "0.22.1-next.6",
4
4
  "mcpName": "io.github.software-mansion/argent",
5
5
  "description": "MCP server for iOS Simulator and Android Emulator control",
6
6
  "license": "Apache-2.0",