agent-device 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +2 -9
  2. package/dist/src/797.js +1 -1
  3. package/dist/src/bin.js +5 -5
  4. package/dist/src/daemon.js +16 -20
  5. package/package.json +7 -9
  6. package/skills/agent-device/SKILL.md +3 -6
  7. package/skills/agent-device/references/permissions.md +3 -15
  8. package/skills/agent-device/references/snapshot-refs.md +1 -4
  9. package/dist/bin/axsnapshot +0 -0
  10. package/ios-runner/AXSnapshot/Package.swift +0 -18
  11. package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
  12. package/src/__tests__/cli-close.test.ts +0 -155
  13. package/src/__tests__/cli-help.test.ts +0 -102
  14. package/src/bin.ts +0 -3
  15. package/src/cli.ts +0 -305
  16. package/src/core/__tests__/capabilities.test.ts +0 -75
  17. package/src/core/__tests__/dispatch-open.test.ts +0 -25
  18. package/src/core/__tests__/open-target.test.ts +0 -55
  19. package/src/core/capabilities.ts +0 -57
  20. package/src/core/dispatch.ts +0 -382
  21. package/src/core/open-target.ts +0 -27
  22. package/src/daemon/__tests__/device-ready.test.ts +0 -52
  23. package/src/daemon/__tests__/is-predicates.test.ts +0 -68
  24. package/src/daemon/__tests__/selectors.test.ts +0 -261
  25. package/src/daemon/__tests__/session-routing.test.ts +0 -108
  26. package/src/daemon/__tests__/session-selector.test.ts +0 -64
  27. package/src/daemon/__tests__/session-store.test.ts +0 -142
  28. package/src/daemon/__tests__/snapshot-processing.test.ts +0 -47
  29. package/src/daemon/action-utils.ts +0 -29
  30. package/src/daemon/context.ts +0 -48
  31. package/src/daemon/device-ready.ts +0 -155
  32. package/src/daemon/handlers/__tests__/find.test.ts +0 -99
  33. package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
  34. package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
  35. package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
  36. package/src/daemon/handlers/__tests__/session.test.ts +0 -820
  37. package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
  38. package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
  39. package/src/daemon/handlers/find.ts +0 -324
  40. package/src/daemon/handlers/interaction.ts +0 -550
  41. package/src/daemon/handlers/parse-utils.ts +0 -8
  42. package/src/daemon/handlers/record-trace.ts +0 -154
  43. package/src/daemon/handlers/session.ts +0 -1137
  44. package/src/daemon/handlers/snapshot.ts +0 -439
  45. package/src/daemon/is-predicates.ts +0 -46
  46. package/src/daemon/selectors.ts +0 -540
  47. package/src/daemon/session-routing.ts +0 -22
  48. package/src/daemon/session-selector.ts +0 -39
  49. package/src/daemon/session-store.ts +0 -296
  50. package/src/daemon/snapshot-processing.ts +0 -131
  51. package/src/daemon/types.ts +0 -56
  52. package/src/daemon-client.ts +0 -272
  53. package/src/daemon.ts +0 -295
  54. package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
  55. package/src/platforms/android/__tests__/index.test.ts +0 -274
  56. package/src/platforms/android/devices.ts +0 -196
  57. package/src/platforms/android/index.ts +0 -784
  58. package/src/platforms/android/ui-hierarchy.ts +0 -312
  59. package/src/platforms/boot-diagnostics.ts +0 -128
  60. package/src/platforms/ios/__tests__/index.test.ts +0 -312
  61. package/src/platforms/ios/__tests__/runner-client.test.ts +0 -155
  62. package/src/platforms/ios/apps.ts +0 -358
  63. package/src/platforms/ios/ax-snapshot.ts +0 -207
  64. package/src/platforms/ios/config.ts +0 -28
  65. package/src/platforms/ios/devicectl.ts +0 -134
  66. package/src/platforms/ios/devices.ts +0 -100
  67. package/src/platforms/ios/index.ts +0 -20
  68. package/src/platforms/ios/runner-client.ts +0 -994
  69. package/src/platforms/ios/simulator.ts +0 -164
  70. package/src/utils/__tests__/args.test.ts +0 -239
  71. package/src/utils/__tests__/daemon-client.test.ts +0 -95
  72. package/src/utils/__tests__/exec.test.ts +0 -16
  73. package/src/utils/__tests__/finders.test.ts +0 -34
  74. package/src/utils/__tests__/keyed-lock.test.ts +0 -55
  75. package/src/utils/__tests__/process-identity.test.ts +0 -33
  76. package/src/utils/__tests__/retry.test.ts +0 -44
  77. package/src/utils/args.ts +0 -239
  78. package/src/utils/command-schema.ts +0 -622
  79. package/src/utils/device.ts +0 -84
  80. package/src/utils/errors.ts +0 -35
  81. package/src/utils/exec.ts +0 -339
  82. package/src/utils/finders.ts +0 -101
  83. package/src/utils/interactive.ts +0 -4
  84. package/src/utils/interactors.ts +0 -173
  85. package/src/utils/keyed-lock.ts +0 -14
  86. package/src/utils/output.ts +0 -204
  87. package/src/utils/process-identity.ts +0 -100
  88. package/src/utils/retry.ts +0 -180
  89. package/src/utils/snapshot.ts +0 -64
  90. package/src/utils/timeouts.ts +0 -9
  91. package/src/utils/version.ts +0 -26
@@ -1,312 +0,0 @@
1
- import type { RawSnapshotNode, Rect, SnapshotOptions } from '../../utils/snapshot.ts';
2
-
3
- export function findBounds(xml: string, query: string): { x: number; y: number } | null {
4
- const q = query.toLowerCase();
5
- const nodeRegex = /<node[^>]+>/g;
6
- let match = nodeRegex.exec(xml);
7
- while (match) {
8
- const node = match[0];
9
- const attrs = parseXmlNodeAttributes(node);
10
- const textVal = (readXmlAttr(attrs, 'text') ?? '').toLowerCase();
11
- const descVal = (readXmlAttr(attrs, 'content-desc') ?? '').toLowerCase();
12
- if (textVal.includes(q) || descVal.includes(q)) {
13
- const rect = parseBounds(readXmlAttr(attrs, 'bounds'));
14
- if (rect) {
15
- return {
16
- x: Math.floor(rect.x + rect.width / 2),
17
- y: Math.floor(rect.y + rect.height / 2),
18
- };
19
- }
20
- return { x: 0, y: 0 };
21
- }
22
- match = nodeRegex.exec(xml);
23
- }
24
- return null;
25
- }
26
-
27
- export function parseUiHierarchy(
28
- xml: string,
29
- maxNodes: number,
30
- options: SnapshotOptions,
31
- ): { nodes: RawSnapshotNode[]; truncated?: boolean } {
32
- const tree = parseUiHierarchyTree(xml);
33
- const nodes: RawSnapshotNode[] = [];
34
- let truncated = false;
35
- const maxDepth = options.depth ?? Number.POSITIVE_INFINITY;
36
- const scopedRoot = options.scope ? findScopeNode(tree, options.scope) : null;
37
- const roots = scopedRoot ? [scopedRoot] : tree.children;
38
-
39
- const interactiveDescendantMemo = new Map<AndroidNode, boolean>();
40
- const hasInteractiveDescendant = (node: AndroidNode): boolean => {
41
- const cached = interactiveDescendantMemo.get(node);
42
- if (cached !== undefined) return cached;
43
- for (const child of node.children) {
44
- if (child.hittable || hasInteractiveDescendant(child)) {
45
- interactiveDescendantMemo.set(node, true);
46
- return true;
47
- }
48
- }
49
- interactiveDescendantMemo.set(node, false);
50
- return false;
51
- };
52
-
53
- const walk = (
54
- node: AndroidNode,
55
- depth: number,
56
- parentIndex?: number,
57
- ancestorHittable: boolean = false,
58
- ancestorCollection: boolean = false,
59
- ) => {
60
- if (nodes.length >= maxNodes) {
61
- truncated = true;
62
- return;
63
- }
64
- if (depth > maxDepth) return;
65
-
66
- const include = options.raw
67
- ? true
68
- : shouldIncludeAndroidNode(
69
- node,
70
- options,
71
- ancestorHittable,
72
- hasInteractiveDescendant(node),
73
- ancestorCollection,
74
- );
75
- let currentIndex = parentIndex;
76
- if (include) {
77
- currentIndex = nodes.length;
78
- nodes.push({
79
- index: currentIndex,
80
- type: node.type ?? undefined,
81
- label: node.label ?? undefined,
82
- value: node.value ?? undefined,
83
- identifier: node.identifier ?? undefined,
84
- rect: node.rect,
85
- enabled: node.enabled,
86
- hittable: node.hittable,
87
- depth,
88
- parentIndex,
89
- });
90
- }
91
- const nextAncestorHittable = ancestorHittable || Boolean(node.hittable);
92
- const nextAncestorCollection = ancestorCollection || isCollectionContainerType(node.type);
93
- for (const child of node.children) {
94
- walk(child, depth + 1, currentIndex, nextAncestorHittable, nextAncestorCollection);
95
- if (truncated) return;
96
- }
97
- };
98
-
99
- for (const root of roots) {
100
- walk(root, 0, undefined, false, false);
101
- if (truncated) break;
102
- }
103
-
104
- return truncated ? { nodes, truncated } : { nodes };
105
- }
106
-
107
- export function readNodeAttributes(node: string): {
108
- text: string | null;
109
- desc: string | null;
110
- resourceId: string | null;
111
- className: string | null;
112
- bounds: string | null;
113
- clickable?: boolean;
114
- enabled?: boolean;
115
- focusable?: boolean;
116
- focused?: boolean;
117
- } {
118
- const attrs = parseXmlNodeAttributes(node);
119
- const getAttr = (name: string): string | null => readXmlAttr(attrs, name);
120
- const boolAttr = (name: string): boolean | undefined => {
121
- const raw = getAttr(name);
122
- if (raw === null) return undefined;
123
- return raw === 'true';
124
- };
125
- return {
126
- text: getAttr('text'),
127
- desc: getAttr('content-desc'),
128
- resourceId: getAttr('resource-id'),
129
- className: getAttr('class'),
130
- bounds: getAttr('bounds'),
131
- clickable: boolAttr('clickable'),
132
- enabled: boolAttr('enabled'),
133
- focusable: boolAttr('focusable'),
134
- focused: boolAttr('focused'),
135
- };
136
- }
137
-
138
- function parseXmlNodeAttributes(node: string): Map<string, string> {
139
- const attrs = new Map<string, string>();
140
- const start = node.indexOf(' ');
141
- const end = node.lastIndexOf('>');
142
- if (start < 0 || end <= start) return attrs;
143
-
144
- const attrRegex = /([^\s=/>]+)\s*=\s*(["'])([\s\S]*?)\2/y;
145
- let cursor = start;
146
- while (cursor < end) {
147
- while (cursor < end) {
148
- const char = node[cursor];
149
- if (char !== ' ' && char !== '\n' && char !== '\r' && char !== '\t') break;
150
- cursor += 1;
151
- }
152
- if (cursor >= end) break;
153
- const char = node[cursor];
154
- if (char === '/' || char === '>') break;
155
-
156
- attrRegex.lastIndex = cursor;
157
- const match = attrRegex.exec(node);
158
- if (!match) break;
159
- attrs.set(match[1], match[3]);
160
- cursor = attrRegex.lastIndex;
161
- }
162
-
163
- return attrs;
164
- }
165
-
166
- function readXmlAttr(attrs: Map<string, string>, name: string): string | null {
167
- return attrs.get(name) ?? null;
168
- }
169
-
170
- export function parseBounds(bounds: string | null): Rect | undefined {
171
- if (!bounds) return undefined;
172
- const match = /\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(bounds);
173
- if (!match) return undefined;
174
- const x1 = Number(match[1]);
175
- const y1 = Number(match[2]);
176
- const x2 = Number(match[3]);
177
- const y2 = Number(match[4]);
178
- return { x: x1, y: y1, width: Math.max(0, x2 - x1), height: Math.max(0, y2 - y1) };
179
- }
180
-
181
- type AndroidNode = {
182
- type: string | null;
183
- label: string | null;
184
- value: string | null;
185
- identifier: string | null;
186
- rect?: Rect;
187
- enabled?: boolean;
188
- hittable?: boolean;
189
- depth: number;
190
- parentIndex?: number;
191
- children: AndroidNode[];
192
- };
193
-
194
- function parseUiHierarchyTree(xml: string): AndroidNode {
195
- const root: AndroidNode = {
196
- type: null,
197
- label: null,
198
- value: null,
199
- identifier: null,
200
- depth: -1,
201
- children: [],
202
- };
203
- const stack: AndroidNode[] = [root];
204
- const tokenRegex = /<node\b[^>]*>|<\/node>/g;
205
- let match = tokenRegex.exec(xml);
206
- while (match) {
207
- const token = match[0];
208
- if (token.startsWith('</node')) {
209
- if (stack.length > 1) stack.pop();
210
- match = tokenRegex.exec(xml);
211
- continue;
212
- }
213
- const attrs = readNodeAttributes(token);
214
- const rect = parseBounds(attrs.bounds);
215
- const parent = stack[stack.length - 1];
216
- const node: AndroidNode = {
217
- type: attrs.className,
218
- label: attrs.text || attrs.desc,
219
- value: attrs.text,
220
- identifier: attrs.resourceId,
221
- rect,
222
- enabled: attrs.enabled,
223
- hittable: attrs.clickable ?? attrs.focusable,
224
- depth: parent.depth + 1,
225
- parentIndex: undefined,
226
- children: [],
227
- };
228
- parent.children.push(node);
229
- if (!token.endsWith('/>')) {
230
- stack.push(node);
231
- }
232
- match = tokenRegex.exec(xml);
233
- }
234
- return root;
235
- }
236
-
237
- function shouldIncludeAndroidNode(
238
- node: AndroidNode,
239
- options: SnapshotOptions,
240
- ancestorHittable: boolean,
241
- descendantHittable: boolean,
242
- ancestorCollection: boolean,
243
- ): boolean {
244
- const type = normalizeAndroidType(node.type);
245
- const hasText = Boolean(node.label && node.label.trim().length > 0);
246
- const hasId = Boolean(node.identifier && node.identifier.trim().length > 0);
247
- const hasMeaningfulText = hasText && !isGenericAndroidId(node.label ?? '');
248
- const hasMeaningfulId = hasId && !isGenericAndroidId(node.identifier ?? '');
249
- const isStructural = isStructuralAndroidType(type);
250
- const isVisual = type === 'imageview' || type === 'imagebutton';
251
- if (options.interactiveOnly) {
252
- if (node.hittable) return true;
253
- // Keep text proxies for tappable rows while dropping structural noise.
254
- const proxyCandidate = hasMeaningfulText || hasMeaningfulId;
255
- if (!proxyCandidate) return false;
256
- if (isVisual) return false;
257
- if (isStructural && !ancestorCollection) return false;
258
- return ancestorHittable || descendantHittable || ancestorCollection;
259
- }
260
- if (options.compact) {
261
- return hasMeaningfulText || hasMeaningfulId || Boolean(node.hittable);
262
- }
263
- if (isStructural || isVisual) {
264
- if (node.hittable) return true;
265
- if (hasMeaningfulText) return true;
266
- if (hasMeaningfulId && descendantHittable) return true;
267
- return descendantHittable;
268
- }
269
- return true;
270
- }
271
-
272
- function isCollectionContainerType(type: string | null): boolean {
273
- if (!type) return false;
274
- const normalized = normalizeAndroidType(type);
275
- return (
276
- normalized.includes('recyclerview') ||
277
- normalized.includes('listview') ||
278
- normalized.includes('gridview')
279
- );
280
- }
281
-
282
- function normalizeAndroidType(type: string | null): string {
283
- if (!type) return '';
284
- return type.toLowerCase();
285
- }
286
-
287
- function isStructuralAndroidType(type: string): boolean {
288
- const short = type.split('.').pop() ?? type;
289
- return short.includes('layout') || short === 'viewgroup' || short === 'view';
290
- }
291
-
292
- function isGenericAndroidId(value: string): boolean {
293
- const trimmed = value.trim();
294
- if (!trimmed) return false;
295
- return /^[\w.]+:id\/[\w.-]+$/i.test(trimmed);
296
- }
297
-
298
- function findScopeNode(root: AndroidNode, scope: string): AndroidNode | null {
299
- const query = scope.toLowerCase();
300
- const stack: AndroidNode[] = [...root.children];
301
- while (stack.length > 0) {
302
- const node = stack.shift() as AndroidNode;
303
- const label = node.label?.toLowerCase() ?? '';
304
- const value = node.value?.toLowerCase() ?? '';
305
- const identifier = node.identifier?.toLowerCase() ?? '';
306
- if (label.includes(query) || value.includes(query) || identifier.includes(query)) {
307
- return node;
308
- }
309
- stack.push(...node.children);
310
- }
311
- return null;
312
- }
@@ -1,128 +0,0 @@
1
- import { asAppError } from '../utils/errors.ts';
2
-
3
- export type BootFailureReason =
4
- | 'IOS_BOOT_TIMEOUT'
5
- | 'IOS_RUNNER_CONNECT_TIMEOUT'
6
- | 'IOS_TOOL_MISSING'
7
- | 'ANDROID_BOOT_TIMEOUT'
8
- | 'ADB_TRANSPORT_UNAVAILABLE'
9
- | 'CI_RESOURCE_STARVATION_SUSPECTED'
10
- | 'BOOT_COMMAND_FAILED'
11
- | 'UNKNOWN';
12
-
13
- type BootDiagnosticContext = {
14
- platform?: 'ios' | 'android';
15
- phase?: 'boot' | 'connect' | 'transport';
16
- };
17
-
18
- export function classifyBootFailure(input: {
19
- error?: unknown;
20
- message?: string;
21
- stdout?: string;
22
- stderr?: string;
23
- context?: BootDiagnosticContext;
24
- }): BootFailureReason {
25
- const appErr = input.error ? asAppError(input.error) : null;
26
- const platform = input.context?.platform;
27
- const phase = input.context?.phase;
28
- if (appErr?.code === 'TOOL_MISSING') {
29
- return platform === 'android' ? 'ADB_TRANSPORT_UNAVAILABLE' : 'IOS_TOOL_MISSING';
30
- }
31
- const details = (appErr?.details ?? {}) as Record<string, unknown>;
32
- const detailMessage = typeof details.message === 'string' ? details.message : undefined;
33
- const detailStdout = typeof details.stdout === 'string' ? details.stdout : undefined;
34
- const detailStderr = typeof details.stderr === 'string' ? details.stderr : undefined;
35
- const nestedBoot = details.boot && typeof details.boot === 'object'
36
- ? (details.boot as Record<string, unknown>)
37
- : null;
38
- const nestedBootstatus = details.bootstatus && typeof details.bootstatus === 'object'
39
- ? (details.bootstatus as Record<string, unknown>)
40
- : null;
41
-
42
- const haystack = [
43
- input.message,
44
- appErr?.message,
45
- input.stdout,
46
- input.stderr,
47
- detailMessage,
48
- detailStdout,
49
- detailStderr,
50
- typeof nestedBoot?.stdout === 'string' ? nestedBoot.stdout : undefined,
51
- typeof nestedBoot?.stderr === 'string' ? nestedBoot.stderr : undefined,
52
- typeof nestedBootstatus?.stdout === 'string' ? nestedBootstatus.stdout : undefined,
53
- typeof nestedBootstatus?.stderr === 'string' ? nestedBootstatus.stderr : undefined,
54
- ]
55
- .filter(Boolean)
56
- .join('\n')
57
- .toLowerCase();
58
-
59
- if (
60
- platform === 'ios' &&
61
- (
62
- haystack.includes('runner did not accept connection') ||
63
- (phase === 'connect' &&
64
- (
65
- haystack.includes('timed out') ||
66
- haystack.includes('timeout') ||
67
- haystack.includes('econnrefused') ||
68
- haystack.includes('connection refused') ||
69
- haystack.includes('fetch failed') ||
70
- haystack.includes('socket hang up')
71
- ))
72
- )
73
- ) {
74
- return 'IOS_RUNNER_CONNECT_TIMEOUT';
75
- }
76
- if (platform === 'ios' && phase === 'boot' && (haystack.includes('timed out') || haystack.includes('timeout'))) {
77
- return 'IOS_BOOT_TIMEOUT';
78
- }
79
- if (platform === 'android' && phase === 'boot' && (haystack.includes('timed out') || haystack.includes('timeout'))) {
80
- return 'ANDROID_BOOT_TIMEOUT';
81
- }
82
- if (
83
- haystack.includes('resource temporarily unavailable') ||
84
- haystack.includes('killed: 9') ||
85
- haystack.includes('cannot allocate memory') ||
86
- haystack.includes('system is low on memory')
87
- ) {
88
- return 'CI_RESOURCE_STARVATION_SUSPECTED';
89
- }
90
- if (
91
- platform === 'android' &&
92
- (
93
- haystack.includes('device not found') ||
94
- haystack.includes('no devices') ||
95
- haystack.includes('device offline') ||
96
- haystack.includes('offline') ||
97
- haystack.includes('unauthorized') ||
98
- haystack.includes('not authorized') ||
99
- haystack.includes('unable to locate device') ||
100
- haystack.includes('invalid device')
101
- )
102
- ) {
103
- return 'ADB_TRANSPORT_UNAVAILABLE';
104
- }
105
- if (appErr?.code === 'COMMAND_FAILED' || haystack.length > 0) return 'BOOT_COMMAND_FAILED';
106
- return 'UNKNOWN';
107
- }
108
-
109
- export function bootFailureHint(reason: BootFailureReason): string {
110
- switch (reason) {
111
- case 'IOS_BOOT_TIMEOUT':
112
- return 'Retry simulator boot and inspect simctl bootstatus logs; in CI consider increasing AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS.';
113
- case 'IOS_RUNNER_CONNECT_TIMEOUT':
114
- return 'Retry runner startup, inspect xcodebuild logs, and verify simulator responsiveness before command execution.';
115
- case 'ANDROID_BOOT_TIMEOUT':
116
- return 'Retry emulator startup and verify sys.boot_completed reaches 1; consider increasing startup budget in CI.';
117
- case 'ADB_TRANSPORT_UNAVAILABLE':
118
- return 'Check adb server/device transport (adb devices -l), restart adb, and ensure the target device is online and authorized.';
119
- case 'CI_RESOURCE_STARVATION_SUSPECTED':
120
- return 'CI machine may be resource constrained; reduce parallel jobs or use a larger runner.';
121
- case 'IOS_TOOL_MISSING':
122
- return 'Xcode command-line tools are missing or not in PATH; run xcode-select --install and verify xcrun works.';
123
- case 'BOOT_COMMAND_FAILED':
124
- return 'Inspect command stderr/stdout for the failing boot phase and retry after environment validation.';
125
- default:
126
- return 'Retry once and inspect verbose logs for the failing phase.';
127
- }
128
- }