@sensaiorg/adapter-ios 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simctl-client.d.ts","sourceRoot":"","sources":["../../src/transport/simctl-client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,OAAO,CAAuB;gBAE1B,WAAW,EAAE,MAAM;IAI/B;;OAEG;YACW,SAAS;IAwBvB;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAI9B;;OAEG;YACW,GAAG;IAejB;;OAEG;IACG,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAapE;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,aAAa,CAAC;IAgD7C;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;IASnC;;OAEG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhD;;OAEG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD;;OAEG;IACG,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C;;OAEG;IACG,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,SAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOzG;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;IAIpC;;OAEG;IACG,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI1D;;OAEG;IACH,MAAM,IAAI,OAAO;IAIjB;;OAEG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,SAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IASnE;;OAEG;IACG,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IAMxC;;OAEG;IACG,YAAY,CAAC,OAAO,SAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IAUhD;;OAEG;IACG,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,QAAQ,IAAI,OAAO;CAGpB"}
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ /**
3
+ * SimctlClient — wraps xcrun simctl + idb commands for iOS Simulator interaction.
4
+ *
5
+ * Uses simctl for: screenshots, app lifecycle, logs, device info
6
+ * Uses idb for: tap, type, swipe (simctl doesn't support these)
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.SimctlClient = void 0;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_util_1 = require("node:util");
12
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
13
+ class SimctlClient {
14
+ simulatorId;
15
+ bootedState = false;
16
+ idbPath = null;
17
+ constructor(simulatorId) {
18
+ this.simulatorId = simulatorId;
19
+ }
20
+ /**
21
+ * Detect idb binary path asynchronously. Checks common locations.
22
+ */
23
+ async detectIdb() {
24
+ const candidates = [
25
+ process.env.IDB_PATH,
26
+ process.env.HOME + "/Library/Python/3.9/bin/idb",
27
+ process.env.HOME + "/Library/Python/3.11/bin/idb",
28
+ process.env.HOME + "/Library/Python/3.12/bin/idb",
29
+ "/usr/local/bin/idb",
30
+ "/opt/homebrew/bin/idb",
31
+ "idb", // fallback to PATH
32
+ ].filter(Boolean);
33
+ for (const candidate of candidates) {
34
+ try {
35
+ await execFileAsync(candidate, ["--help"], {
36
+ timeout: 5_000,
37
+ });
38
+ this.idbPath = candidate;
39
+ return;
40
+ }
41
+ catch {
42
+ // try next
43
+ }
44
+ }
45
+ }
46
+ /**
47
+ * Initialize idb detection. Call this before using interaction commands.
48
+ */
49
+ async initIdb() {
50
+ await this.detectIdb();
51
+ }
52
+ /**
53
+ * Execute an idb command. Throws if idb is not available.
54
+ */
55
+ async idb(subcommands, args = []) {
56
+ if (!this.idbPath) {
57
+ throw new Error("idb (Facebook's iOS Development Bridge) is not installed. " +
58
+ "Install with: pip3 install fb-idb. " +
59
+ "Also ensure idb_companion is installed: brew install idb-companion");
60
+ }
61
+ const { stdout } = await execFileAsync(this.idbPath, [
62
+ ...subcommands,
63
+ ...args,
64
+ ], { timeout: 15_000 });
65
+ return stdout;
66
+ }
67
+ /**
68
+ * Execute a simctl command.
69
+ */
70
+ async exec(subcommand, args = []) {
71
+ const { stdout } = await execFileAsync("xcrun", [
72
+ "simctl",
73
+ subcommand,
74
+ this.simulatorId,
75
+ ...args,
76
+ ], {
77
+ timeout: 30_000,
78
+ maxBuffer: 10 * 1024 * 1024,
79
+ });
80
+ return stdout;
81
+ }
82
+ /**
83
+ * Get simulator device info.
84
+ */
85
+ async getDeviceInfo() {
86
+ try {
87
+ const { stdout } = await execFileAsync("xcrun", [
88
+ "simctl", "list", "devices", "--json",
89
+ ], { timeout: 10_000 });
90
+ const data = JSON.parse(stdout);
91
+ // Find our simulator
92
+ for (const [runtime, devices] of Object.entries(data.devices)) {
93
+ for (const device of devices) {
94
+ const isTarget = this.simulatorId === "booted"
95
+ ? device.state === "Booted"
96
+ : device.udid === this.simulatorId;
97
+ if (isTarget) {
98
+ this.bootedState = device.state === "Booted";
99
+ return {
100
+ udid: device.udid,
101
+ name: device.name,
102
+ runtime,
103
+ state: device.state,
104
+ booted: this.bootedState,
105
+ };
106
+ }
107
+ }
108
+ }
109
+ return {
110
+ udid: this.simulatorId,
111
+ name: "Unknown",
112
+ runtime: "Unknown",
113
+ state: "Shutdown",
114
+ booted: false,
115
+ };
116
+ }
117
+ catch {
118
+ return {
119
+ udid: this.simulatorId,
120
+ name: "Not Available",
121
+ runtime: "Unknown",
122
+ state: "Not Available",
123
+ booted: false,
124
+ };
125
+ }
126
+ }
127
+ /**
128
+ * Take a screenshot and return as base64 PNG.
129
+ */
130
+ async screenshot() {
131
+ const tmpPath = `/tmp/sensai-ios-${Date.now()}.png`;
132
+ await this.exec("io", ["screenshot", tmpPath]);
133
+ const { readFile, unlink } = await import("node:fs/promises");
134
+ const data = await readFile(tmpPath);
135
+ await unlink(tmpPath).catch(() => { });
136
+ return data;
137
+ }
138
+ /**
139
+ * Launch an app by bundle ID.
140
+ */
141
+ async launchApp(bundleId) {
142
+ await this.exec("launch", [bundleId]);
143
+ }
144
+ /**
145
+ * Terminate an app by bundle ID.
146
+ */
147
+ async terminateApp(bundleId) {
148
+ await this.exec("terminate", [bundleId]);
149
+ }
150
+ /**
151
+ * Tap at screen coordinates using idb.
152
+ */
153
+ async tap(x, y) {
154
+ await this.idb(["ui", "tap"], [String(x), String(y)]);
155
+ }
156
+ /**
157
+ * Type text into the focused field using idb.
158
+ */
159
+ async typeText(text) {
160
+ await this.idb(["ui", "text"], [text]);
161
+ }
162
+ /**
163
+ * Swipe from one point to another using idb.
164
+ */
165
+ async swipe(startX, startY, endX, endY, durationSec = 0.3) {
166
+ await this.idb(["ui", "swipe"], [
167
+ String(startX), String(startY), String(endX), String(endY),
168
+ "--duration", String(durationSec),
169
+ ]);
170
+ }
171
+ /**
172
+ * Describe all accessibility elements on screen using idb.
173
+ */
174
+ async describeAll() {
175
+ return this.idb(["ui", "describe-all"]);
176
+ }
177
+ /**
178
+ * Describe accessibility element at a point using idb.
179
+ */
180
+ async describePoint(x, y) {
181
+ return this.idb(["ui", "describe-point"], [String(x), String(y)]);
182
+ }
183
+ /**
184
+ * Check if idb is available for interaction commands.
185
+ */
186
+ hasIdb() {
187
+ return this.idbPath !== null;
188
+ }
189
+ /**
190
+ * Get recent logs from the simulator.
191
+ */
192
+ async getLogs(predicate, duration = "30s") {
193
+ const { stdout } = await execFileAsync("xcrun", [
194
+ "simctl", "spawn", this.simulatorId, "log", "show",
195
+ "--last", duration,
196
+ "--predicate", predicate,
197
+ ], { timeout: 30_000, maxBuffer: 10 * 1024 * 1024 });
198
+ return stdout;
199
+ }
200
+ /**
201
+ * Get the resolved UDID (needed for spawn commands when using "booted").
202
+ */
203
+ async getResolvedUdid() {
204
+ if (this.simulatorId !== "booted")
205
+ return this.simulatorId;
206
+ const info = await this.getDeviceInfo();
207
+ return info.udid;
208
+ }
209
+ /**
210
+ * Get crash/exception logs from the last N minutes.
211
+ */
212
+ async getCrashLogs(minutes = 5) {
213
+ const { stdout } = await execFileAsync("xcrun", [
214
+ "simctl", "spawn", this.simulatorId, "log", "show",
215
+ "--last", `${minutes}m`,
216
+ "--predicate",
217
+ 'eventMessage contains "crash" OR eventMessage contains "exception" OR eventMessage contains "fatal" OR eventMessage contains "SIGABRT"',
218
+ ], { timeout: 30_000, maxBuffer: 10 * 1024 * 1024 });
219
+ return stdout;
220
+ }
221
+ /**
222
+ * Send key input to the simulator via idb.
223
+ */
224
+ async keyPress(keycode) {
225
+ await this.idb(["ui", "key"], [String(keycode)]);
226
+ }
227
+ isBooted() {
228
+ return this.bootedState;
229
+ }
230
+ }
231
+ exports.SimctlClient = SimctlClient;
232
+ //# sourceMappingURL=simctl-client.js.map
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@sensaiorg/adapter-ios",
3
+ "version": "0.1.0",
4
+ "description": "SensAI iOS adapter — debug iOS apps via xcrun simctl and Xcode instruments",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -b",
9
+ "clean": "rm -rf dist .tsbuildinfo"
10
+ },
11
+ "license": "Apache-2.0",
12
+ "dependencies": {
13
+ "@sensaiorg/core": "0.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.12.0",
17
+ "@types/node": "^22.0.0",
18
+ "typescript": "^5.7.0",
19
+ "zod": "^3.24.0"
20
+ }
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @sensai/adapter-ios — iOS platform adapter for SensAI.
3
+ *
4
+ * Provides debugging tools via xcrun simctl, Xcode instruments, and
5
+ * optionally an on-device debug agent for React Native inspection.
6
+ */
7
+
8
+ export { IosAdapter } from "./ios-adapter.js";
@@ -0,0 +1,104 @@
1
+ /**
2
+ * IosAdapter — implements IPlatformAdapter for iOS Simulator and devices.
3
+ *
4
+ * Uses:
5
+ * - xcrun simctl: screenshots, app lifecycle, UI hierarchy, status bar
6
+ * - Xcode instruments: performance profiling
7
+ * - Safari Web Inspector Protocol: WebView debugging
8
+ * - IDB (Facebook's iOS Development Bridge): advanced interactions
9
+ */
10
+
11
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
+ import type {
13
+ IPlatformAdapter,
14
+ IUiProvider,
15
+ ILogProvider,
16
+ IInteractionProvider,
17
+ INetworkProvider,
18
+ IPerformanceProvider,
19
+ IScreenshotProvider,
20
+ IAppStateProvider,
21
+ PlatformCapabilities,
22
+ ConnectionStatus,
23
+ } from "@sensai/core";
24
+
25
+ import { SimctlClient } from "./transport/simctl-client.js";
26
+ import { registerIosTools } from "./tools/ios-tools.js";
27
+
28
+ export interface IosAdapterConfig {
29
+ /** Specific simulator UDID (default: "booted" = first booted simulator) */
30
+ simulatorId?: string;
31
+ /** Bundle ID of the target app */
32
+ bundleId?: string;
33
+ /** Path to idb binary (optional, for advanced features) */
34
+ idbPath?: string;
35
+ }
36
+
37
+ export class IosAdapter implements IPlatformAdapter {
38
+ readonly platform = "ios" as const;
39
+ readonly displayName: string;
40
+ readonly capabilities: PlatformCapabilities = {
41
+ uiTree: true, // via XCUITest accessibility hierarchy
42
+ logs: true, // via os_log / simctl spawn log
43
+ interaction: true, // via simctl ui / idb
44
+ network: false, // Future: mitmproxy integration
45
+ performance: true, // via instruments
46
+ screenshot: true, // via simctl io screenshot
47
+ appState: false, // Future: on-device agent
48
+ hotReload: true, // Metro bundler (React Native)
49
+ evalCode: false,
50
+ };
51
+
52
+ private readonly config: Required<IosAdapterConfig>;
53
+ private readonly simctl: SimctlClient;
54
+
55
+ readonly ui: IUiProvider | null = null; // TODO: SimctlUiProvider
56
+ readonly logs: ILogProvider | null = null; // TODO: OsLogProvider
57
+ readonly interaction: IInteractionProvider | null = null; // TODO: SimctlInteractionProvider
58
+ readonly network: INetworkProvider | null = null;
59
+ readonly performance: IPerformanceProvider | null = null; // TODO: InstrumentsProvider
60
+ readonly screenshot: IScreenshotProvider | null = null; // TODO: SimctlScreenshotProvider
61
+ readonly appState: IAppStateProvider | null = null;
62
+
63
+ constructor(config: IosAdapterConfig = {}) {
64
+ this.config = {
65
+ simulatorId: config.simulatorId ?? "booted",
66
+ bundleId: config.bundleId ?? process.env.IOS_BUNDLE_ID ?? "com.chronocrew",
67
+ idbPath: config.idbPath ?? process.env.IDB_PATH ?? "idb",
68
+ };
69
+
70
+ this.displayName = `iOS Simulator (${this.config.simulatorId})`;
71
+ this.simctl = new SimctlClient(this.config.simulatorId);
72
+ }
73
+
74
+ async initialize(): Promise<ConnectionStatus> {
75
+ await this.simctl.initIdb();
76
+ const deviceInfo = await this.simctl.getDeviceInfo();
77
+
78
+ return {
79
+ platform: "ios",
80
+ device: deviceInfo.name ?? this.config.simulatorId,
81
+ connected: deviceInfo.booted,
82
+ capabilities: this.capabilities,
83
+ details: {
84
+ udid: deviceInfo.udid,
85
+ runtime: deviceInfo.runtime,
86
+ bundleId: this.config.bundleId,
87
+ },
88
+ };
89
+ }
90
+
91
+ shutdown(): void {
92
+ // No persistent connections to clean up for simctl
93
+ }
94
+
95
+ isConnected(): boolean {
96
+ return this.simctl.isBooted();
97
+ }
98
+
99
+ registerPlatformTools(server: McpServer): void {
100
+ // Use "ios_" prefix so tools don't clash with Android tool names
101
+ const prefix = "ios_";
102
+ registerIosTools(server, this.simctl, this.config.bundleId, prefix);
103
+ }
104
+ }