@limrun/api 0.17.4 → 0.18.0-rc.1

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/src/ios-client.ts CHANGED
@@ -25,16 +25,158 @@ export interface SimctlExecutionEvents {
25
25
  error: (error: Error) => void;
26
26
  }
27
27
 
28
+ // ============================================================================
29
+ // Accessibility Selector - used for element-based operations
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Selector criteria for finding accessibility elements.
34
+ * All non-undefined fields must match for an element to be selected.
35
+ */
36
+ export type AccessibilitySelector = {
37
+ /** Match by AXUniqueId (accessibilityIdentifier) - exact match */
38
+ accessibilityId?: string;
39
+ /** Match by AXLabel - exact match */
40
+ label?: string;
41
+ /** Match by AXLabel - contains (case-insensitive) */
42
+ labelContains?: string;
43
+ /** Match by element type/role (e.g., "Button", "TextField") - case-insensitive */
44
+ elementType?: string;
45
+ /** Match by title - exact match */
46
+ title?: string;
47
+ /** Match by title - contains (case-insensitive) */
48
+ titleContains?: string;
49
+ /** Match by AXValue - exact match */
50
+ value?: string;
51
+ };
52
+
53
+ /**
54
+ * A point on the screen for accessibility queries
55
+ */
56
+ export type AccessibilityPoint = {
57
+ x: number;
58
+ y: number;
59
+ };
60
+
61
+ // ============================================================================
62
+ // Result Types
63
+ // ============================================================================
64
+
65
+ export type ScreenshotData = {
66
+ /** Base64-encoded JPEG image data */
67
+ base64: string;
68
+ /** Width in points (for tap coordinates) */
69
+ width: number;
70
+ /** Height in points (for tap coordinates) */
71
+ height: number;
72
+ };
73
+
74
+ export type TapElementResult = {
75
+ elementLabel?: string;
76
+ elementType?: string;
77
+ };
78
+
79
+ export type ElementResult = {
80
+ elementLabel?: string;
81
+ };
82
+
83
+ export type InstalledApp = {
84
+ bundleId: string;
85
+ name: string;
86
+ installType: string;
87
+ };
88
+
89
+ export type LsofEntry = {
90
+ kind: 'unix';
91
+ path: string;
92
+ };
93
+
28
94
  /**
29
95
  * A client for interacting with a Limrun iOS instance
30
96
  */
31
97
  export type InstanceClient = {
32
98
  /**
33
99
  * Take a screenshot of the current screen
34
- * @returns A promise that resolves to the screenshot data
100
+ * @returns A promise that resolves to the screenshot data with base64 image and dimensions
35
101
  */
36
102
  screenshot: () => Promise<ScreenshotData>;
37
103
 
104
+ /**
105
+ * Get the element tree (accessibility hierarchy) of the current screen
106
+ * @param point Optional point to get the element at that specific location
107
+ * @returns A promise that resolves to the JSON string of the accessibility tree
108
+ */
109
+ elementTree: (point?: AccessibilityPoint) => Promise<string>;
110
+
111
+ /**
112
+ * Tap at the specified coordinates
113
+ * @param x X coordinate in points
114
+ * @param y Y coordinate in points
115
+ */
116
+ tap: (x: number, y: number) => Promise<void>;
117
+
118
+ /**
119
+ * Tap an accessibility element by selector
120
+ * @param selector The selector criteria to find the element
121
+ * @returns Information about the tapped element
122
+ */
123
+ tapElement: (selector: AccessibilitySelector) => Promise<TapElementResult>;
124
+
125
+ /**
126
+ * Increment an accessibility element (useful for sliders, steppers, etc.)
127
+ * @param selector The selector criteria to find the element
128
+ * @returns Information about the incremented element
129
+ */
130
+ incrementElement: (selector: AccessibilitySelector) => Promise<ElementResult>;
131
+
132
+ /**
133
+ * Decrement an accessibility element (useful for sliders, steppers, etc.)
134
+ * @param selector The selector criteria to find the element
135
+ * @returns Information about the decremented element
136
+ */
137
+ decrementElement: (selector: AccessibilitySelector) => Promise<ElementResult>;
138
+
139
+ /**
140
+ * Set the value of an accessibility element (useful for text fields, etc.)
141
+ * This is much faster than typing character by character.
142
+ * @param text The text value to set
143
+ * @param selector The selector criteria to find the element
144
+ * @returns Information about the modified element
145
+ */
146
+ setElementValue: (text: string, selector: AccessibilitySelector) => Promise<ElementResult>;
147
+
148
+ /**
149
+ * Type text into the currently focused input field
150
+ * @param text The text to type
151
+ * @param pressEnter If true, press Enter after typing
152
+ */
153
+ typeText: (text: string, pressEnter?: boolean) => Promise<void>;
154
+
155
+ /**
156
+ * Press a key on the keyboard, optionally with modifiers
157
+ * @param key The key to press (e.g., 'a', 'enter', 'backspace', 'up', 'f1')
158
+ * @param modifiers Optional modifier keys (e.g., ['shift'], ['command', 'shift'])
159
+ */
160
+ pressKey: (key: string, modifiers?: string[]) => Promise<void>;
161
+
162
+ /**
163
+ * Launch an installed app by bundle identifier
164
+ * @param bundleId Bundle identifier of the app to launch
165
+ */
166
+ launchApp: (bundleId: string) => Promise<void>;
167
+
168
+ /**
169
+ * List installed apps on the simulator
170
+ * @returns Array of installed apps with bundleId, name, and installType
171
+ */
172
+ listApps: () => Promise<InstalledApp[]>;
173
+
174
+ /**
175
+ * Open a URL in the simulator (web URLs open in Safari, deep links open corresponding apps)
176
+ * @param url The URL to open
177
+ */
178
+ openUrl: (url: string) => Promise<void>;
179
+
38
180
  /**
39
181
  * Disconnect from the Limrun instance
40
182
  */
@@ -109,11 +251,6 @@ export type InstanceClient = {
109
251
  lsof: () => Promise<LsofEntry[]>;
110
252
  };
111
253
 
112
- export type LsofEntry = {
113
- kind: 'unix';
114
- path: string;
115
- };
116
-
117
254
  /**
118
255
  * Controls the verbosity of logging in the client
119
256
  */
@@ -153,27 +290,38 @@ export type InstanceClientOptions = {
153
290
  maxReconnectDelay?: number;
154
291
  };
155
292
 
156
- type ScreenshotRequest = {
157
- type: 'screenshot';
158
- id: string;
159
- };
160
-
161
- type ScreenshotResponse = {
162
- type: 'screenshot';
163
- dataUri: string;
164
- id: string;
165
- };
293
+ // ============================================================================
294
+ // Internal Types - Message Protocol
295
+ // ============================================================================
166
296
 
167
- type ScreenshotData = {
168
- dataUri: string;
297
+ /**
298
+ * Generic pending request tracker
299
+ */
300
+ type PendingRequest<T> = {
301
+ resolve: (value: T) => void;
302
+ reject: (reason: Error) => void;
303
+ timeout: NodeJS.Timeout;
169
304
  };
170
305
 
171
- type ScreenshotErrorResponse = {
172
- type: 'screenshotError';
173
- message: string;
174
- id: string;
306
+ /**
307
+ * Mapping from response type to the result type it produces
308
+ */
309
+ type ResponseTypeMap = {
310
+ screenshotResult: ScreenshotData;
311
+ viewTreeResult: string;
312
+ tapResult: void;
313
+ tapElementResult: TapElementResult;
314
+ incrementElementResult: ElementResult;
315
+ decrementElementResult: ElementResult;
316
+ setElementValueResult: ElementResult;
317
+ typeTextResult: void;
318
+ pressKeyResult: void;
319
+ launchAppResult: void;
320
+ listAppsResult: InstalledApp[];
321
+ listOpenFilesResult: LsofEntry[];
175
322
  };
176
323
 
324
+ // Simctl uses streaming, so it's handled separately
177
325
  type SimctlRequest = {
178
326
  type: 'simctl';
179
327
  id: string;
@@ -188,33 +336,28 @@ type SimctlStreamResponse = {
188
336
  exitCode?: number;
189
337
  };
190
338
 
191
- type SimctlErrorResponse = {
192
- type: 'simctlError';
193
- id: string;
194
- message: string;
195
- };
196
-
197
- type ListOpenFilesRequest = {
198
- type: 'listOpenFiles';
199
- id: string;
200
- kind: 'unix';
201
- };
202
-
203
- type ListOpenFilesResponse = {
204
- type: 'listOpenFilesResult';
339
+ /**
340
+ * Generic server response with optional error
341
+ */
342
+ type ServerResponse = {
343
+ type: string;
205
344
  id: string;
206
- files?: LsofEntry[];
207
345
  error?: string;
346
+ // Response-specific fields
347
+ base64?: string;
348
+ width?: number;
349
+ height?: number;
350
+ json?: string;
351
+ elementLabel?: string;
352
+ elementType?: string;
353
+ apps?: string;
354
+ files?: LsofEntry[];
355
+ // Simctl streaming fields
356
+ stdout?: string;
357
+ stderr?: string;
358
+ exitCode?: number;
208
359
  };
209
360
 
210
- type ServerMessage =
211
- | ScreenshotResponse
212
- | ScreenshotErrorResponse
213
- | SimctlStreamResponse
214
- | SimctlErrorResponse
215
- | ListOpenFilesResponse
216
- | { type: string; [key: string]: unknown };
217
-
218
361
  /**
219
362
  * Handle for a running simctl command execution.
220
363
  *
@@ -408,22 +551,10 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
408
551
  let intentionalDisconnect = false;
409
552
  let lastError: string | undefined;
410
553
 
411
- const screenshotRequests: Map<
412
- string,
413
- {
414
- resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => void;
415
- rejecter: (reason?: any) => void;
416
- }
417
- > = new Map();
418
-
419
- const lsofRequests: Map<
420
- string,
421
- {
422
- resolver: (value: LsofEntry[] | PromiseLike<LsofEntry[]>) => void;
423
- rejecter: (reason?: any) => void;
424
- }
425
- > = new Map();
554
+ // Centralized pending requests map - handles all request/response patterns
555
+ const pendingRequests: Map<string, PendingRequest<any>> = new Map();
426
556
 
557
+ // Simctl uses streaming, so it needs separate handling
427
558
  const simctlExecutions: Map<string, SimctlExecution> = new Map();
428
559
 
429
560
  const stateChangeCallbacks: Set<ConnectionStateCallback> = new Set();
@@ -459,11 +590,11 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
459
590
  };
460
591
 
461
592
  const failPendingRequests = (reason: string): void => {
462
- screenshotRequests.forEach((request) => request.rejecter(new Error(reason)));
463
- screenshotRequests.clear();
464
-
465
- lsofRequests.forEach((request) => request.rejecter(new Error(reason)));
466
- lsofRequests.clear();
593
+ pendingRequests.forEach((request) => {
594
+ clearTimeout(request.timeout);
595
+ request.reject(new Error(reason));
596
+ });
597
+ pendingRequests.clear();
467
598
 
468
599
  simctlExecutions.forEach((execution) => execution._handleError(new Error(reason)));
469
600
  simctlExecutions.clear();
@@ -523,6 +654,69 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
523
654
  }, currentDelay);
524
655
  };
525
656
 
657
+ // Generate unique request ID
658
+ const generateId = (): string => {
659
+ return `ts-client-${Date.now()}-${Math.random().toString(36).substring(7)}`;
660
+ };
661
+
662
+ // Generic request sender with timeout and response handling
663
+ const sendRequest = <T>(
664
+ type: string,
665
+ params: Record<string, unknown> = {},
666
+ timeoutMs: number = 30000,
667
+ ): Promise<T> => {
668
+ return new Promise((resolve, reject) => {
669
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
670
+ reject(new Error('WebSocket is not connected or connection is not open.'));
671
+ return;
672
+ }
673
+
674
+ const id = generateId();
675
+ const timeout = setTimeout(() => {
676
+ pendingRequests.delete(id);
677
+ reject(new Error(`Request ${type} timed out`));
678
+ }, timeoutMs);
679
+
680
+ pendingRequests.set(id, { resolve, reject, timeout });
681
+
682
+ const request = { type, id, ...params };
683
+ logger.debug('Sending request:', request);
684
+
685
+ ws.send(JSON.stringify(request), (err?: Error) => {
686
+ if (err) {
687
+ clearTimeout(timeout);
688
+ pendingRequests.delete(id);
689
+ logger.error(`Failed to send ${type} request:`, err);
690
+ reject(err);
691
+ }
692
+ });
693
+ });
694
+ };
695
+
696
+ // Response handlers - transform raw responses to typed results
697
+ const responseHandlers: Record<string, (msg: ServerResponse) => unknown> = {
698
+ screenshotResult: (msg) => ({
699
+ base64: msg.base64!,
700
+ width: msg.width!,
701
+ height: msg.height!,
702
+ }),
703
+ elementTreeResult: (msg) => msg.json!,
704
+ tapResult: () => undefined,
705
+ tapElementResult: (msg) => ({
706
+ elementLabel: msg.elementLabel,
707
+ elementType: msg.elementType,
708
+ }),
709
+ incrementElementResult: (msg) => ({ elementLabel: msg.elementLabel }),
710
+ decrementElementResult: (msg) => ({ elementLabel: msg.elementLabel }),
711
+ setElementValueResult: (msg) => ({ elementLabel: msg.elementLabel }),
712
+ typeTextResult: () => undefined,
713
+ pressKeyResult: () => undefined,
714
+ launchAppResult: () => undefined,
715
+ listAppsResult: (msg) => JSON.parse(msg.apps || '[]') as InstalledApp[],
716
+ listOpenFilesResult: (msg) => msg.files || [],
717
+ openUrlResult: () => undefined,
718
+ };
719
+
526
720
  const setupWebSocket = (): void => {
527
721
  cleanup();
528
722
  updateConnectionState('connecting');
@@ -530,7 +724,7 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
530
724
  ws = new WebSocket(endpointWebSocketUrl);
531
725
 
532
726
  ws.on('message', (data: Data) => {
533
- let message: ServerMessage;
727
+ let message: ServerResponse;
534
728
  try {
535
729
  message = JSON.parse(data.toString());
536
730
  } catch (e) {
@@ -538,163 +732,66 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
538
732
  return;
539
733
  }
540
734
 
541
- switch (message.type) {
542
- case 'screenshot': {
543
- if (!('dataUri' in message) || typeof message.dataUri !== 'string' || !('id' in message)) {
544
- logger.warn('Received invalid screenshot message:', message);
545
- break;
546
- }
547
-
548
- const screenshotMessage = message as ScreenshotResponse;
549
- const request = screenshotRequests.get(screenshotMessage.id);
550
-
551
- if (!request) {
552
- logger.warn(
553
- `Received screenshot data for unknown or already handled session: ${screenshotMessage.id}`,
554
- );
555
- break;
556
- }
557
-
558
- logger.debug(`Received screenshot data URI for session ${screenshotMessage.id}.`);
559
- request.resolver({ dataUri: screenshotMessage.dataUri });
560
- screenshotRequests.delete(screenshotMessage.id);
561
- break;
735
+ // Handle simctl streaming separately (it uses multiple messages per request)
736
+ if (message.type === 'simctlStream') {
737
+ const execution = simctlExecutions.get(message.id);
738
+ if (!execution) {
739
+ logger.warn(`Received simctl stream for unknown execution: ${message.id}`);
740
+ return;
562
741
  }
563
- case 'screenshotError': {
564
- if (!('message' in message) || !('id' in message)) {
565
- logger.warn('Received invalid screenshot error message:', message);
566
- break;
567
- }
568
-
569
- const errorMessage = message as ScreenshotErrorResponse;
570
- const request = screenshotRequests.get(errorMessage.id);
571
742
 
572
- if (!request) {
573
- logger.warn(
574
- `Received screenshot error for unknown or already handled session: ${errorMessage.id}`,
575
- );
576
- break;
743
+ if (message.stdout) {
744
+ try {
745
+ execution._handleStdout(Buffer.from(message.stdout, 'base64'));
746
+ } catch (err) {
747
+ logger.error('Failed to decode stdout data:', err);
577
748
  }
578
-
579
- logger.error(
580
- `Server reported an error capturing screenshot for session ${errorMessage.id}:`,
581
- errorMessage.message,
582
- );
583
- request.rejecter(new Error(errorMessage.message));
584
- screenshotRequests.delete(errorMessage.id);
585
- break;
586
749
  }
587
- case 'simctlStream': {
588
- if (!('id' in message)) {
589
- logger.warn('Received invalid simctl stream message:', message);
590
- break;
591
- }
592
-
593
- const streamMessage = message as SimctlStreamResponse;
594
- const execution = simctlExecutions.get(streamMessage.id);
595
750
 
596
- if (!execution) {
597
- logger.warn(
598
- `Received simctl stream for unknown or already completed execution: ${streamMessage.id}`,
599
- );
600
- break;
751
+ if (message.stderr) {
752
+ try {
753
+ execution._handleStderr(Buffer.from(message.stderr, 'base64'));
754
+ } catch (err) {
755
+ logger.error('Failed to decode stderr data:', err);
601
756
  }
602
-
603
- // Handle stdout if present
604
- if (streamMessage.stdout) {
605
- try {
606
- const stdoutBuffer = Buffer.from(streamMessage.stdout, 'base64');
607
- execution._handleStdout(stdoutBuffer);
608
- } catch (err) {
609
- logger.error('Failed to decode stdout data:', err);
610
- }
611
- }
612
-
613
- // Handle stderr if present
614
- if (streamMessage.stderr) {
615
- try {
616
- const stderrBuffer = Buffer.from(streamMessage.stderr, 'base64');
617
- execution._handleStderr(stderrBuffer);
618
- } catch (err) {
619
- logger.error('Failed to decode stderr data:', err);
620
- }
621
- }
622
-
623
- // Handle exit code if present (final message)
624
- if (streamMessage.exitCode !== undefined) {
625
- logger.debug(
626
- `Simctl execution ${streamMessage.id} completed with exit code ${streamMessage.exitCode}`,
627
- );
628
- execution._handleExit(streamMessage.exitCode);
629
- simctlExecutions.delete(streamMessage.id);
630
- }
631
- break;
632
757
  }
633
- case 'simctlError': {
634
- if (!('message' in message) || !('id' in message)) {
635
- logger.warn('Received invalid simctl error message:', message);
636
- break;
637
- }
638
758
 
639
- const errorMessage = message as SimctlErrorResponse;
640
- const execution = simctlExecutions.get(errorMessage.id);
641
-
642
- if (!execution) {
643
- logger.warn(
644
- `Received simctl error for unknown or already handled execution: ${errorMessage.id}`,
645
- );
646
- break;
647
- }
648
-
649
- logger.error(
650
- `Server reported an error for simctl execution ${errorMessage.id}:`,
651
- errorMessage.message,
652
- );
653
- execution._handleError(new Error(errorMessage.message));
654
- simctlExecutions.delete(errorMessage.id);
655
- break;
759
+ if (message.exitCode !== undefined) {
760
+ logger.debug(`Simctl execution ${message.id} completed with exit code ${message.exitCode}`);
761
+ execution._handleExit(message.exitCode);
762
+ simctlExecutions.delete(message.id);
656
763
  }
657
- case 'listOpenFilesResult': {
658
- if (!('id' in message)) {
659
- logger.warn('Received invalid listOpenFilesResult message:', message);
660
- break;
661
- }
662
-
663
- const lsofMessage = message as ListOpenFilesResponse;
664
- const request = lsofRequests.get(lsofMessage.id);
764
+ return;
765
+ }
665
766
 
666
- if (!request) {
667
- logger.warn(
668
- `Received listOpenFilesResult for unknown or already handled session: ${lsofMessage.id}`,
669
- );
670
- break;
671
- }
767
+ // Handle all other request/response patterns generically
768
+ const request = pendingRequests.get(message.id);
769
+ if (!request) {
770
+ logger.debug(`Received response for unknown or already handled request: ${message.id}`);
771
+ return;
772
+ }
672
773
 
673
- if (lsofMessage.error) {
674
- logger.error(
675
- `Server reported an error listing open files for session ${lsofMessage.id}:`,
676
- lsofMessage.error,
677
- );
678
- request.rejecter(new Error(lsofMessage.error));
679
- lsofRequests.delete(lsofMessage.id);
680
- break;
681
- }
774
+ clearTimeout(request.timeout);
775
+ pendingRequests.delete(message.id);
682
776
 
683
- if (!lsofMessage.files || !Array.isArray(lsofMessage.files)) {
684
- logger.warn('Received listOpenFilesResult without files array:', message);
685
- request.rejecter(new Error('Invalid listOpenFilesResult: missing files array'));
686
- lsofRequests.delete(lsofMessage.id);
687
- break;
688
- }
777
+ // Check for error
778
+ if (message.error) {
779
+ logger.error(`Server error for ${message.type}: ${message.error}`);
780
+ request.reject(new Error(message.error));
781
+ return;
782
+ }
689
783
 
690
- logger.debug(`Received listOpenFilesResult for session ${lsofMessage.id}.`);
691
- request.resolver(lsofMessage.files);
692
- lsofRequests.delete(lsofMessage.id);
693
- break;
784
+ // Use handler to transform response, or resolve with raw message
785
+ const handler = responseHandlers[message.type];
786
+ if (handler) {
787
+ try {
788
+ request.resolve(handler(message));
789
+ } catch (err) {
790
+ request.reject(err as Error);
694
791
  }
695
- default:
696
- logger.warn('Received unexpected message type:', message);
697
- break;
792
+ } else {
793
+ logger.warn('Received unexpected message type:', message.type);
794
+ request.resolve(message);
698
795
  }
699
796
  });
700
797
 
@@ -749,6 +846,17 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
749
846
  hasResolved = true;
750
847
  resolveConnection({
751
848
  screenshot,
849
+ elementTree,
850
+ tap,
851
+ tapElement,
852
+ incrementElement,
853
+ decrementElement,
854
+ setElementValue,
855
+ typeText,
856
+ pressKey,
857
+ launchApp,
858
+ listApps,
859
+ openUrl,
752
860
  disconnect,
753
861
  getConnectionState,
754
862
  onConnectionStateChange,
@@ -760,93 +868,64 @@ export async function createInstanceClient(options: InstanceClientOptions): Prom
760
868
  });
761
869
  };
762
870
 
763
- const screenshot = async (): Promise<ScreenshotData> => {
764
- if (!ws || ws.readyState !== WebSocket.OPEN) {
765
- return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
766
- }
871
+ // ========================================================================
872
+ // Client Methods - using centralized sendRequest
873
+ // ========================================================================
767
874
 
768
- const id = 'ts-client-' + Date.now();
769
- const screenshotRequest: ScreenshotRequest = {
770
- type: 'screenshot',
771
- id,
772
- };
875
+ const screenshot = (): Promise<ScreenshotData> => {
876
+ return sendRequest<ScreenshotData>('screenshot');
877
+ };
773
878
 
774
- return new Promise<ScreenshotData>((resolve, reject) => {
775
- logger.debug('Sending screenshot request:', screenshotRequest);
776
- ws!.send(JSON.stringify(screenshotRequest), (err?: Error) => {
777
- if (err) {
778
- logger.error('Failed to send screenshot request:', err);
779
- reject(err);
780
- }
781
- });
879
+ const elementTree = (point?: AccessibilityPoint): Promise<string> => {
880
+ return sendRequest<string>('elementTree', { point });
881
+ };
782
882
 
783
- const timeout = setTimeout(() => {
784
- if (screenshotRequests.has(id)) {
785
- logger.error(`Screenshot request timed out for session ${id}`);
786
- screenshotRequests.get(id)?.rejecter(new Error('Screenshot request timed out'));
787
- screenshotRequests.delete(id);
788
- }
789
- }, 30000);
790
- screenshotRequests.set(id, {
791
- resolver: (value: ScreenshotData | PromiseLike<ScreenshotData>) => {
792
- clearTimeout(timeout);
793
- resolve(value);
794
- screenshotRequests.delete(id);
795
- },
796
- rejecter: (reason?: any) => {
797
- clearTimeout(timeout);
798
- reject(reason);
799
- screenshotRequests.delete(id);
800
- },
801
- });
802
- });
883
+ const tap = (x: number, y: number): Promise<void> => {
884
+ return sendRequest<void>('tap', { x, y });
803
885
  };
804
886
 
805
- const lsof = async (): Promise<LsofEntry[]> => {
806
- if (!ws || ws.readyState !== WebSocket.OPEN) {
807
- return Promise.reject(new Error('WebSocket is not connected or connection is not open.'));
808
- }
887
+ const tapElement = (selector: AccessibilitySelector): Promise<TapElementResult> => {
888
+ return sendRequest<TapElementResult>('tapElement', { selector });
889
+ };
809
890
 
810
- const id = 'ts-client-' + Date.now();
811
- const lsofRequest: ListOpenFilesRequest = {
812
- type: 'listOpenFiles',
813
- id,
814
- kind: 'unix',
815
- };
891
+ const incrementElement = (selector: AccessibilitySelector): Promise<ElementResult> => {
892
+ return sendRequest<ElementResult>('incrementElement', { selector });
893
+ };
816
894
 
817
- return new Promise<LsofEntry[]>((resolve, reject) => {
818
- logger.debug('Sending listOpenFiles request:', lsofRequest);
819
- ws!.send(JSON.stringify(lsofRequest), (err?: Error) => {
820
- if (err) {
821
- logger.error('Failed to send listOpenFiles request:', err);
822
- reject(err);
823
- }
824
- });
895
+ const decrementElement = (selector: AccessibilitySelector): Promise<ElementResult> => {
896
+ return sendRequest<ElementResult>('decrementElement', { selector });
897
+ };
825
898
 
826
- const timeout = setTimeout(() => {
827
- if (lsofRequests.has(id)) {
828
- logger.error(`listOpenFiles request timed out for session ${id}`);
829
- lsofRequests.get(id)?.rejecter(new Error('listOpenFiles request timed out'));
830
- lsofRequests.delete(id);
831
- }
832
- }, 30000);
833
- lsofRequests.set(id, {
834
- resolver: (value: LsofEntry[] | PromiseLike<LsofEntry[]>) => {
835
- clearTimeout(timeout);
836
- resolve(value);
837
- lsofRequests.delete(id);
838
- },
839
- rejecter: (reason?: any) => {
840
- clearTimeout(timeout);
841
- reject(reason);
842
- lsofRequests.delete(id);
843
- },
844
- });
845
- });
899
+ const setElementValue = (text: string, selector: AccessibilitySelector): Promise<ElementResult> => {
900
+ return sendRequest<ElementResult>('setElementValue', { text, selector });
901
+ };
902
+
903
+ const typeText = (text: string, pressEnter?: boolean): Promise<void> => {
904
+ return sendRequest<void>('typeText', { text, pressEnter });
905
+ };
906
+
907
+ const pressKey = (key: string, modifiers?: string[]): Promise<void> => {
908
+ return sendRequest<void>('pressKey', { key, modifiers });
909
+ };
910
+
911
+ const launchApp = (bundleId: string): Promise<void> => {
912
+ return sendRequest<void>('launchApp', { bundleId });
913
+ };
914
+
915
+ const listApps = (): Promise<InstalledApp[]> => {
916
+ return sendRequest<InstalledApp[]>('listApps');
917
+ };
918
+
919
+ const openUrl = (url: string): Promise<void> => {
920
+ return sendRequest<void>('openUrl', { url });
921
+ };
922
+
923
+ const lsof = (): Promise<LsofEntry[]> => {
924
+ return sendRequest<LsofEntry[]>('listOpenFiles', { kind: 'unix' });
846
925
  };
847
926
 
848
927
  const simctl = (args: string[], opts: { disconnectOnExit?: boolean } = {}): SimctlExecution => {
849
- const id = 'ts-simctl-' + Date.now() + '-' + Math.random().toString(36).substring(7);
928
+ const id = generateId();
850
929
 
851
930
  const cancelCallback = () => {
852
931
  // Clean up execution tracking