agent-device 0.2.4 → 0.2.5

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.
Files changed (46) hide show
  1. package/README.md +41 -4
  2. package/dist/src/bin.js +26 -21
  3. package/dist/src/daemon.js +9 -8
  4. package/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +2 -0
  5. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +15 -0
  6. package/package.json +3 -2
  7. package/skills/agent-device/SKILL.md +22 -6
  8. package/skills/agent-device/references/session-management.md +9 -0
  9. package/skills/agent-device/references/snapshot-refs.md +18 -5
  10. package/skills/agent-device/references/video-recording.md +2 -2
  11. package/src/cli.ts +6 -0
  12. package/src/core/__tests__/capabilities.test.ts +67 -0
  13. package/src/core/capabilities.ts +49 -0
  14. package/src/core/dispatch.ts +29 -118
  15. package/src/daemon/__tests__/is-predicates.test.ts +68 -0
  16. package/src/daemon/__tests__/selectors.test.ts +128 -0
  17. package/src/daemon/__tests__/session-routing.test.ts +108 -0
  18. package/src/daemon/__tests__/session-selector.test.ts +64 -0
  19. package/src/daemon/__tests__/session-store.test.ts +95 -0
  20. package/src/daemon/__tests__/snapshot-processing.test.ts +47 -0
  21. package/src/daemon/action-utils.ts +29 -0
  22. package/src/daemon/app-state.ts +66 -0
  23. package/src/daemon/context.ts +36 -0
  24. package/src/daemon/device-ready.ts +13 -0
  25. package/src/daemon/handlers/__tests__/find.test.ts +99 -0
  26. package/src/daemon/handlers/__tests__/replay-heal.test.ts +364 -0
  27. package/src/daemon/handlers/__tests__/snapshot.test.ts +128 -0
  28. package/src/daemon/handlers/find.ts +304 -0
  29. package/src/daemon/handlers/interaction.ts +510 -0
  30. package/src/daemon/handlers/parse-utils.ts +8 -0
  31. package/src/daemon/handlers/record-trace.ts +154 -0
  32. package/src/daemon/handlers/session.ts +732 -0
  33. package/src/daemon/handlers/snapshot.ts +396 -0
  34. package/src/daemon/is-predicates.ts +46 -0
  35. package/src/daemon/selectors.ts +423 -0
  36. package/src/daemon/session-routing.ts +22 -0
  37. package/src/daemon/session-selector.ts +39 -0
  38. package/src/daemon/session-store.ts +275 -0
  39. package/src/daemon/snapshot-processing.ts +127 -0
  40. package/src/daemon/types.ts +55 -0
  41. package/src/daemon.ts +66 -1592
  42. package/src/platforms/ios/index.ts +0 -62
  43. package/src/platforms/ios/runner-client.ts +2 -0
  44. package/src/utils/args.ts +19 -10
  45. package/src/utils/interactors.ts +102 -16
  46. package/src/utils/snapshot.ts +1 -0
@@ -83,68 +83,6 @@ export async function closeIosApp(device: DeviceInfo, app: string): Promise<void
83
83
  ]);
84
84
  }
85
85
 
86
- export async function pressIos(device: DeviceInfo, x: number, y: number): Promise<void> {
87
- ensureSimulator(device, 'press');
88
- throw new AppError(
89
- 'UNSUPPORTED_OPERATION',
90
- 'simctl io tap is not available; use the XCTest runner for input',
91
- );
92
- }
93
-
94
- export async function longPressIos(
95
- device: DeviceInfo,
96
- x: number,
97
- y: number,
98
- durationMs = 800,
99
- ): Promise<void> {
100
- ensureSimulator(device, 'long-press');
101
- throw new AppError(
102
- 'UNSUPPORTED_OPERATION',
103
- 'long-press is not supported on iOS simulators without XCTest runner support',
104
- );
105
- }
106
-
107
- export async function focusIos(device: DeviceInfo, x: number, y: number): Promise<void> {
108
- await pressIos(device, x, y);
109
- }
110
-
111
- export async function typeIos(device: DeviceInfo, text: string): Promise<void> {
112
- ensureSimulator(device, 'type');
113
- throw new AppError(
114
- 'UNSUPPORTED_OPERATION',
115
- 'simctl io keyboard is not available; use the XCTest runner for input',
116
- );
117
- }
118
-
119
- export async function fillIos(
120
- device: DeviceInfo,
121
- x: number,
122
- y: number,
123
- text: string,
124
- ): Promise<void> {
125
- await focusIos(device, x, y);
126
- await typeIos(device, text);
127
- }
128
-
129
- export async function scrollIos(
130
- device: DeviceInfo,
131
- direction: string,
132
- amount = 0.6,
133
- ): Promise<void> {
134
- ensureSimulator(device, 'scroll');
135
- throw new AppError(
136
- 'UNSUPPORTED_OPERATION',
137
- 'simctl io swipe is not available; use the XCTest runner for input',
138
- );
139
- }
140
-
141
- export async function scrollIntoViewIos(text: string): Promise<void> {
142
- throw new AppError(
143
- 'UNSUPPORTED_OPERATION',
144
- `scrollintoview is not supported on iOS without UI automation (${text})`,
145
- );
146
- }
147
-
148
86
  export async function screenshotIos(device: DeviceInfo, outPath: string): Promise<void> {
149
87
  if (device.kind === 'simulator') {
150
88
  await ensureBootedSimulator(device);
@@ -11,6 +11,7 @@ import net from 'node:net';
11
11
  export type RunnerCommand = {
12
12
  command:
13
13
  | 'tap'
14
+ | 'longPress'
14
15
  | 'type'
15
16
  | 'swipe'
16
17
  | 'findText'
@@ -27,6 +28,7 @@ export type RunnerCommand = {
27
28
  action?: 'get' | 'accept' | 'dismiss';
28
29
  x?: number;
29
30
  y?: number;
31
+ durationMs?: number;
30
32
  direction?: 'up' | 'down' | 'left' | 'right';
31
33
  scale?: number;
32
34
  interactiveOnly?: boolean;
package/src/utils/args.ts CHANGED
@@ -21,8 +21,9 @@ export type ParsedArgs = {
21
21
  appsFilter?: 'launchable' | 'user-installed' | 'all';
22
22
  appsMetadata?: boolean;
23
23
  activity?: string;
24
+ saveScript?: boolean;
24
25
  noRecord?: boolean;
25
- recordJson?: boolean;
26
+ replayUpdate?: boolean;
26
27
  help: boolean;
27
28
  };
28
29
  };
@@ -61,8 +62,12 @@ export function parseArgs(argv: string[]): ParsedArgs {
61
62
  flags.noRecord = true;
62
63
  continue;
63
64
  }
64
- if (arg === '--record-json') {
65
- flags.recordJson = true;
65
+ if (arg === '--save-script') {
66
+ flags.saveScript = true;
67
+ continue;
68
+ }
69
+ if (arg === '--update' || arg === '-u') {
70
+ flags.replayUpdate = true;
66
71
  continue;
67
72
  }
68
73
  if (arg === '--user-installed') {
@@ -180,17 +185,19 @@ Commands:
180
185
  back Navigate back (where supported)
181
186
  home Go to home screen (where supported)
182
187
  app-switcher Open app switcher (where supported)
183
- wait <ms>|text <text>|@ref [timeoutMs] Wait for duration or text to appear
188
+ wait <ms>|text <text>|@ref|<selector> [timeoutMs]
189
+ Wait for duration, text, ref, or selector to appear
184
190
  alert [get|accept|dismiss|wait] [timeout] Inspect or handle alert (iOS simulator)
185
- click <@ref> Click element by snapshot ref
186
- get text <@ref> Return element text by ref
187
- get attrs <@ref> Return element attributes by ref
188
- replay <path> Replay a recorded session
191
+ click <@ref|selector> Click element by snapshot ref or selector
192
+ get text <@ref|selector> Return element text by ref or selector
193
+ get attrs <@ref|selector> Return element attributes by ref or selector
194
+ replay <path> [--update|-u] Replay a recorded session
189
195
  press <x> <y> Tap at coordinates
190
196
  long-press <x> <y> [durationMs] Long press (where supported)
191
197
  focus <x> <y> Focus input at coordinates
192
198
  type <text> Type text in focused field
193
- fill <x> <y> <text> | fill <@ref> <text> Tap then type
199
+ fill <x> <y> <text> | fill <@ref|selector> <text>
200
+ Tap then type
194
201
  scroll <direction> [amount] Scroll in direction (0-1 amount)
195
202
  scrollintoview <text> Scroll until text appears (Android only)
196
203
  screenshot [path] Capture screenshot
@@ -204,6 +211,7 @@ Commands:
204
211
  find value <value> <action> [value] Find by value
205
212
  find role <role> <action> [value] Find by role/type
206
213
  find id <id> <action> [value] Find by identifier/resource-id
214
+ is <predicate> <selector> [value] Assert UI state (visible|hidden|exists|editable|selected|text)
207
215
  settings <wifi|airplane|location> <on|off> Toggle OS settings (simulators)
208
216
  session list List active sessions
209
217
 
@@ -216,8 +224,9 @@ Flags:
216
224
  --session <name> Named session
217
225
  --verbose Stream daemon/runner logs
218
226
  --json JSON output
227
+ --save-script Save session script (.ad) on close
219
228
  --no-record Do not record this action
220
- --record-json Record JSON session log
229
+ --update, -u Replay: update selectors and rewrite replay file in place
221
230
  --user-installed Apps: list user-installed packages (Android only)
222
231
  --all Apps: list all packages (Android only)
223
232
  `;
@@ -15,17 +15,18 @@ import {
15
15
  } from '../platforms/android/index.ts';
16
16
  import {
17
17
  closeIosApp,
18
- fillIos,
19
- focusIos,
20
- longPressIos,
21
18
  openIosApp,
22
19
  openIosDevice,
23
- pressIos,
24
- scrollIos,
25
- scrollIntoViewIos,
26
20
  screenshotIos,
27
- typeIos,
28
21
  } from '../platforms/ios/index.ts';
22
+ import { runIosRunnerCommand } from '../platforms/ios/runner-client.ts';
23
+
24
+ export type RunnerContext = {
25
+ appBundleId?: string;
26
+ verbose?: boolean;
27
+ logPath?: string;
28
+ traceLogPath?: string;
29
+ };
29
30
 
30
31
  export type Interactor = {
31
32
  open(app: string, options?: { activity?: string }): Promise<void>;
@@ -37,11 +38,11 @@ export type Interactor = {
37
38
  type(text: string): Promise<void>;
38
39
  fill(x: number, y: number, text: string): Promise<void>;
39
40
  scroll(direction: string, amount?: number): Promise<void>;
40
- scrollIntoView(text: string): Promise<void>;
41
+ scrollIntoView(text: string): Promise<{ attempts?: number } | void>;
41
42
  screenshot(outPath: string): Promise<void>;
42
43
  };
43
44
 
44
- export function getInteractor(device: DeviceInfo): Interactor {
45
+ export function getInteractor(device: DeviceInfo, runnerContext: RunnerContext): Interactor {
45
46
  switch (device.platform) {
46
47
  case 'android':
47
48
  return {
@@ -62,16 +63,101 @@ export function getInteractor(device: DeviceInfo): Interactor {
62
63
  open: (app) => openIosApp(device, app),
63
64
  openDevice: () => openIosDevice(device),
64
65
  close: (app) => closeIosApp(device, app),
65
- tap: (x, y) => pressIos(device, x, y),
66
- longPress: (x, y, durationMs) => longPressIos(device, x, y, durationMs),
67
- focus: (x, y) => focusIos(device, x, y),
68
- type: (text) => typeIos(device, text),
69
- fill: (x, y, text) => fillIos(device, x, y, text),
70
- scroll: (direction, amount) => scrollIos(device, direction, amount),
71
- scrollIntoView: (text) => scrollIntoViewIos(text),
72
66
  screenshot: (outPath) => screenshotIos(device, outPath),
67
+ ...iosRunnerOverrides(device, runnerContext),
73
68
  };
74
69
  default:
75
70
  throw new AppError('UNSUPPORTED_PLATFORM', `Unsupported platform: ${device.platform}`);
76
71
  }
77
72
  }
73
+
74
+ type IoRunnerOverrides = Pick<Interactor, 'tap' | 'longPress' | 'focus' | 'type' | 'fill' | 'scroll' | 'scrollIntoView'>;
75
+
76
+ function iosRunnerOverrides(device: DeviceInfo, ctx: RunnerContext): IoRunnerOverrides {
77
+ const runnerOpts = { verbose: ctx.verbose, logPath: ctx.logPath, traceLogPath: ctx.traceLogPath };
78
+
79
+ return {
80
+ tap: async (x, y) => {
81
+ await runIosRunnerCommand(
82
+ device,
83
+ { command: 'tap', x, y, appBundleId: ctx.appBundleId },
84
+ runnerOpts,
85
+ );
86
+ },
87
+ longPress: async (x, y, durationMs) => {
88
+ await runIosRunnerCommand(
89
+ device,
90
+ { command: 'longPress', x, y, durationMs, appBundleId: ctx.appBundleId },
91
+ runnerOpts,
92
+ );
93
+ },
94
+ focus: async (x, y) => {
95
+ await runIosRunnerCommand(
96
+ device,
97
+ { command: 'tap', x, y, appBundleId: ctx.appBundleId },
98
+ runnerOpts,
99
+ );
100
+ },
101
+ type: async (text) => {
102
+ await runIosRunnerCommand(
103
+ device,
104
+ { command: 'type', text, appBundleId: ctx.appBundleId },
105
+ runnerOpts,
106
+ );
107
+ },
108
+ fill: async (x, y, text) => {
109
+ await runIosRunnerCommand(
110
+ device,
111
+ { command: 'tap', x, y, appBundleId: ctx.appBundleId },
112
+ runnerOpts,
113
+ );
114
+ await runIosRunnerCommand(
115
+ device,
116
+ { command: 'type', text, clearFirst: true, appBundleId: ctx.appBundleId },
117
+ runnerOpts,
118
+ );
119
+ },
120
+ scroll: async (direction, _amount) => {
121
+ if (!['up', 'down', 'left', 'right'].includes(direction)) {
122
+ throw new AppError('INVALID_ARGS', `Unknown direction: ${direction}`);
123
+ }
124
+ const inverted = invertScrollDirection(direction as 'up' | 'down' | 'left' | 'right');
125
+ await runIosRunnerCommand(
126
+ device,
127
+ { command: 'swipe', direction: inverted, appBundleId: ctx.appBundleId },
128
+ runnerOpts,
129
+ );
130
+ },
131
+ scrollIntoView: async (text) => {
132
+ const maxAttempts = 8;
133
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
134
+ const found = (await runIosRunnerCommand(
135
+ device,
136
+ { command: 'findText', text, appBundleId: ctx.appBundleId },
137
+ runnerOpts,
138
+ )) as { found?: boolean };
139
+ if (found?.found) return { attempts: attempt + 1 };
140
+ await runIosRunnerCommand(
141
+ device,
142
+ { command: 'swipe', direction: 'up', appBundleId: ctx.appBundleId },
143
+ runnerOpts,
144
+ );
145
+ await new Promise((resolve) => setTimeout(resolve, 300));
146
+ }
147
+ throw new AppError('COMMAND_FAILED', `scrollintoview could not find text: ${text}`);
148
+ },
149
+ };
150
+ }
151
+
152
+ function invertScrollDirection(direction: 'up' | 'down' | 'left' | 'right'): 'up' | 'down' | 'left' | 'right' {
153
+ switch (direction) {
154
+ case 'up':
155
+ return 'down';
156
+ case 'down':
157
+ return 'up';
158
+ case 'left':
159
+ return 'right';
160
+ case 'right':
161
+ return 'left';
162
+ }
163
+ }
@@ -21,6 +21,7 @@ export type RawSnapshotNode = {
21
21
  identifier?: string;
22
22
  rect?: Rect;
23
23
  enabled?: boolean;
24
+ selected?: boolean;
24
25
  hittable?: boolean;
25
26
  depth?: number;
26
27
  parentIndex?: number;