@swmansion/argent 0.15.1-next.5 → 0.15.1-next.7
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
|
@@ -6324,6 +6324,7 @@ var FAILURE_CODES = {
|
|
|
6324
6324
|
CHROMIUM_ELECTRON_EXITED_BEFORE_READY: "CHROMIUM_ELECTRON_EXITED_BEFORE_READY",
|
|
6325
6325
|
KEYBOARD_KEY_UNSUPPORTED: "KEYBOARD_KEY_UNSUPPORTED",
|
|
6326
6326
|
KEYBOARD_CHARACTER_UNSUPPORTED: "KEYBOARD_CHARACTER_UNSUPPORTED",
|
|
6327
|
+
SECRET_PLACEHOLDER_UNKNOWN: "SECRET_PLACEHOLDER_UNKNOWN",
|
|
6327
6328
|
SCREENSHOT_DIFF_INPUT_INVALID: "SCREENSHOT_DIFF_INPUT_INVALID",
|
|
6328
6329
|
BOOT_DEVICE_TARGET_SELECTION_INVALID: "BOOT_DEVICE_TARGET_SELECTION_INVALID",
|
|
6329
6330
|
BOOT_IOS_UNSUPPORTED_HOST: "BOOT_IOS_UNSUPPORTED_HOST",
|
package/dist/installer.mjs
CHANGED
|
@@ -16231,6 +16231,7 @@ var FAILURE_CODES = {
|
|
|
16231
16231
|
CHROMIUM_ELECTRON_EXITED_BEFORE_READY: "CHROMIUM_ELECTRON_EXITED_BEFORE_READY",
|
|
16232
16232
|
KEYBOARD_KEY_UNSUPPORTED: "KEYBOARD_KEY_UNSUPPORTED",
|
|
16233
16233
|
KEYBOARD_CHARACTER_UNSUPPORTED: "KEYBOARD_CHARACTER_UNSUPPORTED",
|
|
16234
|
+
SECRET_PLACEHOLDER_UNKNOWN: "SECRET_PLACEHOLDER_UNKNOWN",
|
|
16234
16235
|
SCREENSHOT_DIFF_INPUT_INVALID: "SCREENSHOT_DIFF_INPUT_INVALID",
|
|
16235
16236
|
BOOT_DEVICE_TARGET_SELECTION_INVALID: "BOOT_DEVICE_TARGET_SELECTION_INVALID",
|
|
16236
16237
|
BOOT_IOS_UNSUPPORTED_HOST: "BOOT_IOS_UNSUPPORTED_HOST",
|
package/dist/mcp-server.mjs
CHANGED
|
@@ -17750,6 +17750,7 @@ var FAILURE_CODES = {
|
|
|
17750
17750
|
CHROMIUM_ELECTRON_EXITED_BEFORE_READY: "CHROMIUM_ELECTRON_EXITED_BEFORE_READY",
|
|
17751
17751
|
KEYBOARD_KEY_UNSUPPORTED: "KEYBOARD_KEY_UNSUPPORTED",
|
|
17752
17752
|
KEYBOARD_CHARACTER_UNSUPPORTED: "KEYBOARD_CHARACTER_UNSUPPORTED",
|
|
17753
|
+
SECRET_PLACEHOLDER_UNKNOWN: "SECRET_PLACEHOLDER_UNKNOWN",
|
|
17753
17754
|
SCREENSHOT_DIFF_INPUT_INVALID: "SCREENSHOT_DIFF_INPUT_INVALID",
|
|
17754
17755
|
BOOT_DEVICE_TARGET_SELECTION_INVALID: "BOOT_DEVICE_TARGET_SELECTION_INVALID",
|
|
17755
17756
|
BOOT_IOS_UNSUPPORTED_HOST: "BOOT_IOS_UNSUPPORTED_HOST",
|
|
@@ -18981,6 +18982,14 @@ var DEFAULT_DELAY_MS = 1400;
|
|
|
18981
18982
|
function autoScreenshotEnabled(options) {
|
|
18982
18983
|
return !isFlagEnabled("disable-auto-screenshot", options);
|
|
18983
18984
|
}
|
|
18985
|
+
var SECRET_PLACEHOLDER_MARKER = "{{secret:";
|
|
18986
|
+
function containsSecretPlaceholder(args) {
|
|
18987
|
+
try {
|
|
18988
|
+
return JSON.stringify(args)?.includes(SECRET_PLACEHOLDER_MARKER) ?? false;
|
|
18989
|
+
} catch {
|
|
18990
|
+
return true;
|
|
18991
|
+
}
|
|
18992
|
+
}
|
|
18984
18993
|
function getUdidFromArgs(args) {
|
|
18985
18994
|
if (args && typeof args === "object" && "udid" in args && typeof args.udid === "string") {
|
|
18986
18995
|
return args.udid;
|
|
@@ -19209,7 +19218,15 @@ async function startMcpServer(options) {
|
|
|
19209
19218
|
content = await toMcpContent(result, outputHint, ctx, params.arguments);
|
|
19210
19219
|
}
|
|
19211
19220
|
const udid = getUdidFromArgs(params.arguments);
|
|
19212
|
-
if (autoScreenshotOn && udid && shouldAutoScreenshot(params.name)) {
|
|
19221
|
+
if (autoScreenshotOn && udid && shouldAutoScreenshot(params.name) && containsSecretPlaceholder(params.arguments)) {
|
|
19222
|
+
content = [
|
|
19223
|
+
...content,
|
|
19224
|
+
{
|
|
19225
|
+
type: "text",
|
|
19226
|
+
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."
|
|
19227
|
+
}
|
|
19228
|
+
];
|
|
19229
|
+
} else if (autoScreenshotOn && udid && shouldAutoScreenshot(params.name)) {
|
|
19213
19230
|
const maxWaitMs = getAutoScreenshotDelayMs(params.name);
|
|
19214
19231
|
if (maxWaitMs > 0) {
|
|
19215
19232
|
try {
|
package/dist/tool-server.cjs
CHANGED
|
@@ -404,6 +404,7 @@ var init_failure_codes = __esm({
|
|
|
404
404
|
CHROMIUM_ELECTRON_EXITED_BEFORE_READY: "CHROMIUM_ELECTRON_EXITED_BEFORE_READY",
|
|
405
405
|
KEYBOARD_KEY_UNSUPPORTED: "KEYBOARD_KEY_UNSUPPORTED",
|
|
406
406
|
KEYBOARD_CHARACTER_UNSUPPORTED: "KEYBOARD_CHARACTER_UNSUPPORTED",
|
|
407
|
+
SECRET_PLACEHOLDER_UNKNOWN: "SECRET_PLACEHOLDER_UNKNOWN",
|
|
407
408
|
SCREENSHOT_DIFF_INPUT_INVALID: "SCREENSHOT_DIFF_INPUT_INVALID",
|
|
408
409
|
BOOT_DEVICE_TARGET_SELECTION_INVALID: "BOOT_DEVICE_TARGET_SELECTION_INVALID",
|
|
409
410
|
BOOT_IOS_UNSUPPORTED_HOST: "BOOT_IOS_UNSUPPORTED_HOST",
|
|
@@ -126687,6 +126688,54 @@ Fails if the device backend is not reachable \u2014 the simulator-server for iOS
|
|
|
126687
126688
|
// ../tool-server/src/tools/keyboard/index.ts
|
|
126688
126689
|
init_zod();
|
|
126689
126690
|
|
|
126691
|
+
// ../tool-server/src/utils/secrets.ts
|
|
126692
|
+
init_src();
|
|
126693
|
+
var SECRET_ENV_PREFIX = "ARGENT_SECRET_";
|
|
126694
|
+
var SECRET_PLACEHOLDER_MARKER = "{{secret:";
|
|
126695
|
+
var PLACEHOLDER_RE = /\{\{secret:([A-Za-z_][A-Za-z0-9_]*)\}\}/g;
|
|
126696
|
+
function availableSecretNames(env = process.env) {
|
|
126697
|
+
return Object.keys(env).filter((k) => k.startsWith(SECRET_ENV_PREFIX) && env[k] !== void 0).map((k) => k.slice(SECRET_ENV_PREFIX.length)).sort();
|
|
126698
|
+
}
|
|
126699
|
+
var REDUNDANT_PREFIX_RE = /^argent_secret_/i;
|
|
126700
|
+
function resolveSecretPlaceholders(text, env = process.env) {
|
|
126701
|
+
const secrets = [];
|
|
126702
|
+
const resolved = text.replace(PLACEHOLDER_RE, (placeholder, rawName) => {
|
|
126703
|
+
let name = rawName;
|
|
126704
|
+
let value = env[SECRET_ENV_PREFIX + name];
|
|
126705
|
+
if (value === void 0 && REDUNDANT_PREFIX_RE.test(name)) {
|
|
126706
|
+
name = name.replace(REDUNDANT_PREFIX_RE, "");
|
|
126707
|
+
value = env[SECRET_ENV_PREFIX + name];
|
|
126708
|
+
}
|
|
126709
|
+
if (value === void 0) {
|
|
126710
|
+
const names = availableSecretNames(env);
|
|
126711
|
+
throw new InvalidToolInputError(
|
|
126712
|
+
`Unknown secret "${rawName}" \u2014 no ${SECRET_ENV_PREFIX}${name} environment variable is set on the machine running the tool-server. Available secrets: ${names.length ? names.join(", ") : "(none)"}. To make it available, ask the user to export ${SECRET_ENV_PREFIX}${name} in the tool-server's environment \u2014 never ask the user for the secret value itself.`,
|
|
126713
|
+
{
|
|
126714
|
+
error_code: FAILURE_CODES.SECRET_PLACEHOLDER_UNKNOWN,
|
|
126715
|
+
failure_stage: "secret_placeholder_resolution",
|
|
126716
|
+
error_kind: "validation"
|
|
126717
|
+
}
|
|
126718
|
+
);
|
|
126719
|
+
}
|
|
126720
|
+
if (!secrets.some((s) => s.name === name)) secrets.push({ name, value });
|
|
126721
|
+
return value;
|
|
126722
|
+
});
|
|
126723
|
+
return { text: resolved, secrets };
|
|
126724
|
+
}
|
|
126725
|
+
function redactSecretsFromError(err, secrets) {
|
|
126726
|
+
const scrub = (s) => secrets.reduce(
|
|
126727
|
+
(acc, { name, value }) => value ? acc.split(value).join(`${SECRET_PLACEHOLDER_MARKER}${name}}}`) : acc,
|
|
126728
|
+
s
|
|
126729
|
+
);
|
|
126730
|
+
if (err instanceof Error) {
|
|
126731
|
+
err.message = scrub(err.message);
|
|
126732
|
+
if (err.stack) err.stack = scrub(err.stack);
|
|
126733
|
+
return err;
|
|
126734
|
+
}
|
|
126735
|
+
if (typeof err === "string") return scrub(err);
|
|
126736
|
+
return err;
|
|
126737
|
+
}
|
|
126738
|
+
|
|
126690
126739
|
// ../tool-server/src/tools/keyboard/simulator-server-keys.ts
|
|
126691
126740
|
init_src();
|
|
126692
126741
|
|
|
@@ -127165,7 +127214,9 @@ var zodSchema24 = external_exports.object({
|
|
|
127165
127214
|
udid: external_exports.string().describe(
|
|
127166
127215
|
"Target device id from `list-devices` (iOS UDID, Android serial, Vega serial, or Chromium id)."
|
|
127167
127216
|
),
|
|
127168
|
-
text: external_exports.string().optional().describe(
|
|
127217
|
+
text: external_exports.string().optional().describe(
|
|
127218
|
+
'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>}}` types the value of the `ARGENT_SECRET_<NAME>` environment variable set on the machine running the tool-server \u2014 e.g. text: "{{secret:APP_PASSWORD}}" types the value of `ARGENT_SECRET_APP_PASSWORD`. Only env vars with the `ARGENT_SECRET_` prefix are resolvable. Placeholders can be embedded in longer text and are never echoed back resolved. If the secret you need is not set, ask the user to export it as `ARGENT_SECRET_<NAME>` and restart the session \u2014 NEVER ask the user to paste the secret value into the conversation.'
|
|
127219
|
+
),
|
|
127169
127220
|
key: external_exports.string().optional().describe(
|
|
127170
127221
|
"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."
|
|
127171
127222
|
),
|
|
@@ -127181,12 +127232,21 @@ var capability16 = {
|
|
|
127181
127232
|
vega: { vvd: true }
|
|
127182
127233
|
};
|
|
127183
127234
|
function createKeyboardTool(registry2) {
|
|
127235
|
+
const dispatch = dispatchByPlatform({
|
|
127236
|
+
toolId: "keyboard",
|
|
127237
|
+
capability: capability16,
|
|
127238
|
+
ios: makeIosImpl3(registry2),
|
|
127239
|
+
iosRemote: makeIosRemoteImpl(registry2),
|
|
127240
|
+
android: makeAndroidImpl(registry2),
|
|
127241
|
+
chromium: makeChromiumImpl(registry2),
|
|
127242
|
+
vega: vegaImpl4
|
|
127243
|
+
});
|
|
127184
127244
|
return {
|
|
127185
127245
|
id: "keyboard",
|
|
127186
127246
|
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.
|
|
127187
127247
|
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).
|
|
127188
127248
|
Returns { typed: string, keys: number }. Fails if an unsupported key name is provided or the device's input backend is not reachable.
|
|
127189
|
-
- text: types a string (supports uppercase, digits, common punctuation)
|
|
127249
|
+
- 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 (prefix mandatory; \`{{secret:APP_PASSWORD}}\` \u2194 \`ARGENT_SECRET_APP_PASSWORD\`), so the plaintext never enters agent context; the result echoes the placeholder, not the value, and the after-typing auto-screenshot is skipped.
|
|
127190
127250
|
- 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.
|
|
127191
127251
|
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).
|
|
127192
127252
|
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).`,
|
|
@@ -127197,15 +127257,17 @@ Provide text, key, or both \u2014 when both are given, the text is typed first a
|
|
|
127197
127257
|
// simulator-server, CDP, or Vega adb), since distinguishing a TV target is
|
|
127198
127258
|
// async and a tvOS udid must never resolve simulator-server.
|
|
127199
127259
|
services: () => ({}),
|
|
127200
|
-
execute:
|
|
127201
|
-
|
|
127202
|
-
|
|
127203
|
-
|
|
127204
|
-
|
|
127205
|
-
|
|
127206
|
-
|
|
127207
|
-
|
|
127208
|
-
|
|
127260
|
+
execute: async (services, params, options) => {
|
|
127261
|
+
if (params.text === void 0) return dispatch(services, params, options);
|
|
127262
|
+
const { text, secrets } = resolveSecretPlaceholders(params.text);
|
|
127263
|
+
if (secrets.length === 0) return dispatch(services, params, options);
|
|
127264
|
+
try {
|
|
127265
|
+
const result = await dispatch(services, { ...params, text }, options);
|
|
127266
|
+
return { ...result, typed: params.text };
|
|
127267
|
+
} catch (err) {
|
|
127268
|
+
throw redactSecretsFromError(err, secrets);
|
|
127269
|
+
}
|
|
127270
|
+
}
|
|
127209
127271
|
};
|
|
127210
127272
|
}
|
|
127211
127273
|
|
|
@@ -133168,7 +133230,7 @@ async function describeVega(_serial) {
|
|
|
133168
133230
|
|
|
133169
133231
|
// ../tool-server/src/utils/ui-tree-match.ts
|
|
133170
133232
|
init_zod();
|
|
133171
|
-
var
|
|
133233
|
+
var selectorFieldsSchema = external_exports.object({
|
|
133172
133234
|
text: external_exports.string().min(1).optional().describe("Case-insensitive substring of the element's visible label or value."),
|
|
133173
133235
|
identifier: external_exports.string().min(1).optional().describe(
|
|
133174
133236
|
"The element's identifier (accessibilityIdentifier / resource-id / testid), matched case-insensitively as the exact identifier or the unqualified resource-id name ('submit' matches 'com.example.app:id/submit')."
|
|
@@ -133176,9 +133238,13 @@ var selectorSchema = external_exports.object({
|
|
|
133176
133238
|
role: external_exports.string().min(1).optional().describe(
|
|
133177
133239
|
"Case-insensitive substring of the element's role (e.g. AXButton, button, TextView)."
|
|
133178
133240
|
)
|
|
133179
|
-
}).
|
|
133180
|
-
|
|
133181
|
-
|
|
133241
|
+
}).strict();
|
|
133242
|
+
var selectorSchema = selectorFieldsSchema.refine(
|
|
133243
|
+
(s) => Boolean(s.text || s.identifier || s.role),
|
|
133244
|
+
{
|
|
133245
|
+
message: "selector needs at least one of text, identifier, or role"
|
|
133246
|
+
}
|
|
133247
|
+
);
|
|
133182
133248
|
function nodeText(node) {
|
|
133183
133249
|
return [node.label, node.value].filter(Boolean).join(" ");
|
|
133184
133250
|
}
|
|
@@ -133195,15 +133261,32 @@ function identifierMatches(actual, needle) {
|
|
|
133195
133261
|
if (!actual) return false;
|
|
133196
133262
|
return equalsCI(actual, needle) || actual.toLowerCase().endsWith(`:id/${needle.toLowerCase()}`);
|
|
133197
133263
|
}
|
|
133264
|
+
var uiTreeMatchInternals = {
|
|
133265
|
+
createRegExp(pattern) {
|
|
133266
|
+
return new RegExp(pattern);
|
|
133267
|
+
}
|
|
133268
|
+
};
|
|
133269
|
+
function regexMatchesNonEmpty(regex, actual) {
|
|
133270
|
+
if (!actual) return false;
|
|
133271
|
+
return regex.test(actual);
|
|
133272
|
+
}
|
|
133198
133273
|
function textMatches(actual, expected, mode) {
|
|
133274
|
+
if (mode === "matches") {
|
|
133275
|
+
return regexMatchesNonEmpty(uiTreeMatchInternals.createRegExp(expected), actual);
|
|
133276
|
+
}
|
|
133199
133277
|
return mode === "equals" ? equalsCI(actual, expected) : includesCI(actual, expected);
|
|
133200
133278
|
}
|
|
133201
|
-
function
|
|
133279
|
+
function matchNodeWithRegex(node, selector, textRegex) {
|
|
133202
133280
|
if (selector.text !== void 0) {
|
|
133203
133281
|
if (!includesCI(node.label, selector.text) && !includesCI(node.value, selector.text)) {
|
|
133204
133282
|
return false;
|
|
133205
133283
|
}
|
|
133206
133284
|
}
|
|
133285
|
+
if (textRegex !== void 0) {
|
|
133286
|
+
if (!regexMatchesNonEmpty(textRegex, node.label) && !regexMatchesNonEmpty(textRegex, node.value)) {
|
|
133287
|
+
return false;
|
|
133288
|
+
}
|
|
133289
|
+
}
|
|
133207
133290
|
if (selector.identifier !== void 0 && !identifierMatches(node.identifier, selector.identifier)) {
|
|
133208
133291
|
return false;
|
|
133209
133292
|
}
|
|
@@ -133212,13 +133295,17 @@ function matchNode(node, selector) {
|
|
|
133212
133295
|
}
|
|
133213
133296
|
return true;
|
|
133214
133297
|
}
|
|
133215
|
-
function
|
|
133216
|
-
|
|
133217
|
-
|
|
133298
|
+
function selectorTextRegex(selector) {
|
|
133299
|
+
return selector.textMatches === void 0 ? void 0 : uiTreeMatchInternals.createRegExp(selector.textMatches);
|
|
133300
|
+
}
|
|
133301
|
+
function collectMatches(node, selector, textRegex, acc) {
|
|
133302
|
+
if (matchNodeWithRegex(node, selector, textRegex)) acc.push(node);
|
|
133303
|
+
for (const child of node.children) collectMatches(child, selector, textRegex, acc);
|
|
133218
133304
|
}
|
|
133219
133305
|
function findAll(root2, selector) {
|
|
133220
133306
|
const acc = [];
|
|
133221
|
-
|
|
133307
|
+
const textRegex = selectorTextRegex(selector);
|
|
133308
|
+
for (const child of root2.children) collectMatches(child, selector, textRegex, acc);
|
|
133222
133309
|
return acc;
|
|
133223
133310
|
}
|
|
133224
133311
|
function isVisible(node) {
|
|
@@ -133282,21 +133369,29 @@ function nodeAtPoint(root2, point) {
|
|
|
133282
133369
|
for (const child of root2.children) walk(child);
|
|
133283
133370
|
return best;
|
|
133284
133371
|
}
|
|
133285
|
-
function
|
|
133372
|
+
function fullConsumptionRegex(selector) {
|
|
133373
|
+
return selector.textMatches === void 0 ? void 0 : uiTreeMatchInternals.createRegExp(`^(?:${selector.textMatches})$`);
|
|
133374
|
+
}
|
|
133375
|
+
function exactFieldCount(node, selector, fullTextRegex) {
|
|
133286
133376
|
let count = 0;
|
|
133287
133377
|
if (selector.text !== void 0 && (equalsCI(node.label, selector.text) || equalsCI(node.value, selector.text))) {
|
|
133288
133378
|
count++;
|
|
133289
133379
|
}
|
|
133380
|
+
if (fullTextRegex !== void 0 && (regexMatchesNonEmpty(fullTextRegex, node.label) || regexMatchesNonEmpty(fullTextRegex, node.value))) {
|
|
133381
|
+
count++;
|
|
133382
|
+
}
|
|
133290
133383
|
if (selector.identifier !== void 0 && equalsCI(node.identifier, selector.identifier)) count++;
|
|
133291
133384
|
if (selector.role !== void 0 && equalsCI(node.role, selector.role)) count++;
|
|
133292
133385
|
return count;
|
|
133293
133386
|
}
|
|
133294
133387
|
function selectorToFrame(root2, selector) {
|
|
133295
133388
|
const visible = findAll(root2, selector).filter(isVisible);
|
|
133389
|
+
if (visible.length === 0) return void 0;
|
|
133390
|
+
const fullTextRegex = fullConsumptionRegex(selector);
|
|
133296
133391
|
let best;
|
|
133297
133392
|
let bestExact = -1;
|
|
133298
133393
|
for (const n of visible) {
|
|
133299
|
-
const exact = exactFieldCount(n, selector);
|
|
133394
|
+
const exact = exactFieldCount(n, selector, fullTextRegex);
|
|
133300
133395
|
if (best === void 0 || exact !== bestExact) {
|
|
133301
133396
|
if (exact > bestExact) {
|
|
133302
133397
|
best = n;
|
|
@@ -133589,6 +133684,7 @@ Allowed tools and their args (udid is auto-injected, do NOT include it in args):
|
|
|
133589
133684
|
gesture-rotate: { centerX: number, centerY: number, radius: number, startAngle: number, endAngle: number, durationMs?: number } [ios only]
|
|
133590
133685
|
button: { button: "home"|"back"|"power"|"volumeUp"|"volumeDown"|"appSwitch"|"actionButton" } [ios/android]
|
|
133591
133686
|
keyboard: { text?: string, key?: string, delayMs?: number } (key pressed after text; TV: text only) [ios/android/chromium/vega/tv]
|
|
133687
|
+
text supports {{secret:<NAME>}} placeholders, resolved server-side from ARGENT_SECRET_<NAME> env vars (prefix mandatory) \u2014 credentials never enter agent context
|
|
133592
133688
|
rotate: { orientation: "Portrait"|"LandscapeLeft"|"LandscapeRight"|"PortraitUpsideDown" } [ios/android]
|
|
133593
133689
|
tv-remote: { button: <remote button | array of them>, repeat?: number } [apple tv/android tv/vega]
|
|
133594
133690
|
buttons: up/down/left/right/select/back/home/menu/playPause (+ rewind/fastForward/next/previous/volumeUp/volumeDown/mute \u2014 work on Android TV and Vega; rejected on the Apple TV simulator)
|
|
@@ -144519,14 +144615,67 @@ function chromiumLaunchSpec(launch) {
|
|
|
144519
144615
|
return typeof c === "string" ? { path: c } : { path: c.path, args: c.args };
|
|
144520
144616
|
}
|
|
144521
144617
|
function selectorToYaml(sel) {
|
|
144618
|
+
if (sel.text !== void 0 && sel.textMatches !== void 0) {
|
|
144619
|
+
throw new Error(
|
|
144620
|
+
'Cannot serialize flow selector without losing constraints: both `text` and `textMatches` are set, but flow YAML can represent only one `text` constraint (a literal string or `{ matches: "<regex>" }`). Use either literal or regex text matching.'
|
|
144621
|
+
);
|
|
144622
|
+
}
|
|
144623
|
+
if (sel.loose && sel.text !== void 0 && (typeof sel.text !== "string" || sel.text.length === 0)) {
|
|
144624
|
+
throw new Error(
|
|
144625
|
+
"Cannot serialize loose flow selector: `text` must be a non-empty string so bare-string YAML can round-trip through selector validation."
|
|
144626
|
+
);
|
|
144627
|
+
}
|
|
144628
|
+
if (sel.loose && (sel.text === void 0 || sel.textMatches !== void 0 || sel.identifier !== void 0 || sel.role !== void 0)) {
|
|
144629
|
+
const incompatible = [
|
|
144630
|
+
sel.textMatches !== void 0 ? "textMatches" : void 0,
|
|
144631
|
+
sel.identifier !== void 0 ? "identifier" : void 0,
|
|
144632
|
+
sel.role !== void 0 ? "role" : void 0
|
|
144633
|
+
].filter((field) => field !== void 0);
|
|
144634
|
+
throw new Error(
|
|
144635
|
+
"Cannot serialize loose flow selector without changing its meaning: bare-string YAML can represent only a loose text-only selector" + (incompatible.length > 0 ? `; incompatible fields: ${incompatible.join(", ")}` : "") + "."
|
|
144636
|
+
);
|
|
144637
|
+
}
|
|
144522
144638
|
if (sel.loose && sel.text !== void 0 && sel.identifier === void 0 && sel.role === void 0) {
|
|
144523
144639
|
return sel.text;
|
|
144524
144640
|
}
|
|
144525
|
-
const { loose: _loose, identifier, ...rest } = sel;
|
|
144526
|
-
|
|
144641
|
+
const { loose: _loose, identifier, textMatches: textMatches2, ...rest } = sel;
|
|
144642
|
+
const out = { ...rest };
|
|
144643
|
+
if (textMatches2 !== void 0) out.text = { matches: textMatches2 };
|
|
144644
|
+
if (identifier !== void 0) out.id = identifier;
|
|
144645
|
+
return out;
|
|
144527
144646
|
}
|
|
144528
144647
|
function describeSelector(s) {
|
|
144529
|
-
return Object.entries(s).filter(([k]) => k !== "loose").map(
|
|
144648
|
+
return Object.entries(s).filter(([k]) => k !== "loose").map(
|
|
144649
|
+
([k, v]) => k === "textMatches" ? `text=/${v}/` : `${k === "identifier" ? "id" : k}="${v}"`
|
|
144650
|
+
).join(" ");
|
|
144651
|
+
}
|
|
144652
|
+
function describeTextExpectation(expectedText, textMatch, verbForm = "mode") {
|
|
144653
|
+
const expected = expectedText ?? "";
|
|
144654
|
+
const mode = textMatch ?? "contains";
|
|
144655
|
+
switch (mode) {
|
|
144656
|
+
case "contains":
|
|
144657
|
+
return `${verbForm === "infinitive" ? "contain" : mode} ${JSON.stringify(expected)}`;
|
|
144658
|
+
case "equals":
|
|
144659
|
+
return `${verbForm === "infinitive" ? "equal" : mode} ${JSON.stringify(expected)}`;
|
|
144660
|
+
case "matches":
|
|
144661
|
+
return `${verbForm === "infinitive" ? "match" : mode} /${expected}/`;
|
|
144662
|
+
}
|
|
144663
|
+
}
|
|
144664
|
+
function textWaitToYaml(selector, expectedText, textMatch) {
|
|
144665
|
+
const expected = expectedText ?? "";
|
|
144666
|
+
const mode = textMatch ?? "contains";
|
|
144667
|
+
switch (mode) {
|
|
144668
|
+
case "contains":
|
|
144669
|
+
return { text: { in: selector, contains: expected } };
|
|
144670
|
+
case "equals":
|
|
144671
|
+
return { text: { in: selector, equals: expected } };
|
|
144672
|
+
case "matches":
|
|
144673
|
+
return { text: { in: selector, matches: expected } };
|
|
144674
|
+
default: {
|
|
144675
|
+
const exhaustive = mode;
|
|
144676
|
+
throw new Error(`Unsupported text match mode: ${exhaustive}`);
|
|
144677
|
+
}
|
|
144678
|
+
}
|
|
144530
144679
|
}
|
|
144531
144680
|
function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
|
|
144532
144681
|
const sel = selectorToYaml(selector);
|
|
@@ -144542,7 +144691,7 @@ function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
|
|
|
144542
144691
|
body = { hidden: sel };
|
|
144543
144692
|
break;
|
|
144544
144693
|
case "text":
|
|
144545
|
-
body =
|
|
144694
|
+
body = textWaitToYaml(sel, expectedText, textMatch);
|
|
144546
144695
|
break;
|
|
144547
144696
|
}
|
|
144548
144697
|
if (timeoutMs !== void 0) body.timeout = timeoutMs;
|
|
@@ -144635,6 +144784,16 @@ function badEntry(raw, detail) {
|
|
|
144635
144784
|
error_kind: "validation"
|
|
144636
144785
|
});
|
|
144637
144786
|
}
|
|
144787
|
+
function validatePattern(raw, pattern, where) {
|
|
144788
|
+
try {
|
|
144789
|
+
new RegExp(pattern);
|
|
144790
|
+
} catch (err) {
|
|
144791
|
+
badEntry(
|
|
144792
|
+
raw,
|
|
144793
|
+
`${where} \`matches\` is not a valid regular expression: ${err instanceof Error ? err.message : String(err)}`
|
|
144794
|
+
);
|
|
144795
|
+
}
|
|
144796
|
+
}
|
|
144638
144797
|
function editDistance(a, b) {
|
|
144639
144798
|
let prevPrev = new Array(b.length + 1);
|
|
144640
144799
|
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
@@ -144698,11 +144857,46 @@ function parseSelector(raw, where) {
|
|
|
144698
144857
|
}
|
|
144699
144858
|
normalized = { ...rest, identifier: id };
|
|
144700
144859
|
}
|
|
144860
|
+
if (normalized !== null && typeof normalized === "object") {
|
|
144861
|
+
const { text, ...rest } = normalized;
|
|
144862
|
+
if (text !== null && typeof text === "object") {
|
|
144863
|
+
const keys = Object.keys(text);
|
|
144864
|
+
if (!Array.isArray(text)) {
|
|
144865
|
+
rejectUnknownKeys(
|
|
144866
|
+
raw,
|
|
144867
|
+
text,
|
|
144868
|
+
["matches"],
|
|
144869
|
+
`${where}: text matcher`
|
|
144870
|
+
);
|
|
144871
|
+
}
|
|
144872
|
+
const pattern = text.matches;
|
|
144873
|
+
if (keys.length !== 1 || keys[0] !== "matches") {
|
|
144874
|
+
badEntry(
|
|
144875
|
+
raw,
|
|
144876
|
+
`${where}: a text matcher takes exactly { matches: '<regex>' } \u2014 for a substring, use the plain-string form (text: "\u2026")`
|
|
144877
|
+
);
|
|
144878
|
+
}
|
|
144879
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
144880
|
+
badEntry(raw, `${where}: text matcher needs a non-empty \`matches\` pattern`);
|
|
144881
|
+
}
|
|
144882
|
+
validatePattern(raw, pattern, `${where}: text`);
|
|
144883
|
+
const fields = selectorFieldsSchema.safeParse(rest);
|
|
144884
|
+
if (!fields.success) {
|
|
144885
|
+
badEntry(raw, `${where}: ${fields.error.issues[0]?.message ?? "invalid selector"}`);
|
|
144886
|
+
}
|
|
144887
|
+
return { ...fields.data, textMatches: pattern };
|
|
144888
|
+
}
|
|
144889
|
+
}
|
|
144701
144890
|
const r = selectorSchema.safeParse(normalized);
|
|
144702
144891
|
if (!r.success) badEntry(raw, `${where}: ${r.error.issues[0]?.message ?? "invalid selector"}`);
|
|
144703
144892
|
return r.data;
|
|
144704
144893
|
}
|
|
144705
144894
|
var WAIT_CONDITIONS = ["exists", "visible", "hidden", "text"];
|
|
144895
|
+
var TEXT_MATCH_MODES = Object.keys({
|
|
144896
|
+
contains: true,
|
|
144897
|
+
equals: true,
|
|
144898
|
+
matches: true
|
|
144899
|
+
});
|
|
144706
144900
|
var SCROLL_DIRECTIONS = ["up", "down", "left", "right"];
|
|
144707
144901
|
function parseWaitFields(raw, kind) {
|
|
144708
144902
|
if (raw === null || typeof raw !== "object") {
|
|
@@ -144742,22 +144936,30 @@ function parseWaitFields(raw, kind) {
|
|
|
144742
144936
|
if (condition === "text") {
|
|
144743
144937
|
const t = b.text;
|
|
144744
144938
|
if (t === null || typeof t !== "object") {
|
|
144745
|
-
badEntry(
|
|
144939
|
+
badEntry(
|
|
144940
|
+
{ [kind]: b },
|
|
144941
|
+
`${kind} text needs { in: <selector>, contains|equals|matches: <string> }`
|
|
144942
|
+
);
|
|
144746
144943
|
}
|
|
144747
144944
|
const tb = t;
|
|
144748
144945
|
if (!Array.isArray(tb)) {
|
|
144749
|
-
rejectUnknownKeys({ [kind]: b }, tb, ["in",
|
|
144946
|
+
rejectUnknownKeys({ [kind]: b }, tb, ["in", ...TEXT_MATCH_MODES], `${kind}.text`);
|
|
144750
144947
|
}
|
|
144751
|
-
const
|
|
144752
|
-
|
|
144753
|
-
|
|
144754
|
-
|
|
144948
|
+
const comparators = TEXT_MATCH_MODES.filter((mode) => mode in tb);
|
|
144949
|
+
if (comparators.length !== 1) {
|
|
144950
|
+
badEntry(
|
|
144951
|
+
{ [kind]: b },
|
|
144952
|
+
`${kind} text needs exactly one of \`contains\`, \`equals\`, or \`matches\``
|
|
144953
|
+
);
|
|
144755
144954
|
}
|
|
144756
|
-
const textMatch =
|
|
144757
|
-
const expected =
|
|
144955
|
+
const textMatch = comparators[0];
|
|
144956
|
+
const expected = tb[textMatch];
|
|
144758
144957
|
if (typeof expected !== "string" || expected.length === 0) {
|
|
144759
144958
|
badEntry({ [kind]: b }, `${kind} text needs a non-empty \`${textMatch}\``);
|
|
144760
144959
|
}
|
|
144960
|
+
if (textMatch === "matches") {
|
|
144961
|
+
validatePattern({ [kind]: b }, expected, `${kind} text`);
|
|
144962
|
+
}
|
|
144761
144963
|
return {
|
|
144762
144964
|
condition: "text",
|
|
144763
144965
|
selector: parseSelector(tb.in, `${kind}.text.in`),
|
|
@@ -145869,7 +146071,14 @@ You can still edit the .yaml file directly afterwards to remove or reorder steps
|
|
|
145869
146071
|
return `${n}. type: ${selectorLabel(step.into)} \u2190 "${step.text}"`;
|
|
145870
146072
|
case "await":
|
|
145871
146073
|
case "assert": {
|
|
145872
|
-
|
|
146074
|
+
let tail;
|
|
146075
|
+
if (step.condition !== "text") {
|
|
146076
|
+
tail = `${step.condition} ${selectorLabel(step.selector)}`;
|
|
146077
|
+
} else {
|
|
146078
|
+
const selector = selectorLabel(step.selector);
|
|
146079
|
+
const expected = step.expectedText ?? "";
|
|
146080
|
+
tail = step.textMatch === "matches" ? `text ${selector} matches /${expected}/` : step.textMatch === "equals" ? `text ${selector} == ${JSON.stringify(expected)}` : `text ${selector} contains ${JSON.stringify(expected)}`;
|
|
146081
|
+
}
|
|
145873
146082
|
return `${n}. ${step.kind}: ${tail}`;
|
|
145874
146083
|
}
|
|
145875
146084
|
case "wait":
|
|
@@ -146263,11 +146472,11 @@ function assertReason(condition, selector, expectedText, textMatch, matches2) {
|
|
|
146263
146472
|
case "text": {
|
|
146264
146473
|
const first = firstInReadingOrder(matches2.filter(isVisible)) ?? firstInReadingOrder(matches2);
|
|
146265
146474
|
if (!first) return `no element matched selector ${sel}`;
|
|
146266
|
-
const wanted = textMatch
|
|
146475
|
+
const wanted = describeTextExpectation(expectedText, textMatch, "infinitive");
|
|
146267
146476
|
const shown = assertText(first);
|
|
146268
146477
|
const own = nodeText(first);
|
|
146269
146478
|
const ownNote = own && own !== shown ? ` (own text "${own}")` : "";
|
|
146270
|
-
return `element matched ${sel} but its text was "${shown}"${ownNote} (wanted to ${wanted}
|
|
146479
|
+
return `element matched ${sel} but its text was "${shown}"${ownNote} (wanted to ${wanted})`;
|
|
146271
146480
|
}
|
|
146272
146481
|
default:
|
|
146273
146482
|
return `assertion failed for selector ${sel}`;
|
|
@@ -148763,6 +148972,7 @@ function pushReport(state3, report) {
|
|
|
148763
148972
|
function selectorLabel2(sel) {
|
|
148764
148973
|
const parts = [];
|
|
148765
148974
|
if (sel.text !== void 0) parts.push(`"${sel.text}"`);
|
|
148975
|
+
if (sel.textMatches !== void 0) parts.push(`/${sel.textMatches}/`);
|
|
148766
148976
|
if (sel.identifier) parts.push(`id=${sel.identifier}`);
|
|
148767
148977
|
if (sel.role) parts.push(`role=${sel.role}`);
|
|
148768
148978
|
return parts.join(" ");
|
|
@@ -148781,7 +148991,7 @@ function stepTarget(step) {
|
|
|
148781
148991
|
case "assert": {
|
|
148782
148992
|
const sel = selectorLabel2(step.selector);
|
|
148783
148993
|
if (step.condition === "text") {
|
|
148784
|
-
return `${sel} ${step.
|
|
148994
|
+
return `${sel} ${describeTextExpectation(step.expectedText, step.textMatch)}`;
|
|
148785
148995
|
}
|
|
148786
148996
|
return `${step.condition} ${sel}`;
|
|
148787
148997
|
}
|
|
@@ -149682,7 +149892,7 @@ function normLabel(s) {
|
|
|
149682
149892
|
return (s || "").toLowerCase().replace(/-/g, "").replace(/[\s,]+/g, " ").trim();
|
|
149683
149893
|
}
|
|
149684
149894
|
var MAX_FRAME_AREA = 0.85;
|
|
149685
|
-
function
|
|
149895
|
+
function matchNode(n, match, needle) {
|
|
149686
149896
|
const label = normLabel(n.label);
|
|
149687
149897
|
const ident = normLabel(n.identifier);
|
|
149688
149898
|
const value = normLabel(n.value);
|
|
@@ -149709,7 +149919,7 @@ function findElementMatch(tree, match) {
|
|
|
149709
149919
|
const candidates = [];
|
|
149710
149920
|
const walk = (n) => {
|
|
149711
149921
|
if (!n || typeof n !== "object") return;
|
|
149712
|
-
const m =
|
|
149922
|
+
const m = matchNode(n, match, needle);
|
|
149713
149923
|
if (m && n.frame) {
|
|
149714
149924
|
const f = n.frame;
|
|
149715
149925
|
const cx = f.x + f.width / 2;
|
package/package.json
CHANGED
|
@@ -37,7 +37,9 @@ Beyond raw `tool:` steps and `echo:`, flows support declarative directives inter
|
|
|
37
37
|
|
|
38
38
|
A **selector** is `{ text?, id?, role? }` (all-must-match; `text`/`role` are case-insensitive substrings, `id` matches the element's testID / accessibilityIdentifier / resource-id exactly, case-insensitive, also accepting the unqualified Android resource-id name — `submit` matches `com.example.app:id/submit`) — the same semantics `await-ui-element` uses, though that tool spells the `id` field `identifier` (flow YAML also accepts `identifier` as an alias for `id`, but `id` is the canonical spelling and what the recorder writes). A bare string is a _loose_ selector: it resolves **identifier-first, then falls back to text** (label/value), so `tap: Login` matches a `testID="Login"` or, failing that, visible text "Login" — no need to know which. Loose fallback applies uniformly to every selector slot (`tap`, `type.into`, `await`, `assert`, `scroll-to`). Use the map form to be strict: `{ id: submit-btn }` (identifier only) or `{ text: Login }` (text only, no fallback).
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
`text` also takes a **regex matcher map** — `{ text: { matches: '^Order #\d+$' } }`, in any selector slot — for dynamic text no literal can pin. It tests each node's native **own** label/value (not the adapter-hoisted `subtreeText`), though on iOS a container's own label may itself aggregate descendant text, so a wrapper and its leaf can both match. Same regex rules as `text.in`'s `matches` (see _`await` and `assert`_): unanchored, **case-sensitive**, single-quoted, invalid pattern fails at parse. So `assert: { visible: { text: { matches: '^Taps: \d+$' } } }` asserts a counter is on screen with no locator at all, and `tap: { text: { matches: '^Order #\d+$' } }` taps a dynamic row — though a stable `id` stays the more robust action target.
|
|
41
|
+
|
|
42
|
+
Selectors resolve against the **full native hierarchy** (iOS: the UIView tree; Android: the complete accessibility hierarchy including not-important views) — strictly more than `describe` or the raw `await-ui-element` tool see (both use the trimmed tree), with complete `testID`/`resource-id` coverage. So an `id` selector works even when `describe` collapses or omits the element — don't fall back to coordinate taps just because a testID isn't visible in `describe` output. And when several elements match — including wrappers whose native text aggregates descendant content — the action directives (`tap`, `type`, `scroll-to`) pick the **most specific** match: an exact text/identifier match beats a substring hit (for a regex matcher, a pattern consuming the element's whole text counts as exact), then the smallest frame wins.
|
|
41
43
|
|
|
42
44
|
**Quote strings YAML would mangle.** An unquoted `#` starts a YAML comment — `tap: Order #1234` silently parses as `tap: Order` — and bare `yes`/`no`/`on`/`off`/numbers coerce to non-strings. When a selector or typed text contains `#`, `:`, quotes, or could read as a boolean/number, wrap it: `tap: "Order #1234"`.
|
|
43
45
|
|
|
@@ -47,6 +49,7 @@ The **condition is the key**, and its value is the selector:
|
|
|
47
49
|
|
|
48
50
|
- `{ visible: Home }`, `{ exists: { id: row } }`, `{ hidden: spinner }`
|
|
49
51
|
- `{ text: { in: <selector>, contains: "Taps:" } }` or `{ text: { in: <selector>, equals: "Taps: 0" } }` — `text` locates an element (`in`) and checks its rendered content against exactly one of `contains` (case-insensitive substring) or `equals` (case-insensitive exact match — use it when boundaries matter: `contains: "Taps: 3"` is also satisfied by "Taps: 30"). Reach for `text` only when the locator is an identifier/role; to assert a string is simply on screen, prefer `{ visible: "Taps: 0" }`.
|
|
52
|
+
- `{ text: { in: total, matches: 'Total: \$\d+\.\d{2}' } }` — the third comparator: a JS regex for dynamic content (counters, prices, dates) that neither literal mode can pin. Unanchored like `contains` (anchor with `^…$` for the `equals` analog) and — unlike the literal modes — **case-sensitive**: the pattern carries its own semantics. An invalid pattern fails at parse time. **Quote the pattern in single quotes**: single-quoted and plain YAML scalars keep backslashes; double quotes would need `\\d`. To assert a dynamic string is simply on screen with no locator, prefer a regex **selector** — `{ visible: { text: { matches: '^Taps: \d+$' } } }` (see Selectors); `text.in` + `matches` is for checking a specific element's aggregated text.
|
|
50
53
|
- A container's text aggregates its descendants' text (space-joined), so `text` can assert what a testID wrapper visibly shows even when the string lives in a child node. That also means `equals` against a wrapper must match _everything_ it shows or exactly the wrapper's own label/value — targeting the leaf holding exactly the value (or using `contains`) stays the clearer spelling.
|
|
51
54
|
|
|
52
55
|
This condition-as-key form is the only spelling. `await` also accepts an optional `timeout` sibling key in milliseconds — `- await: { visible: Home, timeout: 15000 }` — for a transition that legitimately needs longer than the default budget. **Omit `timeout` by default**: the default budget covers normal transitions, and a habitual generous override just delays failure reporting on every broken step. Add one only after a step demonstrably needs it — it timed out at the default and the wait is legitimately slow (a cold start, a network round-trip, a long animation). `assert` has no timeout override: a check that needs seconds to become true is a wait — spell it `await`.
|
|
@@ -57,6 +60,8 @@ For a custom poll interval or bundleId, drop to an explicit `- tool: await-ui-el
|
|
|
57
60
|
|
|
58
61
|
`type` presses Enter after typing to commit the value and dismiss the keyboard, so it can't cover later targets. For a chained form whose fields feed one explicit submit — e.g. email then password then a `tap: "Log in"` — set `submit: false` on the intermediate fields so a premature Enter doesn't fire the form early: `type: { into: password, text: "hunter2", submit: false }`.
|
|
59
62
|
|
|
63
|
+
Never record a real credential into a flow — the YAML is committed to the repo. Use a secret placeholder instead: `type: { into: password, text: "{{secret:APP_PASSWORD}}" }`. The placeholder is stored verbatim (the YAML stays secret-free) and is resolved at run time by the tool-server from the `ARGENT_SECRET_APP_PASSWORD` environment variable — including agent-less `argent flow run` in CI, where the variable comes from the job's secrets.
|
|
64
|
+
|
|
60
65
|
`scroll-to` takes an optional `direction` (`up` | `down` | `left` | `right`, default `down` — so the common case is just `- scroll-to: <selector>`) and optionally a `within: <selector>` that anchors the scroll inside a specific container — required to drive a **nested** scroller (e.g. a horizontal carousel inside a vertical list), since the device can't be asked which container to scroll. It scrolls in bounded momentum-free increments, re-checks after each, and stops if a scroll reveals nothing new (end of the container). `tap`/`type` do **not** scroll — add a `scroll-to` before any target that may be off-screen. It's a no-op when the target is already visible, so a defensive `scroll-to` costs nothing on replay and keeps the flow working on smaller screens.
|
|
61
66
|
|
|
62
67
|
### TV targets (Vega)
|
|
@@ -171,6 +171,18 @@ Values: `home`, `back`, `power`, `volumeUp`, `volumeDown`, `appSwitch`, `actionB
|
|
|
171
171
|
|
|
172
172
|
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.
|
|
173
173
|
|
|
174
|
+
**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):
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{ "udid": "<UDID>", "text": "{{secret:APP_PASSWORD}}", "key": "enter" }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The placeholder is resolved on the machine running the tool-server from the `ARGENT_SECRET_<NAME>` environment variable (here `ARGENT_SECRET_APP_PASSWORD`) — the CI-native pattern: expose the secret under that prefix in the environment that starts the tool-server. Rules:
|
|
181
|
+
|
|
182
|
+
- The result echoes the placeholder, never the value. An unknown name fails with the list of available secret _names_.
|
|
183
|
+
- 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.
|
|
184
|
+
- Only `ARGENT_SECRET_*` variables are resolvable; never ask the user to paste a secret value into the conversation — ask them to export the env var instead.
|
|
185
|
+
|
|
174
186
|
### rotate — Change orientation
|
|
175
187
|
|
|
176
188
|
```json
|