codeloop-mcp-server 0.1.87 → 0.1.91
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/evidence/interaction_coverage.d.ts.map +1 -1
- package/dist/evidence/interaction_coverage.js +2 -0
- package/dist/evidence/interaction_coverage.js.map +1 -1
- package/dist/index.js +244 -55
- package/dist/index.js.map +1 -1
- package/dist/runners/active_target.d.ts +31 -0
- package/dist/runners/active_target.d.ts.map +1 -0
- package/dist/runners/active_target.js +65 -0
- package/dist/runners/active_target.js.map +1 -0
- package/dist/runners/app_launcher.d.ts +37 -1
- package/dist/runners/app_launcher.d.ts.map +1 -1
- package/dist/runners/app_launcher.js +114 -15
- package/dist/runners/app_launcher.js.map +1 -1
- package/dist/runners/app_logger.d.ts +1 -1
- package/dist/runners/app_logger.d.ts.map +1 -1
- package/dist/runners/app_logger.js +9 -8
- package/dist/runners/app_logger.js.map +1 -1
- package/dist/runners/device_probe.d.ts +18 -0
- package/dist/runners/device_probe.d.ts.map +1 -1
- package/dist/runners/device_probe.js +47 -20
- package/dist/runners/device_probe.js.map +1 -1
- package/dist/runners/interaction_engine.d.ts +6 -0
- package/dist/runners/interaction_engine.d.ts.map +1 -1
- package/dist/runners/interaction_engine.js +64 -21
- package/dist/runners/interaction_engine.js.map +1 -1
- package/dist/runners/ios_sim_input.d.ts +106 -0
- package/dist/runners/ios_sim_input.d.ts.map +1 -0
- package/dist/runners/ios_sim_input.js +453 -0
- package/dist/runners/ios_sim_input.js.map +1 -0
- package/dist/runners/maestro.d.ts +27 -2
- package/dist/runners/maestro.d.ts.map +1 -1
- package/dist/runners/maestro.js +57 -7
- package/dist/runners/maestro.js.map +1 -1
- package/dist/runners/maestro_generator.d.ts +1 -1
- package/dist/runners/maestro_generator.d.ts.map +1 -1
- package/dist/runners/maestro_generator.js +3 -2
- package/dist/runners/maestro_generator.js.map +1 -1
- package/dist/runners/screenshot.d.ts +6 -0
- package/dist/runners/screenshot.d.ts.map +1 -1
- package/dist/runners/screenshot.js +10 -9
- package/dist/runners/screenshot.js.map +1 -1
- package/dist/runners/video_recorder.d.ts +3 -1
- package/dist/runners/video_recorder.d.ts.map +1 -1
- package/dist/runners/video_recorder.js +19 -6
- package/dist/runners/video_recorder.js.map +1 -1
- package/dist/runners/window_manager.d.ts +55 -25
- package/dist/runners/window_manager.d.ts.map +1 -1
- package/dist/runners/window_manager.js +171 -71
- package/dist/runners/window_manager.js.map +1 -1
- package/dist/tools/discover_interactions.d.ts +22 -0
- package/dist/tools/discover_interactions.d.ts.map +1 -1
- package/dist/tools/discover_interactions.js +278 -5
- package/dist/tools/discover_interactions.js.map +1 -1
- package/dist/tools/discover_screens.d.ts +2 -0
- package/dist/tools/discover_screens.d.ts.map +1 -1
- package/dist/tools/discover_screens.js +139 -1
- package/dist/tools/discover_screens.js.map +1 -1
- package/dist/tools/run_journey.d.ts +7 -0
- package/dist/tools/run_journey.d.ts.map +1 -1
- package/dist/tools/run_journey.js +106 -17
- package/dist/tools/run_journey.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1495,6 +1495,7 @@ Returns: confirmation + the captured image as an MCP ImageContent block so you c
|
|
|
1495
1495
|
workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics; accepted because many agents reach for this conventional name. Pass either `project_dir` OR `workspace_root` — they're equivalent."),
|
|
1496
1496
|
target_change_entry: z.string().optional().describe("0.1.52 C7 — verbatim display name of the change-manifest entry this screenshot exercises (e.g. 'datagrid_column: \"Product Code\"' or 'PhotometricConfigurations.ProductCode'). When present, the screenshot file is auto-anchored: the filename is prefixed with a slugged form of the entry so the change_coverage_evidence (C3) gate's screenshot scan can credit this evidence to the correct manifest entry without fuzzy matching. The value is also persisted alongside the screenshot path in the response so downstream tools (interaction_replay, gate_check) can use it."),
|
|
1497
1497
|
target_type: z.string().optional().describe("Capture source. Omit for desktop/window capture (default). Set 'android_emulator' (or 'android') to capture the BOOTED Android emulator via adb, or 'ios_simulator' (or 'ios', macOS only) to capture the booted iOS simulator via simctl. Use this to screenshot a Flutter/native mobile app running on a simulator instead of the host desktop."),
|
|
1498
|
+
device_id: z.string().optional().describe("Pinned mobile device for emulator/simulator capture: adb serial or simulator UDID. Defaults to the device the last journey/recording targeted, then adb default / simctl 'booted'."),
|
|
1498
1499
|
}, async (params) => {
|
|
1499
1500
|
const authResult = await withAuth(async () => {
|
|
1500
1501
|
const { captureScreenshot } = await import("./runners/screenshot.js");
|
|
@@ -1543,7 +1544,14 @@ Returns: confirmation + the captured image as an MCP ImageContent block so you c
|
|
|
1543
1544
|
const ttRaw = normalizeTargetType(params.target_type);
|
|
1544
1545
|
const explicitTargetType = ttRaw === "android_emulator" || ttRaw === "ios_simulator" ? ttRaw : undefined;
|
|
1545
1546
|
const isMobileCapture = explicitTargetType !== undefined;
|
|
1546
|
-
|
|
1547
|
+
// Pin the capture to the journey/recording device when known.
|
|
1548
|
+
let captureDevice;
|
|
1549
|
+
if (isMobileCapture) {
|
|
1550
|
+
const { resolveDeviceId } = await import("./runners/active_target.js");
|
|
1551
|
+
const vr = await import("./runners/video_recorder.js");
|
|
1552
|
+
captureDevice = resolveDeviceId(params.device_id ?? vr.getActiveRecordingDevice() ?? undefined, cwd, explicitTargetType);
|
|
1553
|
+
}
|
|
1554
|
+
const result = await captureScreenshot(screenshotsDir, finalScreenName, isMobileCapture ? undefined : targetApp, explicitTargetType, { desktopAppMode: isMobileCapture ? false : desktopApp, device: captureDevice });
|
|
1547
1555
|
// Photometry-DB E2E 8 follow-on: when we capture a desktop app
|
|
1548
1556
|
// window, also resolve its on-screen bounds so the agent can
|
|
1549
1557
|
// (a) compute window-relative coords from the returned image
|
|
@@ -1814,8 +1822,15 @@ plan→start_recording→interact→stop→replay sequence: CodeLoop does it for
|
|
|
1814
1822
|
Runs in EVERY mode — launching + driving the app is verification, not a code edit, so a
|
|
1815
1823
|
"don't modify my code" request never disables it (that only pauses the agent's source edits).
|
|
1816
1824
|
|
|
1825
|
+
When the USER asks to test a SPECIFIC platform (e.g. "test the Android app", "now run it on iOS"),
|
|
1826
|
+
pass target_type for that platform (android_emulator / ios_simulator / browser / desktop). It launches
|
|
1827
|
+
that platform's emulator/simulator even when it isn't the host default — the Android emulator boots on
|
|
1828
|
+
macOS too — so an iOS-default Mac can still drive the Android app on request. For a persistent default,
|
|
1829
|
+
set e2e.target in .codeloop/config.json (honored by the verify auto-journey as well).
|
|
1830
|
+
|
|
1817
1831
|
What it does, in order:
|
|
1818
|
-
1. Detects the target (browser / desktop / android_emulator / ios_simulator / Flutter)
|
|
1832
|
+
1. Detects the target (browser / desktop / android_emulator / ios_simulator / Flutter). Override per-call
|
|
1833
|
+
with target_type, or persistently with e2e.target; otherwise mobile-first auto-detection is used.
|
|
1819
1834
|
2. READY/LAUNCH per target: web → headed Playwright at e2e.web_url; desktop → launch evidence.target_app;
|
|
1820
1835
|
Android/iOS → BOOT the emulator/simulator (reuses one already booted; honors e2e.android_avd /
|
|
1821
1836
|
e2e.ios_device; opt out with e2e.boot_device:false). If a mobile device can't be booted it returns
|
|
@@ -2021,6 +2036,7 @@ App logs (stdout, logcat, simctl log) are automatically captured alongside the v
|
|
|
2021
2036
|
target_type: targetTypeSchema.optional()
|
|
2022
2037
|
.describe("Capture method. Auto-detected from project if omitted. desktop=ffmpeg screen, android_emulator=adb screenrecord, ios_simulator=simctl recordVideo, browser=ffmpeg/Playwright"),
|
|
2023
2038
|
auto_launch: z.boolean().default(true).describe("When target_type=desktop and the app isn't already running, auto-launch it from the project's build output via evidence.target_app. Set false to skip (e.g. when the app is started by another process)."),
|
|
2039
|
+
device_id: z.string().optional().describe("Pinned mobile device to record: adb serial (emulator-5554) or simulator UDID. Defaults to the device the last journey targeted, then the first booted device. The pinned device is persisted so follow-up codeloop_interact calls hit the same one."),
|
|
2024
2040
|
project_dir: z.string().optional().describe("Absolute path to the project root. Defaults to CODELOOP_PROJECT_DIR env var or auto-discovered project directory. MUST be an actual project folder — passing the user's home directory is rejected. If your IDE launches the MCP server from the wrong cwd (common on Windows where Cursor uses C:\\Users\\<name> as cwd), set CODELOOP_PROJECT_DIR or pass this param explicitly."),
|
|
2025
2041
|
workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics; accepted because many agents reach for this conventional name. Pass either `project_dir` OR `workspace_root` — they're equivalent."),
|
|
2026
2042
|
}, async (params) => {
|
|
@@ -2118,6 +2134,7 @@ App logs (stdout, logcat, simctl log) are automatically captured alongside the v
|
|
|
2118
2134
|
// concrete commands rather than letting it guess. CodeLoop does not boot
|
|
2119
2135
|
// devices itself (heavy + can hijack the desktop), it directs the agent.
|
|
2120
2136
|
let deviceReadinessDirective;
|
|
2137
|
+
let recordingDevice;
|
|
2121
2138
|
if (targetType === "android_emulator" || targetType === "ios_simulator") {
|
|
2122
2139
|
try {
|
|
2123
2140
|
const { probeBootedDevices, detectMobileTargets, buildOpenSimulatorsDirective } = await import("./runners/device_probe.js");
|
|
@@ -2132,10 +2149,22 @@ App logs (stdout, logcat, simctl log) are automatically captured alongside the v
|
|
|
2132
2149
|
context: "recording",
|
|
2133
2150
|
});
|
|
2134
2151
|
}
|
|
2152
|
+
else {
|
|
2153
|
+
// Pin the recording to ONE device: explicit param → persisted
|
|
2154
|
+
// active target → first booted device of this platform. Persist
|
|
2155
|
+
// the choice so follow-up codeloop_interact calls hit the same one.
|
|
2156
|
+
const { resolveDeviceId, saveActiveTarget } = await import("./runners/active_target.js");
|
|
2157
|
+
recordingDevice =
|
|
2158
|
+
resolveDeviceId(params.device_id, cwd, targetType) ??
|
|
2159
|
+
(targetType === "android_emulator" ? booted.android[0] : booted.ios_devices[0]?.udid);
|
|
2160
|
+
if (recordingDevice) {
|
|
2161
|
+
saveActiveTarget(cwd, { target_type: targetType, device_id: recordingDevice });
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2135
2164
|
}
|
|
2136
2165
|
catch { /* best-effort */ }
|
|
2137
2166
|
}
|
|
2138
|
-
const result = await startBackgroundRecording(videosDir, appName ?? "", params.max_duration_seconds, targetType);
|
|
2167
|
+
const result = await startBackgroundRecording(videosDir, appName ?? "", params.max_duration_seconds, targetType, recordingDevice);
|
|
2139
2168
|
if (autoLaunchSummary) {
|
|
2140
2169
|
result.auto_launch = autoLaunchSummary;
|
|
2141
2170
|
}
|
|
@@ -2923,14 +2952,18 @@ scroll works on desktop via CGEvent (macOS), user32 mouse_event (Windows), xdoto
|
|
|
2923
2952
|
Falls back to arrow key presses if CGEvent fails (permissions).
|
|
2924
2953
|
Browser-specific: Uses Playwright selectors (CSS/text) when target_type is "browser".
|
|
2925
2954
|
Mobile-specific: swipe, back_button, home_button, deep_link, grant_permission, rotate_device,
|
|
2926
|
-
biometric_auth, launch_app, clear_app_data, mock_location, simulate_network
|
|
2955
|
+
biometric_auth, launch_app, clear_app_data, mock_location, simulate_network,
|
|
2956
|
+
push_notification (iOS: simctl push — package_id + value as APNs JSON or alert text),
|
|
2957
|
+
status_bar (iOS: simctl status_bar — value as JSON or "time=9:41,batteryLevel=100", or "clear").
|
|
2958
|
+
iOS (0.1.91): grant_permission → simctl privacy; mock_location → simctl location;
|
|
2959
|
+
clear_app_data → simctl uninstall (REINSTALL needed after); rotate_device → Simulator Cmd+arrow.
|
|
2927
2960
|
Maestro: maestro_flow — generate and run a Maestro YAML flow from high-level steps.
|
|
2928
2961
|
Windows: win_ui_inspect, win_ui_automate — PowerShell UI Automation for UWP/WinUI apps.
|
|
2929
2962
|
|
|
2930
2963
|
MANDATORY for web apps: You MUST type into form fields, fill login/signup forms, test
|
|
2931
2964
|
validation errors, and click submit buttons. Just navigating pages is NOT enough.
|
|
2932
2965
|
Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
2933
|
-
action: z.string().describe("Action to perform: click, double_click, right_click, hover, type, keystroke, hotkey, scroll, drag_drop, long_press, type_and_submit, type_and_tab, fill_form, select_option, toggle, get_text, upload_file, navigate_url, navigate_back, navigate_forward, wait, sequence, swipe, back_button, home_button, deep_link, grant_permission, rotate_device, biometric_auth, launch_app, clear_app_data, mock_location, simulate_network, maestro_flow, win_ui_inspect, win_ui_automate"),
|
|
2966
|
+
action: z.string().describe("Action to perform: click, double_click, right_click, hover, type, keystroke, hotkey, scroll, drag_drop, long_press, type_and_submit, type_and_tab, fill_form, select_option, toggle, get_text, upload_file, navigate_url, navigate_back, navigate_forward, wait, sequence, swipe, back_button, home_button, deep_link, grant_permission, rotate_device, biometric_auth, launch_app, clear_app_data, mock_location, simulate_network, push_notification, status_bar, maestro_flow, win_ui_inspect, win_ui_automate"),
|
|
2934
2967
|
expect_contains: z.string().optional().describe("[get_text] Optional assertion: the read-back text must contain this substring (case-insensitive). When set, success is false if the substring is absent. Use to confirm an AI chatbot answer / dynamic response actually rendered the expected content."),
|
|
2935
2968
|
target_type: targetTypeSchema.optional()
|
|
2936
2969
|
.describe("Interaction target. Auto-detected if omitted. Accepts synonyms: `windows_desktop`/`mac_desktop`/`linux_desktop` → `desktop`; `web` → `browser`; `android` → `android_emulator`; `ios` → `ios_simulator`."),
|
|
@@ -2949,7 +2982,7 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2949
2982
|
direction: z.enum(["up", "down", "left", "right"]).optional().describe("Scroll/swipe direction"),
|
|
2950
2983
|
amount: z.number().optional().describe("Scroll amount or other numeric value"),
|
|
2951
2984
|
duration_ms: z.number().optional().describe("Duration for wait, long_press, swipe"),
|
|
2952
|
-
value: z.string().optional().describe("Value for select_option, permission name, network mode, package ID"),
|
|
2985
|
+
value: z.string().optional().describe("Value for select_option, permission name, network mode, package ID, push_notification payload (APNs JSON or alert text), status_bar overrides (JSON or key=value list, or \"clear\")"),
|
|
2953
2986
|
file_path: z.string().optional().describe("File path for upload_file"),
|
|
2954
2987
|
fields: z.array(z.object({
|
|
2955
2988
|
selector: z.string(),
|
|
@@ -2972,6 +3005,7 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
2972
3005
|
.describe("For win_ui_automate"),
|
|
2973
3006
|
app_name: z.string().optional().describe("App name to bring to front before interaction. Auto-detected from active recording if omitted. Also used for launch_app, win_ui_inspect, win_ui_automate."),
|
|
2974
3007
|
package_id: z.string().optional().describe("Package/bundle ID for mobile actions"),
|
|
3008
|
+
device_id: z.string().optional().describe("Pinned mobile device: adb serial (e.g. emulator-5554) or iOS simulator UDID. When omitted, falls back to the device the last journey/recording targeted (.codeloop/active_target.json), then to adb's default / simctl 'booted'. Pass explicitly when several emulators/simulators are attached."),
|
|
2975
3009
|
intent: z.string().optional().describe("Semantic label for what this interaction is doing — short verb-phrase like 'edit product row', 'confirm delete dialog', 'save form', 'create new range'. STRONGLY RECOMMENDED for desktop coordinate-based clicks (target_type='desktop' with only x/y) because the CRUD classifier in user_journey_evidence can't infer the meaning of a raw (x, y) pair the way it can from a Playwright selector or DOM aria_label. Without `intent`, a sequence of coordinate clicks that delete a record will score `delete_actions: 0` and fail the gate. The classifier reads `intent` (plus `description` / `purpose` / `step` as aliases) into the same target-text bucket as selectors and aria labels, so the SAME keywords that work for browsers (edit / delete / save / submit / create / new) also work here. Example: when clicking the 'Yes' button of a Windows MessageBox at (2640, 820), pass intent='confirm delete'."),
|
|
2976
3010
|
description: z.string().optional().describe("[Alias for intent] Same semantics."),
|
|
2977
3011
|
purpose: z.string().optional().describe("[Alias for intent] Same semantics."),
|
|
@@ -3008,6 +3042,19 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3008
3042
|
tt = await detectTargetType(cwd);
|
|
3009
3043
|
}
|
|
3010
3044
|
}
|
|
3045
|
+
// Pin mobile commands to one device: explicit device_id param →
|
|
3046
|
+
// the device the active recording is capturing → the device the
|
|
3047
|
+
// last journey persisted (active_target.json) → undefined (adb
|
|
3048
|
+
// default / simctl "booted").
|
|
3049
|
+
let deviceId;
|
|
3050
|
+
if (tt === "android_emulator" || tt === "ios_simulator") {
|
|
3051
|
+
const { resolveDeviceId } = await import("./runners/active_target.js");
|
|
3052
|
+
deviceId = resolveDeviceId(params.device_id ?? vr.getActiveRecordingDevice() ?? undefined, cwd, tt);
|
|
3053
|
+
}
|
|
3054
|
+
// iOS sim input backend (CGEvent window mapping / idb) — simctl
|
|
3055
|
+
// has NO tap/type/swipe; see runners/ios_sim_input.ts.
|
|
3056
|
+
const sim = await import("./runners/ios_sim_input.js");
|
|
3057
|
+
const iosUdid = deviceId || "booted";
|
|
3011
3058
|
// For browser target, ensure Playwright headed browser is running
|
|
3012
3059
|
if (tt === "browser" && action !== "wait") {
|
|
3013
3060
|
await bi.ensureBrowserPage();
|
|
@@ -3154,10 +3201,13 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3154
3201
|
success = await bi.browserClick(params.selector);
|
|
3155
3202
|
}
|
|
3156
3203
|
else if (tt === "android_emulator" && params.x != null && params.y != null) {
|
|
3157
|
-
success = await wm.adbTap(params.x, params.y);
|
|
3204
|
+
success = await wm.adbTap(params.x, params.y, deviceId);
|
|
3158
3205
|
}
|
|
3159
3206
|
else if (tt === "ios_simulator" && params.x != null && params.y != null) {
|
|
3160
|
-
|
|
3207
|
+
const r = await sim.iosSimTap(params.x, params.y, iosUdid);
|
|
3208
|
+
success = r.success;
|
|
3209
|
+
detail = `click at (${params.x},${params.y}) — ${r.detail}`;
|
|
3210
|
+
break;
|
|
3161
3211
|
}
|
|
3162
3212
|
else if (params.selector) {
|
|
3163
3213
|
if (process.platform === "win32") {
|
|
@@ -3242,10 +3292,13 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3242
3292
|
success = await bi.browserType(params.selector, params.text);
|
|
3243
3293
|
}
|
|
3244
3294
|
else if (tt === "android_emulator" && params.text) {
|
|
3245
|
-
success = await wm.adbType(params.text);
|
|
3295
|
+
success = await wm.adbType(params.text, deviceId);
|
|
3246
3296
|
}
|
|
3247
3297
|
else if (tt === "ios_simulator" && params.text) {
|
|
3248
|
-
|
|
3298
|
+
const r = await sim.iosSimType(params.text, iosUdid);
|
|
3299
|
+
success = r.success;
|
|
3300
|
+
detail = `type "${(params.text || "").substring(0, 50)}" — ${r.detail}`;
|
|
3301
|
+
break;
|
|
3249
3302
|
}
|
|
3250
3303
|
else if (params.text) {
|
|
3251
3304
|
success = await wm.typeText(params.text);
|
|
@@ -3272,7 +3325,7 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3272
3325
|
// uiautomator hierarchy — lets the agent analyze a Flutter/native
|
|
3273
3326
|
// AI chatbot's answer without OCR. `selector` is treated as a
|
|
3274
3327
|
// text/resource-id substring filter when provided.
|
|
3275
|
-
extractedText = await wm.adbGetText(params.selector);
|
|
3328
|
+
extractedText = await wm.adbGetText(params.selector, deviceId);
|
|
3276
3329
|
success = extractedText !== null && extractedText.length > 0;
|
|
3277
3330
|
const preview = (extractedText ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
3278
3331
|
detail = success
|
|
@@ -3280,11 +3333,14 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3280
3333
|
: "get_text (android) found no visible text — is the emulator booted and the app foregrounded?";
|
|
3281
3334
|
}
|
|
3282
3335
|
else if (tt === "ios_simulator") {
|
|
3283
|
-
//
|
|
3284
|
-
// screenshot via its own vision.
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3336
|
+
// idb (when installed) exposes the accessibility tree; otherwise
|
|
3337
|
+
// the agent reads the answer from a screenshot via its own vision.
|
|
3338
|
+
extractedText = await sim.iosSimGetText(iosUdid);
|
|
3339
|
+
success = extractedText !== null && extractedText.length > 0;
|
|
3340
|
+
const preview = (extractedText ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
3341
|
+
detail = success
|
|
3342
|
+
? `get_text (ios via idb) → "${preview}${(extractedText ?? "").length > 120 ? "…" : ""}"`
|
|
3343
|
+
: "get_text on iOS needs idb (brew install idb-companion && pipx install fb-idb). Without it, call codeloop_capture_screenshot with target_type=\"ios_simulator\" and read the text from the image with your vision.";
|
|
3288
3344
|
}
|
|
3289
3345
|
else {
|
|
3290
3346
|
success = false;
|
|
@@ -3320,22 +3376,22 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3320
3376
|
up: "KEYCODE_DPAD_UP", down: "KEYCODE_DPAD_DOWN",
|
|
3321
3377
|
left: "KEYCODE_DPAD_LEFT", right: "KEYCODE_DPAD_RIGHT",
|
|
3322
3378
|
};
|
|
3323
|
-
success = await wm.adbKey(adbKeyMap[params.key.toLowerCase()] || `KEYCODE_${params.key.toUpperCase()}
|
|
3379
|
+
success = await wm.adbKey(adbKeyMap[params.key.toLowerCase()] || `KEYCODE_${params.key.toUpperCase()}`, deviceId);
|
|
3324
3380
|
}
|
|
3325
3381
|
else if (tt === "ios_simulator") {
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
const mapped = simKeyMap[params.key.toLowerCase()];
|
|
3333
|
-
if (mapped) {
|
|
3334
|
-
success = await wm.simctlKey(mapped);
|
|
3382
|
+
// Named keys (return/tab/arrows/…) go through the shared
|
|
3383
|
+
// key-name map; single printable characters are typed.
|
|
3384
|
+
if (params.key.length === 1 && !/\s/.test(params.key)) {
|
|
3385
|
+
const r = await sim.iosSimType(params.key, iosUdid);
|
|
3386
|
+
success = r.success;
|
|
3387
|
+
detail = `keystroke "${params.key}" — ${r.detail}`;
|
|
3335
3388
|
}
|
|
3336
3389
|
else {
|
|
3337
|
-
|
|
3390
|
+
const r = await sim.iosSimKey(params.key, iosUdid);
|
|
3391
|
+
success = r.success;
|
|
3392
|
+
detail = `keystroke "${params.key}" — ${r.detail}`;
|
|
3338
3393
|
}
|
|
3394
|
+
break;
|
|
3339
3395
|
}
|
|
3340
3396
|
else {
|
|
3341
3397
|
success = await wm.sendKeyByName(params.key);
|
|
@@ -3363,15 +3419,20 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3363
3419
|
const sx = params.x || 540, sy = params.y || 1200;
|
|
3364
3420
|
const ey = dir === "down" ? sy - 600 : dir === "up" ? sy + 600 : sy;
|
|
3365
3421
|
const ex = dir === "left" ? sx + 600 : dir === "right" ? sx - 600 : sx;
|
|
3366
|
-
success = await wm.adbSwipe(sx, sy, ex, ey, 300);
|
|
3422
|
+
success = await wm.adbSwipe(sx, sy, ex, ey, 300, deviceId);
|
|
3367
3423
|
}
|
|
3368
3424
|
else if (tt === "ios_simulator") {
|
|
3369
3425
|
const dir = params.direction || "down";
|
|
3370
|
-
|
|
3371
|
-
|
|
3426
|
+
// Defaults are device PIXELS on a screenshot — centre-ish of a
|
|
3427
|
+
// modern iPhone (~1179×2556).
|
|
3428
|
+
const sx = params.x || 590, sy = params.y || 1280;
|
|
3429
|
+
const dist = params.amount || 600;
|
|
3372
3430
|
const ey = dir === "down" ? sy - dist : dir === "up" ? sy + dist : sy;
|
|
3373
3431
|
const ex = dir === "left" ? sx + dist : dir === "right" ? sx - dist : sx;
|
|
3374
|
-
|
|
3432
|
+
const r = await sim.iosSimSwipe(sx, sy, ex, ey, 300, iosUdid);
|
|
3433
|
+
success = r.success;
|
|
3434
|
+
detail = `scroll ${dir} — ${r.detail}`;
|
|
3435
|
+
break;
|
|
3375
3436
|
}
|
|
3376
3437
|
else {
|
|
3377
3438
|
const t = translateXY(params.x || 500, params.y || 400);
|
|
@@ -3385,7 +3446,13 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3385
3446
|
}
|
|
3386
3447
|
else if (params.x != null && params.y != null && params.x2 != null && params.y2 != null) {
|
|
3387
3448
|
if (tt === "android_emulator") {
|
|
3388
|
-
success = await wm.adbSwipe(params.x, params.y, params.x2, params.y2, params.duration_ms || 500);
|
|
3449
|
+
success = await wm.adbSwipe(params.x, params.y, params.x2, params.y2, params.duration_ms || 500, deviceId);
|
|
3450
|
+
}
|
|
3451
|
+
else if (tt === "ios_simulator") {
|
|
3452
|
+
const r = await sim.iosSimSwipe(params.x, params.y, params.x2, params.y2, params.duration_ms || 500, iosUdid);
|
|
3453
|
+
success = r.success;
|
|
3454
|
+
detail = `drag_drop — ${r.detail}`;
|
|
3455
|
+
break;
|
|
3389
3456
|
}
|
|
3390
3457
|
else {
|
|
3391
3458
|
const a = translateXY(params.x, params.y);
|
|
@@ -3397,7 +3464,13 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3397
3464
|
break;
|
|
3398
3465
|
case "long_press":
|
|
3399
3466
|
if (tt === "android_emulator" && params.x != null && params.y != null) {
|
|
3400
|
-
success = await wm.adbLongPress(params.x, params.y, params.duration_ms || 1000);
|
|
3467
|
+
success = await wm.adbLongPress(params.x, params.y, params.duration_ms || 1000, deviceId);
|
|
3468
|
+
}
|
|
3469
|
+
else if (tt === "ios_simulator" && params.x != null && params.y != null) {
|
|
3470
|
+
const r = await sim.iosSimLongPress(params.x, params.y, params.duration_ms || 1000, iosUdid);
|
|
3471
|
+
success = r.success;
|
|
3472
|
+
detail = `long_press at (${params.x},${params.y}) — ${r.detail}`;
|
|
3473
|
+
break;
|
|
3401
3474
|
}
|
|
3402
3475
|
else if (params.x != null && params.y != null) {
|
|
3403
3476
|
const t = translateXY(params.x, params.y);
|
|
@@ -3409,6 +3482,21 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3409
3482
|
if (tt === "browser" && params.selector && params.text) {
|
|
3410
3483
|
success = await bi.browserTypeAndSubmit(params.selector, params.text);
|
|
3411
3484
|
}
|
|
3485
|
+
else if (tt === "android_emulator" && params.text) {
|
|
3486
|
+
success = await wm.adbType(params.text, deviceId);
|
|
3487
|
+
if (success) {
|
|
3488
|
+
await new Promise(r => setTimeout(r, 100));
|
|
3489
|
+
success = await wm.adbKey("KEYCODE_ENTER", deviceId);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
else if (tt === "ios_simulator" && params.text) {
|
|
3493
|
+
const typed = await sim.iosSimType(params.text, iosUdid);
|
|
3494
|
+
success = typed.success;
|
|
3495
|
+
if (success) {
|
|
3496
|
+
await new Promise(r => setTimeout(r, 100));
|
|
3497
|
+
success = (await sim.iosSimKey("return", iosUdid)).success;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3412
3500
|
else if (params.text) {
|
|
3413
3501
|
success = await wm.typeText(params.text);
|
|
3414
3502
|
if (success) {
|
|
@@ -3422,6 +3510,21 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3422
3510
|
if (tt === "browser" && params.selector && params.text) {
|
|
3423
3511
|
success = await bi.browserTypeAndTab(params.selector, params.text);
|
|
3424
3512
|
}
|
|
3513
|
+
else if (tt === "android_emulator" && params.text) {
|
|
3514
|
+
success = await wm.adbType(params.text, deviceId);
|
|
3515
|
+
if (success) {
|
|
3516
|
+
await new Promise(r => setTimeout(r, 50));
|
|
3517
|
+
success = await wm.adbKey("KEYCODE_TAB", deviceId);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
else if (tt === "ios_simulator" && params.text) {
|
|
3521
|
+
const typed = await sim.iosSimType(params.text, iosUdid);
|
|
3522
|
+
success = typed.success;
|
|
3523
|
+
if (success) {
|
|
3524
|
+
await new Promise(r => setTimeout(r, 50));
|
|
3525
|
+
success = (await sim.iosSimKey("tab", iosUdid)).success;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3425
3528
|
else if (params.text) {
|
|
3426
3529
|
success = await wm.typeText(params.text);
|
|
3427
3530
|
if (success) {
|
|
@@ -3493,10 +3596,10 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3493
3596
|
success = await bi.browserNavigate(params.url);
|
|
3494
3597
|
}
|
|
3495
3598
|
else if (tt === "android_emulator") {
|
|
3496
|
-
success = await wm.adbDeepLink(params.url);
|
|
3599
|
+
success = await wm.adbDeepLink(params.url, deviceId);
|
|
3497
3600
|
}
|
|
3498
3601
|
else if (tt === "ios_simulator") {
|
|
3499
|
-
success = await wm.simctlOpenUrl(params.url);
|
|
3602
|
+
success = await wm.simctlOpenUrl(params.url, iosUdid);
|
|
3500
3603
|
}
|
|
3501
3604
|
else {
|
|
3502
3605
|
const { navigateDesktopBrowser } = await import("./runners/window_manager.js");
|
|
@@ -3507,7 +3610,7 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3507
3610
|
break;
|
|
3508
3611
|
case "navigate_back":
|
|
3509
3612
|
if (tt === "android_emulator") {
|
|
3510
|
-
success = await wm.adbBackButton();
|
|
3613
|
+
success = await wm.adbBackButton(deviceId);
|
|
3511
3614
|
}
|
|
3512
3615
|
else if (tt === "browser") {
|
|
3513
3616
|
success = await bi.browserGoBack();
|
|
@@ -3535,7 +3638,13 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3535
3638
|
break;
|
|
3536
3639
|
case "swipe":
|
|
3537
3640
|
if (tt === "android_emulator" && params.x != null && params.y != null && params.x2 != null && params.y2 != null) {
|
|
3538
|
-
success = await wm.adbSwipe(params.x, params.y, params.x2, params.y2, params.duration_ms || 300);
|
|
3641
|
+
success = await wm.adbSwipe(params.x, params.y, params.x2, params.y2, params.duration_ms || 300, deviceId);
|
|
3642
|
+
}
|
|
3643
|
+
else if (tt === "ios_simulator" && params.x != null && params.y != null && params.x2 != null && params.y2 != null) {
|
|
3644
|
+
const r = await sim.iosSimSwipe(params.x, params.y, params.x2, params.y2, params.duration_ms || 300, iosUdid);
|
|
3645
|
+
success = r.success;
|
|
3646
|
+
detail = `swipe from (${params.x},${params.y}) to (${params.x2},${params.y2}) — ${r.detail}`;
|
|
3647
|
+
break;
|
|
3539
3648
|
}
|
|
3540
3649
|
else if (params.x != null && params.y != null && params.x2 != null && params.y2 != null) {
|
|
3541
3650
|
success = await wm.dragDrop(params.x, params.y, params.x2, params.y2, params.duration_ms || 300);
|
|
@@ -3544,49 +3653,67 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3544
3653
|
break;
|
|
3545
3654
|
case "back_button":
|
|
3546
3655
|
if (tt === "android_emulator")
|
|
3547
|
-
success = await wm.adbBackButton();
|
|
3656
|
+
success = await wm.adbBackButton(deviceId);
|
|
3548
3657
|
detail = "back_button";
|
|
3549
3658
|
break;
|
|
3550
3659
|
case "home_button":
|
|
3551
3660
|
if (tt === "android_emulator")
|
|
3552
|
-
success = await wm.adbHomeButton();
|
|
3661
|
+
success = await wm.adbHomeButton(deviceId);
|
|
3553
3662
|
detail = "home_button";
|
|
3554
3663
|
break;
|
|
3555
3664
|
case "deep_link":
|
|
3556
3665
|
if (params.url) {
|
|
3557
3666
|
if (tt === "android_emulator")
|
|
3558
|
-
success = await wm.adbDeepLink(params.url);
|
|
3667
|
+
success = await wm.adbDeepLink(params.url, deviceId);
|
|
3559
3668
|
else if (tt === "ios_simulator")
|
|
3560
|
-
success = await wm.simctlOpenUrl(params.url);
|
|
3669
|
+
success = await wm.simctlOpenUrl(params.url, iosUdid);
|
|
3561
3670
|
}
|
|
3562
3671
|
detail = `deep_link "${params.url}"`;
|
|
3563
3672
|
break;
|
|
3564
3673
|
case "grant_permission":
|
|
3565
3674
|
if (tt === "android_emulator" && params.package_id && params.value) {
|
|
3566
|
-
success = await wm.adbPermission(params.package_id, params.value, params.grant !== false);
|
|
3675
|
+
success = await wm.adbPermission(params.package_id, params.value, params.grant !== false, deviceId);
|
|
3676
|
+
detail = `grant_permission "${params.value}"`;
|
|
3677
|
+
}
|
|
3678
|
+
else if (tt === "ios_simulator" && params.package_id && params.value) {
|
|
3679
|
+
// P1.2: simctl privacy grant|revoke <service> <bundle>
|
|
3680
|
+
success = await wm.simctlPermission(params.package_id, params.value, params.grant !== false, iosUdid);
|
|
3681
|
+
detail = `grant_permission "${params.value}" → simctl privacy ${params.grant !== false ? "grant" : "revoke"} (services: photos, location, microphone, contacts, calendar, …)`;
|
|
3682
|
+
}
|
|
3683
|
+
else {
|
|
3684
|
+
detail = `grant_permission "${params.value}" — needs package_id + value (permission/service name)`;
|
|
3567
3685
|
}
|
|
3568
|
-
detail = `grant_permission "${params.value}"`;
|
|
3569
3686
|
break;
|
|
3570
3687
|
case "rotate_device":
|
|
3571
3688
|
if (tt === "android_emulator") {
|
|
3572
|
-
success = await wm.adbRotate(params.orientation === "landscape");
|
|
3689
|
+
success = await wm.adbRotate(params.orientation === "landscape", deviceId);
|
|
3690
|
+
detail = `rotate_device ${params.orientation}`;
|
|
3691
|
+
}
|
|
3692
|
+
else if (tt === "ios_simulator") {
|
|
3693
|
+
// P1.2: no simctl rotation exists — drive the Simulator app's
|
|
3694
|
+
// Device menu shortcut (Cmd+Left/Right) via CGEvent.
|
|
3695
|
+
const r = await sim.iosSimRotate(params.orientation === "landscape" ? "landscape" : "portrait", iosUdid);
|
|
3696
|
+
success = r.success;
|
|
3697
|
+
detail = `rotate_device ${params.orientation} — ${r.detail}`;
|
|
3698
|
+
}
|
|
3699
|
+
else {
|
|
3700
|
+
detail = `rotate_device ${params.orientation}`;
|
|
3573
3701
|
}
|
|
3574
|
-
detail = `rotate_device ${params.orientation}`;
|
|
3575
3702
|
break;
|
|
3576
3703
|
case "biometric_auth":
|
|
3577
3704
|
if (tt === "ios_simulator") {
|
|
3578
|
-
success = await wm.simctlBiometric(params.accept !== false);
|
|
3705
|
+
success = await wm.simctlBiometric(params.accept !== false, iosUdid);
|
|
3579
3706
|
}
|
|
3580
3707
|
detail = `biometric_auth ${params.accept !== false ? "accept" : "reject"}`;
|
|
3581
3708
|
break;
|
|
3582
3709
|
case "launch_app":
|
|
3583
3710
|
if (tt === "android_emulator" && params.package_id) {
|
|
3584
|
-
const r = await import("./runners/base.js").then(m => m.runCommand("adb",
|
|
3711
|
+
const r = await import("./runners/base.js").then(m => m.runCommand("adb", wm.adbArgs(deviceId, "shell", "am", "start", "-n", params.package_id), process.cwd()));
|
|
3585
3712
|
success = r.exit_code === 0;
|
|
3586
3713
|
detail = `launch_app "${params.package_id}"`;
|
|
3587
3714
|
}
|
|
3588
3715
|
else if (tt === "ios_simulator" && params.package_id) {
|
|
3589
|
-
success = await wm.simctlLaunch(params.package_id);
|
|
3716
|
+
success = await wm.simctlLaunch(params.package_id, iosUdid);
|
|
3590
3717
|
detail = `launch_app "${params.package_id}"`;
|
|
3591
3718
|
}
|
|
3592
3719
|
else if (tt === "desktop") {
|
|
@@ -3617,21 +3744,83 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3617
3744
|
break;
|
|
3618
3745
|
case "clear_app_data":
|
|
3619
3746
|
if (tt === "android_emulator" && params.package_id) {
|
|
3620
|
-
success = await wm.adbClearData(params.package_id);
|
|
3747
|
+
success = await wm.adbClearData(params.package_id, deviceId);
|
|
3748
|
+
detail = `clear_app_data "${params.package_id}"`;
|
|
3749
|
+
}
|
|
3750
|
+
else if (tt === "ios_simulator" && params.package_id) {
|
|
3751
|
+
// P1.2: iOS has no `pm clear` — uninstall wipes data AND the
|
|
3752
|
+
// binary, so the app must be reinstalled before the next step.
|
|
3753
|
+
success = await wm.simctlUninstall(params.package_id, iosUdid);
|
|
3754
|
+
detail = success
|
|
3755
|
+
? `clear_app_data "${params.package_id}": app uninstalled (data wiped). REINSTALL required — re-run codeloop_run_journey or \`flutter run\`/xcodebuild install before interacting again.`
|
|
3756
|
+
: `clear_app_data "${params.package_id}": simctl uninstall failed — is the bundle id correct and the simulator booted?`;
|
|
3757
|
+
}
|
|
3758
|
+
else {
|
|
3759
|
+
detail = `clear_app_data "${params.package_id}"`;
|
|
3621
3760
|
}
|
|
3622
|
-
detail = `clear_app_data "${params.package_id}"`;
|
|
3623
3761
|
break;
|
|
3624
3762
|
case "mock_location":
|
|
3625
3763
|
if (tt === "android_emulator" && params.latitude != null && params.longitude != null) {
|
|
3626
|
-
|
|
3764
|
+
// P1.3: `adb emu geo` talks to the EMULATOR CONSOLE — it can
|
|
3765
|
+
// never work on physical hardware, so fail with a usable
|
|
3766
|
+
// directive instead of a cryptic adb error.
|
|
3767
|
+
if (!wm.isEmulatorSerial(deviceId)) {
|
|
3768
|
+
success = false;
|
|
3769
|
+
detail = `mock_location is emulator-only (\`adb emu geo\` drives the emulator console; "${deviceId}" is a physical device). On hardware: enable Developer options → "Select mock location app" and use a mock-location app, or test this journey on an emulator.`;
|
|
3770
|
+
break;
|
|
3771
|
+
}
|
|
3772
|
+
success = await wm.adbMockLocation(params.latitude, params.longitude, deviceId);
|
|
3773
|
+
}
|
|
3774
|
+
else if (tt === "ios_simulator" && params.latitude != null && params.longitude != null) {
|
|
3775
|
+
// P1.2: simctl location <udid> set <lat>,<lon>
|
|
3776
|
+
success = await wm.simctlSetLocation(params.latitude, params.longitude, iosUdid);
|
|
3627
3777
|
}
|
|
3628
3778
|
detail = `mock_location (${params.latitude},${params.longitude})`;
|
|
3629
3779
|
break;
|
|
3630
3780
|
case "simulate_network":
|
|
3631
3781
|
if (tt === "android_emulator" && params.value) {
|
|
3632
|
-
|
|
3782
|
+
// P1.3: `adb emu network` is emulator-console-only as well.
|
|
3783
|
+
if (!wm.isEmulatorSerial(deviceId)) {
|
|
3784
|
+
success = false;
|
|
3785
|
+
detail = `simulate_network is emulator-only (\`adb emu network\` drives the emulator console; "${deviceId}" is a physical device). On hardware: toggle airplane mode / Wi-Fi via adb shell, or test this journey on an emulator.`;
|
|
3786
|
+
break;
|
|
3787
|
+
}
|
|
3788
|
+
success = await wm.adbNetworkCondition(params.value, deviceId);
|
|
3789
|
+
detail = `simulate_network "${params.value}"`;
|
|
3790
|
+
}
|
|
3791
|
+
else if (tt === "ios_simulator") {
|
|
3792
|
+
// Honest unsupported: simctl has no network conditioner; the
|
|
3793
|
+
// status_bar override only changes the INDICATOR, not traffic.
|
|
3794
|
+
detail = `simulate_network is not supported on the iOS simulator (simctl has no network conditioner). Install Apple's "Network Link Conditioner" prefpane for host-level shaping, or use action "status_bar" to fake the indicator for screenshots.`;
|
|
3795
|
+
}
|
|
3796
|
+
else {
|
|
3797
|
+
detail = `simulate_network "${params.value}"`;
|
|
3798
|
+
}
|
|
3799
|
+
break;
|
|
3800
|
+
case "push_notification":
|
|
3801
|
+
// P1.2: simulated APNs push — iOS simulator only.
|
|
3802
|
+
if (tt === "ios_simulator" && params.package_id && params.value) {
|
|
3803
|
+
const r = await wm.simctlPush(params.package_id, params.value, iosUdid);
|
|
3804
|
+
success = r.success;
|
|
3805
|
+
detail = `push_notification → ${r.detail}`;
|
|
3806
|
+
}
|
|
3807
|
+
else if (tt === "android_emulator") {
|
|
3808
|
+
detail = `push_notification is iOS-simulator-only (simctl push). On Android, drive the app's own notification trigger or use \`adb shell am broadcast\` for app-specific receivers.`;
|
|
3809
|
+
}
|
|
3810
|
+
else {
|
|
3811
|
+
detail = `push_notification needs package_id (bundle id) + value (APNs JSON payload or plain alert text).`;
|
|
3812
|
+
}
|
|
3813
|
+
break;
|
|
3814
|
+
case "status_bar":
|
|
3815
|
+
// P1.2: simctl status_bar override for demo-clean screenshots.
|
|
3816
|
+
if (tt === "ios_simulator") {
|
|
3817
|
+
const r = await wm.simctlStatusBar(params.value ?? "", iosUdid);
|
|
3818
|
+
success = r.success;
|
|
3819
|
+
detail = `status_bar → ${r.detail}`;
|
|
3820
|
+
}
|
|
3821
|
+
else {
|
|
3822
|
+
detail = `status_bar is iOS-simulator-only (simctl status_bar). Value: JSON or "key=value,…" with time, dataNetwork, wifiMode, wifiBars, cellularMode, cellularBars, operatorName, batteryState, batteryLevel — or "clear".`;
|
|
3633
3823
|
}
|
|
3634
|
-
detail = `simulate_network "${params.value}"`;
|
|
3635
3824
|
break;
|
|
3636
3825
|
case "maestro_flow":
|
|
3637
3826
|
if (params.maestro_steps) {
|
|
@@ -3641,7 +3830,7 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3641
3830
|
if ("error" in genResult) {
|
|
3642
3831
|
return { success: false, action, detail: genResult.error };
|
|
3643
3832
|
}
|
|
3644
|
-
const runResult = await mg.runGeneratedFlow(genResult.flowPath, cwd);
|
|
3833
|
+
const runResult = await mg.runGeneratedFlow(genResult.flowPath, cwd, deviceId);
|
|
3645
3834
|
success = runResult.success;
|
|
3646
3835
|
detail = `maestro_flow (${params.maestro_steps.length} steps) → ${runResult.success ? "passed" : runResult.error}`;
|
|
3647
3836
|
}
|
|
@@ -3813,7 +4002,7 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3813
4002
|
}
|
|
3814
4003
|
break;
|
|
3815
4004
|
default:
|
|
3816
|
-
detail = `Unknown action: "${action}". Available: click, double_click, right_click, hover, type, keystroke, hotkey, scroll, drag_drop, long_press, type_and_submit, type_and_tab, fill_form, select_option, toggle, upload_file, navigate_url, navigate_back, navigate_forward, wait, swipe, back_button, home_button, deep_link, grant_permission, rotate_device, biometric_auth, launch_app, clear_app_data, mock_location, simulate_network, maestro_flow, win_ui_inspect, win_ui_automate, sequence`;
|
|
4005
|
+
detail = `Unknown action: "${action}". Available: click, double_click, right_click, hover, type, keystroke, hotkey, scroll, drag_drop, long_press, type_and_submit, type_and_tab, fill_form, select_option, toggle, upload_file, navigate_url, navigate_back, navigate_forward, wait, swipe, back_button, home_button, deep_link, grant_permission, rotate_device, biometric_auth, launch_app, clear_app_data, mock_location, simulate_network, push_notification, status_bar, maestro_flow, win_ui_inspect, win_ui_automate, sequence`;
|
|
3817
4006
|
return { success: false, action, detail };
|
|
3818
4007
|
}
|
|
3819
4008
|
await trackUsage(apiKey, "interaction");
|