@swmansion/argent 0.20.1-next.9 → 0.21.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.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/cli-cmds.mjs CHANGED
@@ -1435,6 +1435,7 @@ var FAILURE_CODES = {
1435
1435
  CHROMIUM_ELECTRON_EXITED_BEFORE_READY: "CHROMIUM_ELECTRON_EXITED_BEFORE_READY",
1436
1436
  KEYBOARD_KEY_UNSUPPORTED: "KEYBOARD_KEY_UNSUPPORTED",
1437
1437
  KEYBOARD_CHARACTER_UNSUPPORTED: "KEYBOARD_CHARACTER_UNSUPPORTED",
1438
+ KEYBOARD_TEXT_AND_KEY_COMBINED: "KEYBOARD_TEXT_AND_KEY_COMBINED",
1438
1439
  SECRET_PLACEHOLDER_UNKNOWN: "SECRET_PLACEHOLDER_UNKNOWN",
1439
1440
  SCREENSHOT_DIFF_INPUT_INVALID: "SCREENSHOT_DIFF_INPUT_INVALID",
1440
1441
  BOOT_DEVICE_TARGET_SELECTION_INVALID: "BOOT_DEVICE_TARGET_SELECTION_INVALID",
@@ -1801,6 +1802,10 @@ var FLAG_REGISTRY = [
1801
1802
  name: "tool-server-event-log",
1802
1803
  description: "Write structured tool-server lifecycle events to a JSONL file."
1803
1804
  },
1805
+ {
1806
+ name: "microinteractions",
1807
+ 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."
1808
+ },
1804
1809
  {
1805
1810
  name: "video-watermark",
1806
1811
  description: "Overlay the argent corner watermark on recorded screen videos. On by default; turn it off with `argent disable video-watermark`.",
@@ -8320,7 +8325,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
8320
8325
  var SESSION_ID2 = randomUUID5();
8321
8326
  function readCliVersion() {
8322
8327
  if (true) {
8323
- return "0.20.0";
8328
+ return "0.21.0";
8324
8329
  }
8325
8330
  return "0.0.0";
8326
8331
  }
@@ -16584,6 +16584,7 @@ var FAILURE_CODES = {
16584
16584
  CHROMIUM_ELECTRON_EXITED_BEFORE_READY: "CHROMIUM_ELECTRON_EXITED_BEFORE_READY",
16585
16585
  KEYBOARD_KEY_UNSUPPORTED: "KEYBOARD_KEY_UNSUPPORTED",
16586
16586
  KEYBOARD_CHARACTER_UNSUPPORTED: "KEYBOARD_CHARACTER_UNSUPPORTED",
16587
+ KEYBOARD_TEXT_AND_KEY_COMBINED: "KEYBOARD_TEXT_AND_KEY_COMBINED",
16587
16588
  SECRET_PLACEHOLDER_UNKNOWN: "SECRET_PLACEHOLDER_UNKNOWN",
16588
16589
  SCREENSHOT_DIFF_INPUT_INVALID: "SCREENSHOT_DIFF_INPUT_INVALID",
16589
16590
  BOOT_DEVICE_TARGET_SELECTION_INVALID: "BOOT_DEVICE_TARGET_SELECTION_INVALID",
@@ -17401,7 +17402,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
17401
17402
  var SESSION_ID = randomUUID3();
17402
17403
  function readCliVersion() {
17403
17404
  if (true) {
17404
- return "0.20.0";
17405
+ return "0.21.0";
17405
17406
  }
17406
17407
  return "0.0.0";
17407
17408
  }
@@ -16641,6 +16641,7 @@ var FAILURE_CODES = {
16641
16641
  CHROMIUM_ELECTRON_EXITED_BEFORE_READY: "CHROMIUM_ELECTRON_EXITED_BEFORE_READY",
16642
16642
  KEYBOARD_KEY_UNSUPPORTED: "KEYBOARD_KEY_UNSUPPORTED",
16643
16643
  KEYBOARD_CHARACTER_UNSUPPORTED: "KEYBOARD_CHARACTER_UNSUPPORTED",
16644
+ KEYBOARD_TEXT_AND_KEY_COMBINED: "KEYBOARD_TEXT_AND_KEY_COMBINED",
16644
16645
  SECRET_PLACEHOLDER_UNKNOWN: "SECRET_PLACEHOLDER_UNKNOWN",
16645
16646
  SCREENSHOT_DIFF_INPUT_INVALID: "SCREENSHOT_DIFF_INPUT_INVALID",
16646
16647
  BOOT_DEVICE_TARGET_SELECTION_INVALID: "BOOT_DEVICE_TARGET_SELECTION_INVALID",
@@ -19973,7 +19974,7 @@ async function startMcpServer(options) {
19973
19974
  ...content,
19974
19975
  {
19975
19976
  type: "text",
19976
- text: "Auto-screenshot skipped: the input contains a {{secret:\u2026}} placeholder, and a screenshot of this screen could reveal the typed secret. Submit or navigate away first, then verify the resulting screen as usual."
19977
+ text: "Auto-screenshot skipped: the input contains a {{secret:\u2026}} placeholder, and a screenshot of this screen could reveal the typed secret. The secret is already typed \u2014 do not send the typing step again, or the field will hold two copies of it. Submit or navigate away, then verify the resulting screen as usual. Only this call is covered: the next call is screenshotted normally, and captures the secret if the field is still on screen. To cover the submit as well, put the typing and the submit in ONE `run-sequence` the next time you type a secret."
19977
19978
  }
19978
19979
  ];
19979
19980
  } else if (autoScreenshotOn && udid && shouldAutoScreenshot(params.name)) {
@@ -758,6 +758,7 @@ var init_failure_codes = __esm({
758
758
  CHROMIUM_ELECTRON_EXITED_BEFORE_READY: "CHROMIUM_ELECTRON_EXITED_BEFORE_READY",
759
759
  KEYBOARD_KEY_UNSUPPORTED: "KEYBOARD_KEY_UNSUPPORTED",
760
760
  KEYBOARD_CHARACTER_UNSUPPORTED: "KEYBOARD_CHARACTER_UNSUPPORTED",
761
+ KEYBOARD_TEXT_AND_KEY_COMBINED: "KEYBOARD_TEXT_AND_KEY_COMBINED",
761
762
  SECRET_PLACEHOLDER_UNKNOWN: "SECRET_PLACEHOLDER_UNKNOWN",
762
763
  SCREENSHOT_DIFF_INPUT_INVALID: "SCREENSHOT_DIFF_INPUT_INVALID",
763
764
  BOOT_DEVICE_TARGET_SELECTION_INVALID: "BOOT_DEVICE_TARGET_SELECTION_INVALID",
@@ -85070,7 +85071,7 @@ var require_log = __commonJS({
85070
85071
  if (logLevel === "debug")
85071
85072
  console.log(...messages);
85072
85073
  }
85073
- function warn(logLevel, warning) {
85074
+ function warn2(logLevel, warning) {
85074
85075
  if (logLevel === "debug" || logLevel === "warn") {
85075
85076
  if (typeof node_process.emitWarning === "function")
85076
85077
  node_process.emitWarning(warning);
@@ -85079,7 +85080,7 @@ var require_log = __commonJS({
85079
85080
  }
85080
85081
  }
85081
85082
  exports2.debug = debug;
85082
- exports2.warn = warn;
85083
+ exports2.warn = warn2;
85083
85084
  }
85084
85085
  });
85085
85086
 
@@ -90785,6 +90786,10 @@ var FLAG_REGISTRY = [
90785
90786
  name: "tool-server-event-log",
90786
90787
  description: "Write structured tool-server lifecycle events to a JSONL file."
90787
90788
  },
90789
+ {
90790
+ name: "microinteractions",
90791
+ 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."
90792
+ },
90788
90793
  {
90789
90794
  name: "video-watermark",
90790
90795
  description: "Overlay the argent corner watermark on recorded screen videos. On by default; turn it off with `argent disable video-watermark`.",
@@ -96800,7 +96805,7 @@ var _CI_VENDOR_COUNT_FOR_TEST = vendors_default.length;
96800
96805
  var SESSION_ID = (0, import_node_crypto3.randomUUID)();
96801
96806
  function readCliVersion() {
96802
96807
  if (true) {
96803
- return "0.20.0";
96808
+ return "0.21.0";
96804
96809
  }
96805
96810
  return "0.0.0";
96806
96811
  }
@@ -98188,7 +98193,7 @@ var import_node_path5 = __toESM(require("node:path"));
98188
98193
  var import_semver = __toESM(require_semver2());
98189
98194
 
98190
98195
  // ../tool-server/package.json
98191
- var version3 = "0.20.0";
98196
+ var version3 = "0.21.0";
98192
98197
 
98193
98198
  // ../tool-server/src/utils/update-checker.ts
98194
98199
  var import_update_core = __toESM(require_dist4());
@@ -99604,9 +99609,9 @@ ${debugInfo}`);
99604
99609
  this.#dispose.length = 0;
99605
99610
  if (this.#async.length > 0) {
99606
99611
  try {
99607
- let warn;
99612
+ let warn2;
99608
99613
  const timeout = new Promise((resolve11) => {
99609
- warn = setTimeout(() => {
99614
+ warn2 = setTimeout(() => {
99610
99615
  if (DEV) {
99611
99616
  console.warn("spawn is still running after 5s; continuing anyway", this.#stack);
99612
99617
  }
@@ -99614,8 +99619,8 @@ ${debugInfo}`);
99614
99619
  }, 5e3);
99615
99620
  });
99616
99621
  await Promise.race([Promise.all(this.#async), timeout]);
99617
- if (warn)
99618
- clearTimeout(warn);
99622
+ if (warn2)
99623
+ clearTimeout(warn2);
99619
99624
  this.#async.length = 0;
99620
99625
  } catch (error52) {
99621
99626
  console.error("async effect error", error52);
@@ -119222,6 +119227,18 @@ function readMb(config2, key2) {
119222
119227
  return (m[2] || "M").toLowerCase().startsWith("g") ? n * 1024 : n;
119223
119228
  }
119224
119229
 
119230
+ // ../tool-server/src/utils/no-window-env.ts
119231
+ var TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes"]);
119232
+ function truthyEnv(name) {
119233
+ return TRUTHY.has((process.env[name] ?? "").trim().toLowerCase());
119234
+ }
119235
+ function iosHeadlessFromEnv() {
119236
+ return truthyEnv("ARGENT_SIMULATOR_NO_WINDOW");
119237
+ }
119238
+ function androidHeadlessFromEnv() {
119239
+ return truthyEnv("ARGENT_EMULATOR_NO_WINDOW");
119240
+ }
119241
+
119225
119242
  // ../tool-server/src/utils/vega-vvd.ts
119226
119243
  init_src();
119227
119244
  init_adb();
@@ -119764,14 +119781,6 @@ function selectGpuMode() {
119764
119781
  }
119765
119782
  return process.platform === "linux" ? "swiftshader" : "auto";
119766
119783
  }
119767
- function selectExtraEmulatorArgs() {
119768
- const trimmed = (process.env.ARGENT_EMULATOR_NO_WINDOW ?? "").trim().toLowerCase();
119769
- return ["1", "true", "yes"].includes(trimmed) ? ["-no-window"] : [];
119770
- }
119771
- function iosHeadlessFromEnv() {
119772
- const trimmed = (process.env.ARGENT_SIMULATOR_NO_WINDOW ?? "").trim().toLowerCase();
119773
- return ["1", "true", "yes"].includes(trimmed);
119774
- }
119775
119784
  var BOOT_POLL_INTERVALS_MS = {
119776
119785
  serialByAvd: 1500,
119777
119786
  // findSerialByAvdName: re-scan when >1 new emulator appeared
@@ -120155,7 +120164,7 @@ async function bootAndroidImpl(params) {
120155
120164
  await ensureDep("adb");
120156
120165
  await ensureDep("emulator");
120157
120166
  const gpuMode = selectGpuMode();
120158
- const extraEmulatorArgs = selectExtraEmulatorArgs();
120167
+ const extraEmulatorArgs = androidHeadlessFromEnv() ? ["-no-window"] : [];
120159
120168
  for (const msg of linuxBootDiagnostics(params.avdName) ?? []) {
120160
120169
  console.warn(`[boot-device:linux] ${msg}`);
120161
120170
  }
@@ -122653,6 +122662,7 @@ Fails if the device backend is not reachable \u2014 the simulator-server for iOS
122653
122662
 
122654
122663
  // ../tool-server/src/tools/keyboard/index.ts
122655
122664
  init_zod();
122665
+ init_src();
122656
122666
 
122657
122667
  // ../tool-server/src/utils/secrets.ts
122658
122668
  init_src();
@@ -122813,21 +122823,6 @@ async function typeSimulatorServer(registry2, device, params) {
122813
122823
  }
122814
122824
  keysPressed++;
122815
122825
  };
122816
- let namedKeyCode;
122817
- if (params.key) {
122818
- const lower = params.key.toLowerCase();
122819
- namedKeyCode = Object.hasOwn(NAMED_KEYS, lower) ? NAMED_KEYS[lower] : void 0;
122820
- if (namedKeyCode == null) {
122821
- throw new InvalidToolInputError(
122822
- `Unknown key "${params.key}". Supported: ${Object.keys(NAMED_KEYS).join(", ")}`,
122823
- {
122824
- error_code: FAILURE_CODES.KEYBOARD_KEY_UNSUPPORTED,
122825
- failure_stage: "keyboard_named_key_simulator",
122826
- error_kind: "unsupported"
122827
- }
122828
- );
122829
- }
122830
- }
122831
122826
  if (params.text) {
122832
122827
  for (const char of params.text) {
122833
122828
  const press = charToKeyPress(char);
@@ -122841,7 +122836,19 @@ async function typeSimulatorServer(registry2, device, params) {
122841
122836
  await sleep8(delay2);
122842
122837
  }
122843
122838
  }
122844
- if (namedKeyCode != null) {
122839
+ if (params.key) {
122840
+ const lower = params.key.toLowerCase();
122841
+ const namedKeyCode = Object.hasOwn(NAMED_KEYS, lower) ? NAMED_KEYS[lower] : void 0;
122842
+ if (namedKeyCode == null) {
122843
+ throw new InvalidToolInputError(
122844
+ `Unknown key "${params.key}". Supported: ${Object.keys(NAMED_KEYS).join(", ")}`,
122845
+ {
122846
+ error_code: FAILURE_CODES.KEYBOARD_KEY_UNSUPPORTED,
122847
+ failure_stage: "keyboard_named_key_simulator",
122848
+ error_kind: "unsupported"
122849
+ }
122850
+ );
122851
+ }
122845
122852
  await pressKeyCode(namedKeyCode);
122846
122853
  }
122847
122854
  return { typed: params.text ?? params.key ?? "", keys: keysPressed };
@@ -122890,15 +122897,14 @@ function makeIosRemoteImpl(registry2) {
122890
122897
  init_adb();
122891
122898
  async function typeAndroidPhone(device, params) {
122892
122899
  let keysPressed = 0;
122893
- if (params.text) assertTypeableAndroidText(params.text);
122894
- if (params.key) {
122895
- await injectAndroidNamedKey(device.id, params.key);
122896
- keysPressed++;
122897
- }
122898
122900
  if (params.text) {
122899
122901
  await injectAndroidText(device.id, params.text);
122900
122902
  keysPressed += params.text.length;
122901
122903
  }
122904
+ if (params.key) {
122905
+ await injectAndroidNamedKey(device.id, params.key);
122906
+ keysPressed++;
122907
+ }
122902
122908
  return { typed: params.text ?? params.key ?? "", keys: keysPressed };
122903
122909
  }
122904
122910
  function makeAndroidImpl(registry2) {
@@ -122987,21 +122993,6 @@ var sleep9 = (ms) => new Promise((r) => setTimeout(r, ms));
122987
122993
  async function runChromium(api, params) {
122988
122994
  const delay2 = params.delayMs ?? 50;
122989
122995
  let keysPressed = 0;
122990
- let named;
122991
- if (params.key) {
122992
- const lower = params.key.toLowerCase();
122993
- named = Object.hasOwn(CHROMIUM_NAMED_KEYS, lower) ? CHROMIUM_NAMED_KEYS[lower] : void 0;
122994
- if (!named) {
122995
- throw new InvalidToolInputError(
122996
- `Unknown key "${params.key}". Supported: ${Object.keys(CHROMIUM_NAMED_KEYS).join(", ")}`,
122997
- {
122998
- error_code: FAILURE_CODES.KEYBOARD_KEY_UNSUPPORTED,
122999
- failure_stage: "keyboard_named_key_chromium",
123000
- error_kind: "unsupported"
123001
- }
123002
- );
123003
- }
123004
- }
123005
122996
  if (params.text) {
123006
122997
  for (const char of params.text) {
123007
122998
  const desc = charToChromiumKey(char);
@@ -123029,7 +123020,19 @@ async function runChromium(api, params) {
123029
123020
  await sleep9(delay2);
123030
123021
  }
123031
123022
  }
123032
- if (named) {
123023
+ if (params.key) {
123024
+ const lower = params.key.toLowerCase();
123025
+ const named = Object.hasOwn(CHROMIUM_NAMED_KEYS, lower) ? CHROMIUM_NAMED_KEYS[lower] : void 0;
123026
+ if (!named) {
123027
+ throw new InvalidToolInputError(
123028
+ `Unknown key "${params.key}". Supported: ${Object.keys(CHROMIUM_NAMED_KEYS).join(", ")}`,
123029
+ {
123030
+ error_code: FAILURE_CODES.KEYBOARD_KEY_UNSUPPORTED,
123031
+ failure_stage: "keyboard_named_key_chromium",
123032
+ error_kind: "unsupported"
123033
+ }
123034
+ );
123035
+ }
123033
123036
  await api.dispatchKeyEvent({
123034
123037
  type: "keyDown",
123035
123038
  key: named.key,
@@ -123128,7 +123131,7 @@ async function injectViaInputd(subcommands) {
123128
123131
  async function injectVegaButtons(buttons) {
123129
123132
  await injectViaInputd(remoteButtonsToKeycodes(buttons).map((code) => `button_press ${code}`));
123130
123133
  }
123131
- function resolveVegaNamedKeycode(name) {
123134
+ async function injectVegaNamedKey(name) {
123132
123135
  const lower = name.toLowerCase();
123133
123136
  const code = Object.hasOwn(NAMED_KEYCODES, lower) ? NAMED_KEYCODES[lower] : void 0;
123134
123137
  if (!code) {
@@ -123141,10 +123144,7 @@ function resolveVegaNamedKeycode(name) {
123141
123144
  }
123142
123145
  );
123143
123146
  }
123144
- return code;
123145
- }
123146
- async function injectVegaNamedKey(name) {
123147
- await injectViaInputd([`button_press ${resolveVegaNamedKeycode(name)}`]);
123147
+ await injectViaInputd([`button_press ${code}`]);
123148
123148
  }
123149
123149
  async function injectVegaText(text) {
123150
123150
  if (/[\n\r]/.test(text)) {
@@ -123160,7 +123160,6 @@ async function injectVegaText(text) {
123160
123160
  // ../tool-server/src/tools/keyboard/platforms/vega.ts
123161
123161
  async function runVega2(params) {
123162
123162
  let keysPressed = 0;
123163
- if (params.key) resolveVegaNamedKeycode(params.key);
123164
123163
  if (params.text) {
123165
123164
  await injectVegaText(params.text);
123166
123165
  keysPressed += [...params.text].length;
@@ -123182,10 +123181,10 @@ var zodSchema24 = external_exports.object({
123182
123181
  "Target device id from `list-devices` (iOS UDID, Android serial, Vega serial, or Chromium id)."
123183
123182
  ),
123184
123183
  text: external_exports.string().optional().describe(
123185
- 'Text to type character by character. Handles uppercase and common punctuation. To type a credential without its plaintext ever entering your context, use a secret placeholder: `{{secret:<NAME>}}` \u2014 e.g. text: "{{secret:APP_PASSWORD}}". The value is resolved on the machine running the tool-server, from the first source that defines the name: the `ARGENT_SECRET_<NAME>` environment variable, `.argent/secrets.env` in the project, the project\'s `.env.local` / `.env` (only their `ARGENT_SECRET_`-prefixed keys), then `~/.argent/secrets.env`. Nothing else on the host is reachable. Placeholders can be embedded in longer text and are never echoed back resolved. If the secret you need is not set, the failure lists the available names and every source it looked in \u2014 ask the user to add it to one of them (a secrets file applies immediately; an env var needs a restart), NEVER ask the user to paste the secret value into the conversation.'
123184
+ 'Text to type character by character. Cannot be combined with `key` in one call \u2014 one call per action; to type and then press a key, put two `keyboard` steps in one `run-sequence`. Handles uppercase and common punctuation. To type a credential without its plaintext ever entering your context, use a secret placeholder: `{{secret:<NAME>}}` \u2014 e.g. text: "{{secret:APP_PASSWORD}}". The value is resolved on the machine running the tool-server, from the first source that defines the name: the `ARGENT_SECRET_<NAME>` environment variable, `.argent/secrets.env` in the project, the project\'s `.env.local` / `.env` (only their `ARGENT_SECRET_`-prefixed keys), then `~/.argent/secrets.env`. Nothing else on the host is reachable. Placeholders can be embedded in longer text and are never echoed back resolved. If the secret you need is not set, the failure lists the available names and every source it looked in \u2014 ask the user to add it to one of them (a secrets file applies immediately; an env var needs a restart), NEVER ask the user to paste the secret value into the conversation.'
123186
123185
  ),
123187
123186
  key: external_exports.string().optional().describe(
123188
- "Named key to press: enter, escape, backspace, tab, space, arrow-up, arrow-down, arrow-left, arrow-right, f1\u2013f12. When combined with `text`, the key is pressed AFTER the text is typed (so text + enter types and submits). Not supported on TV targets \u2014 move focus with `tv-remote` (up/down/left/right) instead."
123187
+ "Named key to press: enter, escape, backspace, tab, space, arrow-up, arrow-down, arrow-left, arrow-right, f1\u2013f12. Cannot be combined with `text` in one call \u2014 one call per action; to type and then press a key, put two `keyboard` steps in one `run-sequence`. Not supported on TV targets \u2014 move focus with `tv-remote` (up/down/left/right) instead."
123189
123188
  ),
123190
123189
  delayMs: external_exports.number().optional().describe(
123191
123190
  "Delay in ms between key presses (default 50). Ignored on Android phones/tablets (typed via `adb input text`, which has no per-key cadence), on Vega (text/keys injected in a single shot), and on TV targets (Apple TV / Android TV type the whole string at the daemon's own cadence)."
@@ -123214,25 +123213,29 @@ function createKeyboardTool(registry2) {
123214
123213
  // Treat both text and key as sensitive. `key` is an unrestricted string at
123215
123214
  // this boundary, so a value must not reach the event log before execution
123216
123215
  // validates whether it is a supported named key.
123216
+ //
123217
+ // `startedMsg` still words a text+key request because it renders BEFORE
123218
+ // `execute` rejects the combination — and likewise words `{ key: "" }` as
123219
+ // a key press, which `execute` also rejects. `completedMsg` runs only
123220
+ // after a call that succeeded, so it sees neither. Each formatter
123221
+ // therefore has to word a different set of shapes, and the empty
123222
+ // request — neither parameter, a documented no-op — reaches both.
123217
123223
  startedMsg: ({ params }) => {
123218
123224
  if (params.text === void 0) return "Pressing a key";
123219
123225
  if (params.key === void 0) return "Entering text";
123220
123226
  return "Entering text and pressing a key";
123221
123227
  },
123222
- completedMsg: ({ params }) => {
123223
- if (params.text === void 0) return "Pressed a key";
123224
- if (params.key === void 0) return "Entered text";
123225
- return "Entered text and pressed a key";
123226
- },
123228
+ completedMsg: ({ params }) => params.text === void 0 ? "Pressed a key" : "Entered text",
123227
123229
  failedMsg: ({ failureSignal: failureSignal2 }) => `Failed to use keyboard: ${failureSignal2.error_code}`
123228
123230
  },
123229
123231
  description: `Type text or press special keys on the device (iOS simulator, Android emulator or device, Chromium app, Vega Virtual Device, or Apple TV / Android TV) using keyboard events.
123230
123232
  Use when you need to enter text or trigger a named key such as enter, escape, or arrow keys. On Vega and Apple TV / Android TV, prefer the remote tools for D-pad navigation; use keyboard to type into a focused text field (e.g. a search or login box).
123231
- Returns { typed: string, keys: number }. Fails if an unsupported key name is provided or the device's input backend is not reachable.
123232
- - text: types a string (supports uppercase, digits, common punctuation). To type a credential, use \`{{secret:<NAME>}}\` \u2014 resolved server-side from the \`ARGENT_SECRET_<NAME>\` env var or an argent secrets file (\`.argent/secrets.env\` in the project, \`~/.argent/secrets.env\`, or an \`ARGENT_SECRET_\`-prefixed key in the project's \`.env\`/\`.env.local\`), so the plaintext never enters agent context; the result echoes the placeholder, not the value, and the after-typing auto-screenshot is skipped.
123233
+ Returns { typed: string, keys: number }. Fails if text and key are both given in one call (rejected before anything is typed), if an unsupported key name is provided, or if the device's input backend is not reachable.
123234
+ A failure is not rolled back. An unsupported key name is always rejected before anything is sent. Un-typeable text is not: the iOS simulator and Chromium reject it mid-string and leave the characters before it in the field (Android, Vega and TV targets check the whole string up front). A transport failure partway also leaves the text already sent. On a retry, read the field's actual contents \u2014 do not assume it is unchanged.
123235
+ - text: types a string (supports uppercase, digits, common punctuation). To type a credential, use \`{{secret:<NAME>}}\` \u2014 resolved server-side from the \`ARGENT_SECRET_<NAME>\` env var or an argent secrets file (\`.argent/secrets.env\` in the project, \`~/.argent/secrets.env\`, or an \`ARGENT_SECRET_\`-prefixed key in the project's \`.env\`/\`.env.local\`), so the plaintext never enters agent context; the result echoes the placeholder, not the value, and the after-typing auto-screenshot is skipped. To submit after typing a secret, put both steps in ONE \`run-sequence\` \u2014 that keeps the skip covering the Enter, which a second bare \`keyboard\` call would not.
123233
123236
  - key: presses a single named key (enter, escape, backspace, tab, arrow-up/down/left/right, f1\u2013f12) \u2014 NOT supported on TV targets; move focus with \`tv-remote\` instead.
123234
123237
  On a TV target (runtimeKind 'tv') only \`text\` applies \u2014 focus a text field first (with \`tv-remote\`), then type into it (injected HID keyboard on Apple TV, \`adb input text\` on Android TV).
123235
- Provide text, key, or both \u2014 when both are given, the text is typed first and the key is pressed after it (text + key:"enter" types and submits).`,
123238
+ One call does one action: pass text OR key, never both. To type and then press a key, send two \`keyboard\` steps in one \`run-sequence\` \u2014 { text: "hello" } then { key: "enter" } \u2014 which also keeps it to a single round-trip.`,
123236
123239
  zodSchema: zodSchema24,
123237
123240
  capability: capability16,
123238
123241
  searchHint: "type text keyboard input named key enter escape arrow tv vega fire tv search field hid leanback",
@@ -123241,6 +123244,46 @@ Provide text, key, or both \u2014 when both are given, the text is typed first a
123241
123244
  // async and a tvOS udid must never resolve simulator-server.
123242
123245
  services: () => ({}),
123243
123246
  execute: async (services, params, options) => {
123247
+ if (params.text !== void 0 && params.key !== void 0) {
123248
+ throw new InvalidToolInputError(
123249
+ // Says what did NOT happen, so the caller retries instead of first
123250
+ // inspecting the field — and spells the retry out with a literal
123251
+ // example rather than an ellipsis the Android backend can't type.
123252
+ //
123253
+ // The TV caveat is carried statically rather than by probing the
123254
+ // target: this guard runs above the dispatch precisely so a combined
123255
+ // request reaches no device, and distinguishing a TV kind is an async
123256
+ // probe. Without it the prescribed `{ key: "enter" }` is a retry that
123257
+ // cannot succeed on a TV, where `key` is rejected outright
123258
+ // (platforms/tv.ts) — which is the diagnosis this guard would
123259
+ // otherwise pre-empt.
123260
+ 'keyboard takes `text` or `key`, not both \u2014 nothing was typed. To type and then press a key, send two `keyboard` steps in one `run-sequence`: { text: "hello" } followed by { key: "enter" }. On a TV target (Apple TV / Android TV) `key` is not supported at all \u2014 type with `text` and move focus with `tv-remote` (up/down/left/right/select).' + // The one-`run-sequence` form and two bare calls are NOT equivalent
123261
+ // once the text carries a placeholder, and this message is where an
123262
+ // agent converts a combined secret call — the tool description's
123263
+ // caveat is read long before that moment, if at all. The check is
123264
+ // syntactic (the same `.includes` flow-utils.ts uses), so the guard
123265
+ // still resolves nothing.
123266
+ (params.text.includes(SECRET_PLACEHOLDER_MARKER) ? " This `text` carries a `" + SECRET_PLACEHOLDER_MARKER + '...}}` placeholder, so keep both steps in that ONE `run-sequence` rather than splitting them into two bare calls: the auto-screenshot skip is decided per tool call from the whole request, and a separate { key: "enter" } call carries no placeholder \u2014 its screenshot is taken after the key lands and can capture the still-visible secret.' : ""),
123267
+ {
123268
+ error_code: FAILURE_CODES.KEYBOARD_TEXT_AND_KEY_COMBINED,
123269
+ failure_stage: "keyboard_text_and_key_combined"
123270
+ }
123271
+ );
123272
+ }
123273
+ if (params.key === "") {
123274
+ throw new InvalidToolInputError(
123275
+ // Names the omission as the alternative, because a caller that sent an
123276
+ // empty string usually built the value from something absent.
123277
+ "`key` is an empty string, which names no key \u2014 nothing was pressed. Pass a named key (enter, escape, backspace, tab, space, arrow-up, arrow-down, arrow-left, arrow-right, f1\u2013f12), or omit `key` if there is nothing to press.",
123278
+ {
123279
+ // The same code an unknown name gets, because that is what this is:
123280
+ // one telemetry bucket for every unusable `key` value.
123281
+ error_code: FAILURE_CODES.KEYBOARD_KEY_UNSUPPORTED,
123282
+ failure_stage: "keyboard_named_key_empty",
123283
+ error_kind: "unsupported"
123284
+ }
123285
+ );
123286
+ }
123244
123287
  if (params.text === void 0) return dispatch(services, params, options);
123245
123288
  const { text, secrets } = resolveSecretPlaceholders(params.text);
123246
123289
  if (secrets.length === 0) return dispatch(services, params, options);
@@ -123300,10 +123343,211 @@ var shakeZodSchema = external_exports.object({
123300
123343
  });
123301
123344
 
123302
123345
  // ../tool-server/src/tools/shake/platforms/ios.ts
123303
- var import_node_child_process26 = require("node:child_process");
123346
+ var import_node_child_process27 = require("node:child_process");
123304
123347
  var import_node_util20 = require("node:util");
123305
123348
  init_src();
123306
- var execFileAsync20 = (0, import_node_util20.promisify)(import_node_child_process26.execFile);
123349
+
123350
+ // ../tool-server/src/utils/window-shake.ts
123351
+ var import_node_child_process26 = require("node:child_process");
123352
+ var MICROINTERACTIONS_FLAG = "microinteractions";
123353
+ var AMPLITUDE = 22;
123354
+ var STEPS = 60;
123355
+ var OSASCRIPT_TIMEOUT_MS = 5e3;
123356
+ var ORIGIN_MARKER = "ARGENT_WINDOW_ORIGIN:";
123357
+ function warn(detail) {
123358
+ process.stderr.write(`[shake:window] skipped the window animation: ${detail}
123359
+ `);
123360
+ }
123361
+ function asStringLiteral(value) {
123362
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
123363
+ }
123364
+ function titleMatchLookup(procNamesExpr, needles) {
123365
+ return `
123366
+ set win to missing value
123367
+ set needles to {${needles.map(asStringLiteral).join(", ")}}
123368
+ set procNames to ${procNamesExpr}
123369
+ repeat with procRef in procNames
123370
+ set procName to contents of procRef
123371
+ set winTitles to {}
123372
+ try
123373
+ tell process procName to set winTitles to name of every window
123374
+ end try
123375
+ repeat with idx from 1 to (count of winTitles)
123376
+ set winTitle to item idx of winTitles
123377
+ -- A GUI app can carry an unnamed helper window alongside the real one.
123378
+ if winTitle is not missing value then
123379
+ repeat with needle in needles
123380
+ if winTitle contains (contents of needle) then
123381
+ set win to window idx of process procName
123382
+ exit repeat
123383
+ end if
123384
+ end repeat
123385
+ end if
123386
+ if win is not missing value then exit repeat
123387
+ end repeat
123388
+ if win is not missing value then exit repeat
123389
+ end repeat`;
123390
+ }
123391
+ function windowLookup(needles) {
123392
+ if (needles.length === 0) {
123393
+ return `
123394
+ set win to missing value
123395
+ repeat with procRef in {"Simulator", "Device Hub"}
123396
+ -- A repeat variable is a reference into the list and "process <ref>"
123397
+ -- will not coerce one, so without "contents of" the lookup misses.
123398
+ set procName to contents of procRef
123399
+ if exists (process procName) then
123400
+ tell process procName
123401
+ if (count of windows) > 0 then set win to window 1
123402
+ end tell
123403
+ end if
123404
+ if win is not missing value then exit repeat
123405
+ end repeat`;
123406
+ }
123407
+ return titleMatchLookup(
123408
+ `name of every process whose name is "Simulator" or name is "Device Hub"`,
123409
+ needles
123410
+ );
123411
+ }
123412
+ var WOBBLE_OFFSETS = Array.from(
123413
+ { length: STEPS + 1 },
123414
+ (_, i) => {
123415
+ const t = i / STEPS;
123416
+ const decay = Math.exp(-4 * t);
123417
+ const phase = 2 * Math.PI * 3.5 * t;
123418
+ return [
123419
+ Math.round(AMPLITUDE * decay * Math.sin(phase)),
123420
+ Math.round(AMPLITUDE / 3 * decay * Math.sin(2 * phase))
123421
+ ];
123422
+ }
123423
+ );
123424
+ var OFFSETS_LITERAL = WOBBLE_OFFSETS.map(([dx, dy]) => `{${dx}, ${dy}}`).join(", ");
123425
+ function animationScript(needles) {
123426
+ return `on run
123427
+ tell application "System Events"${windowLookup(needles)}
123428
+ if win is missing value then error "no matching window"
123429
+ set origin to position of win
123430
+ set ox to item 1 of origin
123431
+ set oy to item 2 of origin
123432
+ log "${ORIGIN_MARKER}" & ox & "," & oy
123433
+
123434
+ set offsets to {${OFFSETS_LITERAL}}
123435
+ repeat with pair in offsets
123436
+ set position of win to {ox + (item 1 of pair), oy + (item 2 of pair)}
123437
+ end repeat
123438
+
123439
+ set position of win to {ox, oy}
123440
+ delay 0.05
123441
+ set position of win to {ox, oy}
123442
+ end tell
123443
+ end run
123444
+ `;
123445
+ }
123446
+ function restoreScript(needles, ox, oy) {
123447
+ return `on run
123448
+ tell application "System Events"${windowLookup(needles)}
123449
+ if win is missing value then error "no matching window"
123450
+ set position of win to {${ox}, ${oy}}
123451
+ end tell
123452
+ end run
123453
+ `;
123454
+ }
123455
+ function runOsascript(script) {
123456
+ return new Promise((resolve11, reject) => {
123457
+ const child = (0, import_node_child_process26.execFile)(
123458
+ "/usr/bin/osascript",
123459
+ ["-"],
123460
+ { timeout: OSASCRIPT_TIMEOUT_MS },
123461
+ (err, _stdout, stderr) => {
123462
+ if (err) {
123463
+ const raw = String(stderr ?? "");
123464
+ const detail = raw.split("\n").filter((line) => !line.includes(ORIGIN_MARKER)).join("\n").trim() || err.message || "osascript failed";
123465
+ const failure = new Error(detail);
123466
+ failure.wasKilled = Boolean(
123467
+ err.killed || err.signal
123468
+ );
123469
+ failure.rawStderr = raw;
123470
+ reject(failure);
123471
+ return;
123472
+ }
123473
+ resolve11();
123474
+ }
123475
+ );
123476
+ child.on("error", (err) => reject(err));
123477
+ child.stdin?.on("error", () => {
123478
+ });
123479
+ child.stdin?.end(script);
123480
+ });
123481
+ }
123482
+ async function restoreOriginAfterKill(needles, rawStderr) {
123483
+ const match = rawStderr.match(new RegExp(`${ORIGIN_MARKER}\\s*(-?\\d+)\\s*,\\s*(-?\\d+)`));
123484
+ if (!match) return;
123485
+ try {
123486
+ await runOsascript(restoreScript(needles, Number(match[1]), Number(match[2])));
123487
+ } catch {
123488
+ }
123489
+ }
123490
+ async function prepareHostWindowShake(target) {
123491
+ const inert = { begin: () => {
123492
+ }, settle: () => Promise.resolve() };
123493
+ try {
123494
+ if (!isFeatureEnabled(MICROINTERACTIONS_FLAG)) return inert;
123495
+ if (process.platform !== "darwin") return inert;
123496
+ if (iosHeadlessFromEnv()) return inert;
123497
+ let deviceSet = null;
123498
+ try {
123499
+ deviceSet = await deviceSetForUdid(target.udid);
123500
+ } catch {
123501
+ }
123502
+ if (deviceSet !== null) return inert;
123503
+ const needles = target.name ? [target.name] : [];
123504
+ const script = animationScript(needles);
123505
+ let inFlight = null;
123506
+ let dead = false;
123507
+ let warned2 = false;
123508
+ const warnOnce = (detail) => {
123509
+ if (warned2) return;
123510
+ warned2 = true;
123511
+ warn(detail);
123512
+ };
123513
+ return {
123514
+ begin() {
123515
+ if (dead || inFlight !== null) return;
123516
+ let run2;
123517
+ try {
123518
+ run2 = runOsascript(script);
123519
+ } catch (err) {
123520
+ dead = true;
123521
+ warnOnce(err instanceof Error ? err.message : String(err));
123522
+ return;
123523
+ }
123524
+ inFlight = run2.then(
123525
+ () => {
123526
+ inFlight = null;
123527
+ },
123528
+ async (err) => {
123529
+ dead = true;
123530
+ const failure = err;
123531
+ if (failure.wasKilled && typeof failure.rawStderr === "string") {
123532
+ await restoreOriginAfterKill(needles, failure.rawStderr);
123533
+ }
123534
+ warnOnce(err instanceof Error ? err.message : String(err));
123535
+ inFlight = null;
123536
+ }
123537
+ );
123538
+ },
123539
+ settle() {
123540
+ return inFlight ?? Promise.resolve();
123541
+ }
123542
+ };
123543
+ } catch (err) {
123544
+ warn(err instanceof Error ? err.message : String(err));
123545
+ return inert;
123546
+ }
123547
+ }
123548
+
123549
+ // ../tool-server/src/tools/shake/platforms/ios.ts
123550
+ var execFileAsync20 = (0, import_node_util20.promisify)(import_node_child_process27.execFile);
123307
123551
  var SHAKE_NOTIFICATION = "com.apple.UIKit.SimulatorShake";
123308
123552
  var SHAKE_INTERVAL_MS = 400;
123309
123553
  var sleep10 = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -123339,8 +123583,10 @@ var iosImpl4 = {
123339
123583
  const count2 = params.count ?? 1;
123340
123584
  if (await isTvOsSimulator(udid)) rejectTv("shake", device);
123341
123585
  const args = await simctlArgsForUdid(udid, ["spawn", udid, ...NOTIFYUTIL_ARGV]);
123586
+ const shaker = await prepareHostWindowShake({ kind: "ios", udid, name: device.name });
123342
123587
  for (let i = 0; i < count2; i++) {
123343
123588
  if (i > 0) await sleep10(SHAKE_INTERVAL_MS);
123589
+ shaker.begin();
123344
123590
  try {
123345
123591
  await execFileAsync20("xcrun", args, { timeout: 15e3 });
123346
123592
  } catch (err) {
@@ -123348,6 +123594,7 @@ var iosImpl4 = {
123348
123594
  throw shakeFailure(udid, error52.message, error52);
123349
123595
  }
123350
123596
  }
123597
+ await shaker.settle();
123351
123598
  return { shaken: true, count: count2 };
123352
123599
  }
123353
123600
  };
@@ -130065,7 +130312,8 @@ function createRunSequenceTool(registry2) {
130065
130312
  Use when you need sequential actions and do NOT need to observe the screen between them
130066
130313
  (e.g. scrolling multiple times, typing then pressing enter, rotating back and forth).
130067
130314
  Returns { completed, total, steps } with per-step results. Fails if an unrecognised tool name is used in a step (error returned at that step, execution stops).
130068
- No screenshot is captured automatically \u2014 call screenshot separately after the sequence if needed.
130315
+ One screenshot is captured automatically after the whole sequence (not per step) \u2014 call screenshot separately only for a baseline BEFORE it, or to observe an intermediate step.
130316
+ That single capture is also why a secret belongs in this call rather than in two bare ones: the skip is decided from the whole request, so a \`{{secret:...}}\` in any step suppresses the capture that would otherwise follow the submit.
130069
130317
 
130070
130318
  ONLY use this when every step is known in advance. If any step depends on the
130071
130319
  result of a previous one (e.g. tapping a menu item that only appears after
@@ -130081,7 +130329,7 @@ Allowed tools and their args (udid is auto-injected, do NOT include it in args):
130081
130329
  gesture-pinch: { centerX: number, centerY: number, startDistance: number, endDistance: number, endCenterX?: number, endCenterY?: number, angle?: number, durationMs?: number } [ios/android]
130082
130330
  gesture-rotate: { centerX: number, centerY: number, radius?: number, radiusX?: number, radiusY?: number, startAngle: number, endAngle: number, durationMs?: number } [ios/android]
130083
130331
  button: { button: "home"|"back"|"power"|"volumeUp"|"volumeDown"|"appSwitch"|"actionButton" } [ios/android]
130084
- keyboard: { text?: string, key?: string, delayMs?: number } (key pressed after text; TV: text only) [ios/android/chromium/vega/tv]
130332
+ keyboard: { text?: string, key?: string, delayMs?: number } (text OR key per step, never both; TV: text only) [ios/android/chromium/vega/tv]
130085
130333
  text supports {{secret:<NAME>}} placeholders, resolved server-side from ARGENT_SECRET_<NAME> env vars or an argent secrets file \u2014 credentials never enter agent context
130086
130334
  rotate: { orientation: "Portrait"|"LandscapeLeft"|"LandscapeRight"|"PortraitUpsideDown" } [ios/android]
130087
130335
  shake: { count?: number } [ios sim/android emu]
@@ -130096,9 +130344,10 @@ Example \u2014 scroll down three times (use gesture-scroll with positive deltaY
130096
130344
  { "tool": "gesture-swipe", "args": { "fromX": 0.5, "fromY": 0.7, "toX": 0.5, "toY": 0.3 } }
130097
130345
  ]}
130098
130346
 
130099
- Example \u2014 type text and submit (one step: the key is pressed after the text is typed):
130347
+ Example \u2014 type text and submit (two keyboard steps; one call cannot carry both):
130100
130348
  { "udid": "<UDID>", "steps": [
130101
- { "tool": "keyboard", "args": { "text": "hello world", "key": "enter" } }
130349
+ { "tool": "keyboard", "args": { "text": "hello world" } },
130350
+ { "tool": "keyboard", "args": { "key": "enter" } }
130102
130351
  ]}
130103
130352
 
130104
130353
  Example \u2014 TV: move focus right twice then activate (one tv-remote step with a path is cheaper):
@@ -142733,7 +142982,7 @@ Returns { stopped } - the URNs of the services that were actually live and got s
142733
142982
 
142734
142983
  // ../tool-server/src/tools/simulator/stop-metro.ts
142735
142984
  init_zod();
142736
- var import_node_child_process27 = require("node:child_process");
142985
+ var import_node_child_process28 = require("node:child_process");
142737
142986
  function parseNetstatListeningPids(netstatOutput, port) {
142738
142987
  const pids = /* @__PURE__ */ new Set();
142739
142988
  for (const line of netstatOutput.split(/\r?\n/)) {
@@ -142749,7 +142998,7 @@ function parseNetstatListeningPids(netstatOutput, port) {
142749
142998
  }
142750
142999
  function listeningPids(port) {
142751
143000
  if (process.platform === "win32") {
142752
- const output2 = (0, import_node_child_process27.execFileSync)("netstat", ["-ano"], {
143001
+ const output2 = (0, import_node_child_process28.execFileSync)("netstat", ["-ano"], {
142753
143002
  encoding: "utf-8",
142754
143003
  timeout: 5e3,
142755
143004
  // `netstat -ano` dumps every socket on the host; a busy box easily
@@ -142759,7 +143008,7 @@ function listeningPids(port) {
142759
143008
  });
142760
143009
  return parseNetstatListeningPids(output2, port);
142761
143010
  }
142762
- const output = (0, import_node_child_process27.execFileSync)("lsof", ["-ti", `tcp:${port}`, "-sTCP:LISTEN"], {
143011
+ const output = (0, import_node_child_process28.execFileSync)("lsof", ["-ti", `tcp:${port}`, "-sTCP:LISTEN"], {
142763
143012
  encoding: "utf-8",
142764
143013
  timeout: 5e3
142765
143014
  }).trim();
@@ -148760,10 +149009,10 @@ async function runSnapshot(env, opts) {
148760
149009
  }
148761
149010
 
148762
149011
  // ../tool-server/src/utils/status-bar.ts
148763
- var import_node_child_process28 = require("node:child_process");
149012
+ var import_node_child_process29 = require("node:child_process");
148764
149013
  var import_node_util21 = require("node:util");
148765
149014
  init_adb();
148766
- var execFileAsync23 = (0, import_node_util21.promisify)(import_node_child_process28.execFile);
149015
+ var execFileAsync23 = (0, import_node_util21.promisify)(import_node_child_process29.execFile);
148767
149016
  var DEMO_BROADCAST = "am broadcast -a com.android.systemui.demo";
148768
149017
  async function pinStatusBar(device) {
148769
149018
  try {
@@ -150115,7 +150364,7 @@ init_zod();
150115
150364
  // ../tool-server/src/utils/workspace-reader.ts
150116
150365
  var import_promises12 = require("node:fs/promises");
150117
150366
  var import_node_path21 = require("node:path");
150118
- var import_node_child_process29 = require("node:child_process");
150367
+ var import_node_child_process30 = require("node:child_process");
150119
150368
  var import_dotenv2 = __toESM(require_main());
150120
150369
  async function exists(path40) {
150121
150370
  try {
@@ -150159,7 +150408,7 @@ async function listDir(path40) {
150159
150408
  var COMMAND_TIMEOUT_MS = 3e3;
150160
150409
  function runVersionCommand(cmd, args, cwd) {
150161
150410
  return new Promise((resolve11) => {
150162
- const child = (0, import_node_child_process29.execFile)(cmd, args, { cwd, timeout: COMMAND_TIMEOUT_MS }, (err, stdout) => {
150411
+ const child = (0, import_node_child_process30.execFile)(cmd, args, { cwd, timeout: COMMAND_TIMEOUT_MS }, (err, stdout) => {
150163
150412
  if (err) {
150164
150413
  resolve11(null);
150165
150414
  return;
@@ -150448,7 +150697,7 @@ Fails if the workspacePath is not an absolute path or the directory cannot be ac
150448
150697
  };
150449
150698
 
150450
150699
  // ../tool-server/src/tools/system/update-argent.ts
150451
- var import_node_child_process30 = require("node:child_process");
150700
+ var import_node_child_process31 = require("node:child_process");
150452
150701
  var fs51 = __toESM(require("node:fs"));
150453
150702
  var path36 = __toESM(require("node:path"));
150454
150703
  init_zod();
@@ -150583,7 +150832,7 @@ var updateArgentTool = {
150583
150832
  if (spawnRoot) updateArgs.push("--project-root", spawnRoot);
150584
150833
  const cmd = cliEntry ? process.execPath : "argent";
150585
150834
  const args = cliEntry ? [cliEntry, ...updateArgs] : updateArgs;
150586
- const child = (0, import_node_child_process30.spawn)(cmd, args, {
150835
+ const child = (0, import_node_child_process31.spawn)(cmd, args, {
150587
150836
  detached: true,
150588
150837
  stdio: "ignore",
150589
150838
  env: { ...process.env, ARGENT_UPDATE_TRIGGER: "mcp_update" }
@@ -150723,6 +150972,14 @@ async function resolveOutputDir(params, options) {
150723
150972
  if (params.outputDir && (probe3 === void 0 || probe3.presentOnHost)) {
150724
150973
  return params.outputDir;
150725
150974
  }
150975
+ if (params.outputDir) {
150976
+ try {
150977
+ await import_promises13.default.mkdir(params.outputDir);
150978
+ return params.outputDir;
150979
+ } catch (err) {
150980
+ if (err.code === "EEXIST") return params.outputDir;
150981
+ }
150982
+ }
150726
150983
  const dir = import_path10.default.join(
150727
150984
  import_node_os13.default.tmpdir(),
150728
150985
  "argent-screenshot-diff",
@@ -151501,9 +151758,9 @@ function probeArgentToolServer(host, port, timeoutMs = 500) {
151501
151758
 
151502
151759
  // ../tool-server/src/utils/simulator-watcher.ts
151503
151760
  var fs53 = __toESM(require("node:fs"));
151504
- var import_node_child_process31 = require("node:child_process");
151761
+ var import_node_child_process32 = require("node:child_process");
151505
151762
  var import_node_util22 = require("node:util");
151506
- var execFileAsync24 = (0, import_node_util22.promisify)(import_node_child_process31.execFile);
151763
+ var execFileAsync24 = (0, import_node_util22.promisify)(import_node_child_process32.execFile);
151507
151764
  var POLL_INTERVAL_MS2 = 1e4;
151508
151765
  async function getBootedUdidsInSet(deviceSet) {
151509
151766
  const { stdout } = await execFileAsync24("xcrun", [
@@ -151579,7 +151836,7 @@ function startSimulatorWatcher(registry2) {
151579
151836
  }
151580
151837
 
151581
151838
  // ../tool-server/src/utils/preview-window.ts
151582
- var import_node_child_process32 = require("node:child_process");
151839
+ var import_node_child_process33 = require("node:child_process");
151583
151840
  var fs54 = __toESM(require("node:fs"));
151584
151841
  var os14 = __toESM(require("node:os"));
151585
151842
  var path38 = __toESM(require("node:path"));
@@ -151611,10 +151868,10 @@ function ensureLensAppBundle(electronBin) {
151611
151868
  fs54.copyFileSync(path38.join(realContents, "Info.plist"), plist);
151612
151869
  const setPlist = (entry, value) => {
151613
151870
  try {
151614
- (0, import_node_child_process32.execFileSync)("/usr/libexec/PlistBuddy", ["-c", `Set :${entry} ${value}`, plist]);
151871
+ (0, import_node_child_process33.execFileSync)("/usr/libexec/PlistBuddy", ["-c", `Set :${entry} ${value}`, plist]);
151615
151872
  } catch {
151616
151873
  try {
151617
- (0, import_node_child_process32.execFileSync)("/usr/libexec/PlistBuddy", ["-c", `Add :${entry} string ${value}`, plist]);
151874
+ (0, import_node_child_process33.execFileSync)("/usr/libexec/PlistBuddy", ["-c", `Add :${entry} string ${value}`, plist]);
151618
151875
  } catch {
151619
151876
  }
151620
151877
  }
@@ -151673,7 +151930,7 @@ function createPreviewWindowManager(opts = {}) {
151673
151930
  const wrapperBin = ensureLensAppBundle(electronBin);
151674
151931
  const launchBin = wrapperBin ?? electronBin;
151675
151932
  const launchArgs = wrapperBin ? ["--no-sandbox", mainScript] : [mainScript];
151676
- const next = (0, import_node_child_process32.spawn)(launchBin, launchArgs, {
151933
+ const next = (0, import_node_child_process33.spawn)(launchBin, launchArgs, {
151677
151934
  // Strip ELECTRON_RUN_AS_NODE so the child boots as a GUI Electron app, not
151678
151935
  // a bare Node runtime (see electronGuiChildEnv). An Electron-based MCP
151679
151936
  // host puts it in our env, and inheriting it makes main.cjs crash at
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.20.1-next.9",
3
+ "version": "0.21.0",
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",
@@ -110,7 +110,7 @@ An Android app that needs a non-launcher activity has no `launch:` form. Record
110
110
 
111
111
  In a `scroll-to` map, put the selector under `target:`. The map supports `up`, `down`, `left`, and `right` directions. The default is `down`; set it explicitly to reach a target above the viewport or along a horizontal carousel. If the target is already visible, the step is a safe no-op. `tap`, `type`, and `long-press` do not auto-scroll. Add `scroll-to` when the target can be off-screen. Use `within` for a nested scroller.
112
112
 
113
- `type` presses Enter unless `submit: false`. A polished focus tap plus keyboard call usually needs `submit: false`. Store external values as `{{secret:NAME}}`. The runner uses the first source that defines the name: environment `ARGENT_SECRET_NAME`; project `.argent/secrets.env`; project `.env.local`, then `.env`; then `~/.argent/secrets.env`. The two `secrets.env` files accept the bare `NAME`, but the shared dotenv files expose only `ARGENT_SECRET_`-prefixed keys, so a bare `NAME=…` in `.env` or `.env.local` stays unresolved. The runner redacts every resolved value, so do not use a placeholder for content a report must show.
113
+ `type` presses Enter in a second `keyboard` call unless `submit: false`. A polished focus tap plus one text-only `keyboard` call usually needs `submit: false`. Store external values as `{{secret:NAME}}`. The runner uses the first source that defines the name: environment `ARGENT_SECRET_NAME`; project `.argent/secrets.env`; project `.env.local`, then `.env`; then `~/.argent/secrets.env`. The two `secrets.env` files accept the bare `NAME`, but the shared dotenv files expose only `ARGENT_SECRET_`-prefixed keys, so a bare `NAME=…` in `.env` or `.env.local` stays unresolved. The runner redacts every resolved value, so do not use a placeholder for content a report must show.
114
114
 
115
115
  A **selector-less gesture** — a coordinate `tap`/`long-press`, or a `pinch`/`rotate` with no `on:` — resolves no frame, so a tree source it cannot read does not fail it. It settles best effort, dispatches anyway, and the step **passes carrying a warning** that quotes the source's own error. That green says the gesture was sent, not that it landed: one aimed at a moving element can miss it entirely. Restore the tree source, usually by relaunching the app so the instrumentation loads. Accept the warning only where the app serves no tree at all, and put an explicit `wait:` before a gesture that follows a transition. The first such gesture proves the outage and later ones spend that verdict without paying the settle window again. A tree read that comes back, or a relaunch, retires that verdict — which only makes the next gesture pay a fresh window, and it warns again if the source is still down.
116
116
 
@@ -111,7 +111,7 @@ Never tap the on-screen keyboard through the recorder. Some platforms expose it
111
111
 
112
112
  ### Typing
113
113
 
114
- Record the focus tap, then record `keyboard`. Verify the complete value with `describe` or an app validation marker.
114
+ Record the focus tap, then record `keyboard` with `text`. A `keyboard` call carries `text` or `key`, never both. To submit, record a second `keyboard` step with `key: "enter"`. Verify the complete value with `describe` or an app validation marker.
115
115
 
116
116
  **Never `describe` or `screenshot` a non-secure field you just filled from `{{secret:…}}`.** Only a password field is redacted; a plain text input hands the resolved value back into your context, and an API key or token typed into one is the ordinary case. Submit or navigate away first, then verify the resulting screen.
117
117
 
@@ -144,16 +144,16 @@ Stop immediately. Restore the last valid screen with direct MCP calls, not `flow
144
144
 
145
145
  Call `flow-finish-recording`, then read the saved YAML. Apply only meaning-preserving conversions:
146
146
 
147
- | Recorded form | Finished form |
148
- | ---------------------------- | ------------------------------------------------------------------ |
149
- | focus tap + `tool: keyboard` | `type:` |
150
- | keyboard ending in Enter | submitted `type:` without Enter in its text |
151
- | `tool: await-ui-element` | `await:` or `assert:` |
152
- | element-seeking movement | `scroll-to:` |
153
- | coordinate tap or long-press | strict selector after the fallback gate |
154
- | `tool: gesture-pinch` | selector-based `pinch:` with `scale = endDistance / startDistance` |
155
- | `tool: gesture-rotate` | selector-based `rotate:` with `by = endAngle - startAngle` |
156
- | sibling `tool: flow-execute` | recorder-captured `run:` |
147
+ | Recorded form | Finished form |
148
+ | ----------------------------------------- | ------------------------------------------------------------------ |
149
+ | focus tap + `tool: keyboard` | `type:` |
150
+ | text `keyboard` + `key: enter` `keyboard` | submitted `type:` without Enter in its text |
151
+ | `tool: await-ui-element` | `await:` or `assert:` |
152
+ | element-seeking movement | `scroll-to:` |
153
+ | coordinate tap or long-press | strict selector after the fallback gate |
154
+ | `tool: gesture-pinch` | selector-based `pinch:` with `scale = endDistance / startDistance` |
155
+ | `tool: gesture-rotate` | selector-based `rotate:` with `by = endAngle - startAngle` |
156
+ | sibling `tool: flow-execute` | recorder-captured `run:` |
157
157
 
158
158
  Only these unrecorded insertions are allowed, at states observed live:
159
159
 
@@ -59,7 +59,7 @@ Apple system apps cannot load the instrumentation, and nothing in the launch pat
59
59
 
60
60
  - Raw `tool: await-ui-element` accessibility checks.
61
61
  - Point taps or long-presses derived from `describe`, each named by an echo.
62
- - A point focus tap plus raw keyboard with `delayMs: 500`.
62
+ - A point focus tap plus a raw text-only `keyboard` with `delayMs: 500`, and a second raw `keyboard` with `key: "enter"` to submit.
63
63
  - Raw swipes with `settle: true` because `scroll-to` needs the missing flow tree. Momentum-free scrolling keeps later coordinate taps valid.
64
64
 
65
65
  Every point tap or long-press in such a flow passes **carrying a warning**. The app loads no instrumentation, so every tree read fails and each [selector-less gesture](flow-yaml.md#directives) dispatches unsettled. Nothing here repairs it. Accept the warnings, read each green as "the gesture was sent, not that it landed", and put an explicit `wait:` or a raw `tool: await-ui-element` before a gesture that follows a transition. Raw `tool:` steps never take that settle, so they never warn.
@@ -72,7 +72,7 @@ Common schemes: `messages://`, `settings://`, `maps://?q=<query>`, `tel://<numbe
72
72
  | Rotation | `gesture-rotate` | Two-finger rotation with auto-interpolation |
73
73
  | Custom gesture | `gesture-custom` | Arbitrary touch sequences, optional interpolation |
74
74
  | Hardware key | `button` | Home, back, power, volume, appSwitch, actionButton |
75
- | Type text | `keyboard` | Every platform. Supports Enter, Escape, arrows (not on TV) |
75
+ | Type text | `keyboard` | Every platform. Text or one named key per call, never both |
76
76
  | Rotate device | `rotate` | Orientation changes |
77
77
  | Shake device | `shake` | Shake handlers (sim/emu only), Undo-typing prompt, RN dev menu |
78
78
  | Wait for UI | `await-ui-element` | Block until an element is visible/hidden/exists/contains text |
@@ -168,15 +168,17 @@ Values: `home`, `back`, `power`, `volumeUp`, `volumeDown`, `appSwitch`, `actionB
168
168
  ### keyboard — Type text or press special keys
169
169
 
170
170
  ```json
171
- { "udid": "<UDID>", "text": "search query", "key": "enter" }
171
+ { "udid": "<UDID>", "text": "search query" }
172
172
  ```
173
173
 
174
+ One call does one action. `text` and `key` are mutually exclusive, and a call that carries both is rejected with nothing typed. To type and then submit, send two `keyboard` steps in one `run-sequence` (§ 8) — `{ "text": "search query" }`, then `{ "key": "enter" }`. Two separate calls do the same work, but cost an extra round-trip.
175
+
174
176
  Special keys: `enter`, `escape`, `backspace`, `tab`, `space`, `arrow-up`, `arrow-down`, `arrow-left`, `arrow-right`, `f1`–`f12`. Optional: `"delayMs": 100` between keystrokes (default 50ms) — applies to the iOS simulator and Chromium; it is ignored on Android phones/tablets (typed via `adb input text`, no per-key cadence), on Vega, and on TV targets.
175
177
 
176
178
  **Typing secrets.** To enter a credential without its plaintext ever entering your context, transcript, or logs, use a secret placeholder in `text` (works in `keyboard`, `paste`, `run-sequence` keyboard steps, and flow `type` steps):
177
179
 
178
180
  ```json
179
- { "udid": "<UDID>", "text": "{{secret:APP_PASSWORD}}", "key": "enter" }
181
+ { "udid": "<UDID>", "text": "{{secret:APP_PASSWORD}}" }
180
182
  ```
181
183
 
182
184
  The placeholder is resolved on the machine running the tool-server, from the first of these that defines the name:
@@ -191,7 +193,7 @@ The placeholder is resolved on the machine running the tool-server, from the fir
191
193
  Rules:
192
194
 
193
195
  - The result echoes the placeholder, never the value. An unknown name fails with the list of available secret _names_ and every source it looked in, with paths — read that list before asking the user anything.
194
- - The auto-screenshot after the call is skipped so the typed value cannot re-enter your context as pixels. Do **not** `describe` or `screenshot` a non-secure field you just filled with a secret — submit or navigate away first, then verify the resulting screen.
196
+ - The auto-screenshot after the call is skipped so the typed value cannot re-enter your context as pixels. Do **not** `describe` or `screenshot` a non-secure field you just filled with a secret — submit or navigate away first, then verify the resulting screen. To submit, put the text step and the Enter step in **one `run-sequence`**. The skip covers a whole batch that contains the placeholder, but a second bare `keyboard` call gets its own screenshot of the filled field.
195
197
  - Nothing outside those sources is reachable; never ask the user to paste a secret value into the conversation. Ask them to put it in a secrets file instead — a file edit applies to the next call, while an exported env var only reaches a tool-server started afterwards.
196
198
  - The project sources are found by walking up from the tool-server's working directory. If a project file is not being picked up, the failure's source list shows the paths actually consulted; `~/.argent/secrets.env` needs no project and always applies.
197
199
 
@@ -316,7 +318,7 @@ Scroll down three times:
316
318
  }
317
319
  ```
318
320
 
319
- Type into a focused field and submit:
321
+ Type into a focused field and submit. This is the only way to mix text and a key, because one `keyboard` call cannot carry both:
320
322
 
321
323
  ```json
322
324
  {
@@ -21,7 +21,7 @@ description: Control and inspect TV apps via argent — Apple TV (tvOS), Android
21
21
 
22
22
  - `describe {udid}` — focus view: the focused / `[selected]` element + focusable elements with labels and normalized frames. The discovery tool — call before and after navigating. Empty tree → see the per-platform notes.
23
23
  - `tv-remote {udid, button}` — D-pad / remote. `button` is one key **or a whole path** (run in one call). Keys: `up`/`down`/`left`/`right`, `select`, `back`, `menu`, `home`, `playPause`, plus media keys `rewind`/`fastForward`/`next`/`previous`/`volumeUp`/`volumeDown`/`mute`. Single: `{button:"down"}`; repeat: `{button:"down", repeat:3}`; path: `{button:["up","right","select"]}`.
24
- - `keyboard {udid, text}` — type into the focused field (focus it with `tv-remote` first). Named `key` presses (e.g. `{key:"enter"}`) work on Vega; on Apple TV / Android TV move focus with `tv-remote` instead.
24
+ - `keyboard {udid, text}` — type into the focused field (focus it with `tv-remote` first). One call carries `text` or `key`, never both — to type and then press a key, send two `keyboard` steps in one `run-sequence`. Named `key` presses (e.g. `{key:"enter"}`) work on Vega; on Apple TV / Android TV move focus with `tv-remote` instead.
25
25
  - `launch-app` / `restart-app` / `reinstall-app {udid, bundleId}` — `bundleId` from the app manifest. Vega `reinstall-app` takes `appPath` = a `.vpkg`.
26
26
  - `screenshot {udid, scale?}` — Apple TV via `xcrun simctl io` (downscaled); Android TV / Vega host-side via `adb` / `screencap`.
27
27