mobile-debug-mcp 0.9.0 → 0.10.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 (48) hide show
  1. package/.eslintignore +5 -0
  2. package/.eslintrc.cjs +18 -0
  3. package/.github/workflows/.gitkeep +0 -0
  4. package/.github/workflows/ci.yml +63 -0
  5. package/README.md +4 -17
  6. package/dist/android/interact.js +26 -4
  7. package/dist/android/observe.js +3 -3
  8. package/dist/android/utils.js +59 -104
  9. package/dist/ios/interact.js +3 -3
  10. package/dist/ios/observe.js +4 -4
  11. package/dist/ios/utils.js +8 -8
  12. package/dist/server.js +34 -42
  13. package/dist/tools/install.js +1 -1
  14. package/dist/tools/interact.js +89 -0
  15. package/dist/tools/logs.js +2 -2
  16. package/dist/tools/observe.js +126 -0
  17. package/dist/utils/index.js +1 -0
  18. package/dist/utils/java.js +76 -0
  19. package/docs/CHANGELOG.md +21 -6
  20. package/eslint.config.cjs +36 -0
  21. package/eslint.config.js +60 -0
  22. package/package.json +8 -2
  23. package/src/android/interact.ts +24 -5
  24. package/src/android/observe.ts +3 -3
  25. package/src/android/utils.ts +65 -93
  26. package/src/ios/interact.ts +3 -4
  27. package/src/ios/observe.ts +4 -4
  28. package/src/ios/utils.ts +8 -8
  29. package/src/server.ts +37 -58
  30. package/src/tools/interact.ts +84 -0
  31. package/src/tools/observe.ts +132 -0
  32. package/src/utils/index.ts +1 -0
  33. package/src/utils/java.ts +69 -0
  34. package/test/integration/install.integration.ts +3 -3
  35. package/test/integration/run-install-android.ts +1 -1
  36. package/test/integration/run-install-ios.ts +1 -1
  37. package/test/integration/smoke-test.ts +1 -1
  38. package/test/integration/test-dist.ts +1 -1
  39. package/test/integration/test-ui-tree.ts +1 -1
  40. package/test/integration/wait_for_element_real.ts +1 -1
  41. package/test/unit/detect-java.test.ts +22 -0
  42. package/test/unit/install.test.ts +0 -6
  43. package/src/tools/app.ts +0 -46
  44. package/src/tools/devices.ts +0 -6
  45. package/src/tools/install.ts +0 -43
  46. package/src/tools/logs.ts +0 -62
  47. package/src/tools/screenshot.ts +0 -18
  48. package/src/tools/ui.ts +0 -62
package/.eslintignore ADDED
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .vscode/
5
+ coverage/
package/.eslintrc.cjs ADDED
@@ -0,0 +1,18 @@
1
+ module.exports = {
2
+ root: true,
3
+ parser: '@typescript-eslint/parser',
4
+ parserOptions: {
5
+ ecmaVersion: 2020,
6
+ sourceType: 'module',
7
+ project: './tsconfig.json'
8
+ },
9
+ plugins: ['@typescript-eslint', 'unused-imports'],
10
+ rules: {
11
+ // Use plugin to error on unused imports and provide autofix where possible
12
+ 'unused-imports/no-unused-imports': 'error',
13
+ 'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
14
+ // Disable the default TS rule to avoid duplicate warnings
15
+ '@typescript-eslint/no-unused-vars': 'off'
16
+ },
17
+ ignorePatterns: ['dist/', 'node_modules/', '.git/']
18
+ }
File without changes
@@ -0,0 +1,63 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ unit:
10
+ name: Unit tests
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Use Node.js
15
+ uses: actions/setup-node@v4
16
+ with:
17
+ node-version: '18'
18
+ - name: Install dependencies
19
+ run: npm ci
20
+ - name: Lint
21
+ run: npm run lint
22
+ - name: Build
23
+ run: npm run build
24
+ - name: Run unit tests
25
+ run: npm test
26
+
27
+ android-integration:
28
+ name: Android emulator integration (manual)
29
+ runs-on: ubuntu-latest
30
+ needs: unit
31
+ # only run integration when manually triggered to avoid long runs on every PR
32
+ if: github.event_name == 'workflow_dispatch'
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+
36
+ - name: Set up JDK 17
37
+ uses: actions/setup-java@v4
38
+ with:
39
+ distribution: 'temurin'
40
+ java-version: '17'
41
+
42
+ - name: Set up Node.js
43
+ uses: actions/setup-node@v4
44
+ with:
45
+ node-version: '18'
46
+
47
+ - name: Install dependencies
48
+ run: npm ci
49
+
50
+ - name: Start Android emulator
51
+ uses: reactivecircus/android-emulator-runner@v2
52
+ with:
53
+ api-level: 30
54
+ target: google_apis
55
+ arch: x86_64
56
+ force-avd-creation: true
57
+
58
+ - name: Build and run Android integration tests
59
+ env:
60
+ ADB_TIMEOUT: 120000
61
+ run: |
62
+ npm run build
63
+ node test/integration/run-install-android.js || true
package/README.md CHANGED
@@ -2,18 +2,6 @@
2
2
 
3
3
  A minimal, secure MCP server for AI-assisted mobile development. Build, install, and inspect Android/iOS apps from an MCP-compatible client.
4
4
 
5
- This README was shortened to keep high-level info only. Detailed tool definitions moved to docs/TOOLS.md.
6
-
7
- ## Quick start
8
-
9
- ```bash
10
- git clone https://github.com/YOUR_USERNAME/mobile-debug-mcp.git
11
- cd mobile-debug-mcp
12
- npm install
13
- npm run build
14
- npm start
15
- ```
16
-
17
5
  ## Requirements
18
6
 
19
7
  - Node.js >= 18
@@ -34,14 +22,13 @@ npm start
34
22
  }
35
23
  }
36
24
  ```
37
-
38
- > Note: Avoid using `jsonc` fences with inline comments in README code blocks to prevent syntax-highlighting issues on some renderers.
25
+ ## Usage
26
+ //TODO add examples
39
27
 
40
28
  ## Docs
41
29
 
42
- - Tools: docs/TOOLS.md (full input/response examples)
43
- - Changelog: docs/CHANGELOG.md
44
- - Tests: test/
30
+ - Tools: [Tools](docs/TOOLS.md) full input/response examples
31
+ - Changelog: [Changelog](docs/CHANGELOG.md)
45
32
 
46
33
  ## License
47
34
 
@@ -1,4 +1,5 @@
1
- import { execAdb, getAndroidDeviceMetadata, getDeviceInfo, detectJavaHome } from "./utils.js";
1
+ import { execAdb, getAndroidDeviceMetadata, getDeviceInfo, spawnAdb } from "./utils.js";
2
+ import { detectJavaHome } from "../utils/java.js";
2
3
  import { AndroidObserve } from "./observe.js";
3
4
  import { promises as fs } from "fs";
4
5
  import { spawn } from "child_process";
@@ -124,7 +125,7 @@ export class AndroidInteract {
124
125
  // Remove obvious shell profile hints; avoid touching SDKMAN symlinks or on-disk state.
125
126
  delete env.SHELL;
126
127
  }
127
- catch (e) { }
128
+ catch { }
128
129
  // If we detected a compatible JDK, instruct Gradle to use it and avoid daemon reuse
129
130
  // Prepare gradle invocation
130
131
  const gradleArgs = ['assembleDebug'];
@@ -164,8 +165,29 @@ export class AndroidInteract {
164
165
  throw new Error('Could not locate built APK after running Gradle');
165
166
  apkToInstall = built;
166
167
  }
167
- const output = await execAdb(['install', '-r', apkToInstall], deviceId);
168
- return { device: deviceInfo, installed: true, output };
168
+ // Try normal adb install with streaming attempt
169
+ try {
170
+ const res = await spawnAdb(['install', '-r', apkToInstall], deviceId);
171
+ if (res.code === 0) {
172
+ return { device: deviceInfo, installed: true, output: res.stdout };
173
+ }
174
+ // fallthrough to fallback
175
+ }
176
+ catch (e) {
177
+ // Log and continue to fallback
178
+ console.debug('[android] adb install failed, attempting push+pm fallback:', e instanceof Error ? e.message : String(e));
179
+ }
180
+ // Fallback: push APK to device and use pm install -r
181
+ const basename = path.basename(apkToInstall);
182
+ const remotePath = `/data/local/tmp/${basename}`;
183
+ await execAdb(['push', apkToInstall, remotePath], deviceId);
184
+ const pmOut = await execAdb(['shell', 'pm', 'install', '-r', remotePath], deviceId);
185
+ // cleanup remote file
186
+ try {
187
+ await execAdb(['shell', 'rm', remotePath], deviceId);
188
+ }
189
+ catch { }
190
+ return { device: deviceInfo, installed: true, output: pmOut };
169
191
  }
170
192
  catch (e) {
171
193
  return { device: deviceInfo, installed: false, error: e instanceof Error ? e.message : String(e) };
@@ -22,7 +22,7 @@ async function getScreenResolution(deviceId) {
22
22
  return { width: parseInt(match[1]), height: parseInt(match[2]) };
23
23
  }
24
24
  }
25
- catch (e) {
25
+ catch {
26
26
  // ignore
27
27
  }
28
28
  return { width: 0, height: 0 };
@@ -117,8 +117,8 @@ export class AndroidObserve {
117
117
  break; // Success
118
118
  }
119
119
  }
120
- catch (err) {
121
- console.error(`Attempt ${attempts} failed: ${err}`);
120
+ catch (e) {
121
+ console.error(`Attempt ${attempts} failed: ${e}`);
122
122
  }
123
123
  if (attempts === maxAttempts) {
124
124
  throw new Error(`Failed to retrieve valid UI dump after ${maxAttempts} attempts.`);
@@ -1,80 +1,7 @@
1
- import { spawn, execSync } from 'child_process';
2
- import { existsSync, createWriteStream, promises as fsPromises } from 'fs';
1
+ import { spawn } from 'child_process';
2
+ import { createWriteStream, promises as fsPromises } from 'fs';
3
3
  import path from 'path';
4
4
  export const ADB = process.env.ADB_PATH || 'adb';
5
- export async function detectJavaHome() {
6
- try {
7
- // If JAVA_HOME is set, validate it's Java 17
8
- if (process.env.JAVA_HOME) {
9
- try {
10
- const javaBin = path.join(process.env.JAVA_HOME, 'bin', 'java');
11
- const v = execSync(`"${javaBin}" -version`, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
12
- if (/\b17\b/.test(v) || /17\./.test(v))
13
- return process.env.JAVA_HOME;
14
- console.debug('[android] Existing JAVA_HOME does not appear to be Java 17, will search for JDK17');
15
- }
16
- catch (e) {
17
- console.debug('[android] Failed to validate existing JAVA_HOME, searching for JDK17');
18
- }
19
- }
20
- // macOS explicit path
21
- const explicit = '/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home';
22
- if (existsSync(explicit))
23
- return explicit;
24
- // Android Studio JBR candidates
25
- const jbrCandidates = [
26
- '/Applications/Android Studio.app/Contents/jbr',
27
- '/Applications/Android Studio Preview.app/Contents/jbr',
28
- '/Applications/Android Studio Preview 2022.3.app/Contents/jbr',
29
- '/Applications/Android Studio Preview 2023.1.app/Contents/jbr'
30
- ];
31
- for (const p of jbrCandidates) {
32
- const javaBin = path.join(p, 'bin', 'java');
33
- if (existsSync(javaBin)) {
34
- try {
35
- const v = execSync(`"${javaBin}" -version`, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
36
- if (/\b17\b/.test(v) || /17\./.test(v))
37
- return p;
38
- }
39
- catch { }
40
- }
41
- }
42
- // macOS /usr/libexec/java_home
43
- try {
44
- const out = execSync('/usr/libexec/java_home -v 17', { stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
45
- if (out)
46
- return out;
47
- }
48
- catch { }
49
- // macOS common JDK locations
50
- try {
51
- const homes = execSync('ls -1 /Library/Java/JavaVirtualMachines || true', { stdio: ['ignore', 'pipe', 'inherit'] }).toString().split(/\r?\n/).filter(Boolean);
52
- for (const h of homes) {
53
- if (h.toLowerCase().includes('17') || h.toLowerCase().includes('jdk-17')) {
54
- const candidate = `/Library/Java/JavaVirtualMachines/${h}/Contents/Home`;
55
- return candidate;
56
- }
57
- }
58
- }
59
- catch { }
60
- // Linux locations
61
- const linuxCandidates = [
62
- '/usr/lib/jvm/java-17-openjdk-amd64',
63
- '/usr/lib/jvm/java-17-openjdk',
64
- '/usr/lib/jvm/zulu17',
65
- '/usr/lib/jvm/temurin-17-jdk'
66
- ];
67
- for (const p of linuxCandidates) {
68
- try {
69
- if (existsSync(p))
70
- return p;
71
- }
72
- catch { }
73
- }
74
- }
75
- catch (e) { }
76
- return undefined;
77
- }
78
5
  // Helper to construct ADB args with optional device ID
79
6
  function getAdbArgs(args, deviceId) {
80
7
  if (deviceId) {
@@ -82,6 +9,24 @@ function getAdbArgs(args, deviceId) {
82
9
  }
83
10
  return args;
84
11
  }
12
+ /**
13
+ * Determine an effective ADB timeout (ms) prioritizing:
14
+ * 1. provided customTimeout
15
+ * 2. MCP_ADB_TIMEOUT or ADB_TIMEOUT env vars
16
+ * 3. sensible per-command defaults
17
+ */
18
+ function getAdbTimeout(args, customTimeout) {
19
+ if (typeof customTimeout === 'number' && !isNaN(customTimeout))
20
+ return customTimeout;
21
+ const envTimeout = parseInt(process.env.MCP_ADB_TIMEOUT || process.env.ADB_TIMEOUT || '', 10);
22
+ if (!isNaN(envTimeout) && envTimeout > 0)
23
+ return envTimeout;
24
+ if (args.includes('logcat'))
25
+ return 10000;
26
+ if (args.includes('uiautomator') && args.includes('dump'))
27
+ return 20000;
28
+ return 120000;
29
+ }
85
30
  export function execAdb(args, deviceId, options = {}) {
86
31
  const adbArgs = getAdbArgs(args, deviceId);
87
32
  return new Promise((resolve, reject) => {
@@ -101,27 +46,7 @@ export function execAdb(args, deviceId, options = {}) {
101
46
  stderr += data.toString();
102
47
  });
103
48
  }
104
- let timeoutMs;
105
- if (typeof customTimeout === 'number' && !isNaN(customTimeout)) {
106
- timeoutMs = customTimeout;
107
- }
108
- else {
109
- const envTimeout = parseInt(process.env.MCP_ADB_TIMEOUT || process.env.ADB_TIMEOUT || '', 10);
110
- if (!isNaN(envTimeout) && envTimeout > 0) {
111
- timeoutMs = envTimeout;
112
- }
113
- else {
114
- if (args.includes('logcat')) {
115
- timeoutMs = 10000;
116
- }
117
- else if (args.includes('uiautomator') && args.includes('dump')) {
118
- timeoutMs = 20000; // UI dump can be slow
119
- }
120
- else {
121
- timeoutMs = 120000; // default 2 minutes for installs and slow commands
122
- }
123
- }
124
- }
49
+ const timeoutMs = getAdbTimeout(args, customTimeout);
125
50
  const timeout = setTimeout(() => {
126
51
  child.kill();
127
52
  reject(new Error(`ADB command timed out after ${timeoutMs}ms: ${args.join(' ')}`));
@@ -143,6 +68,36 @@ export function execAdb(args, deviceId, options = {}) {
143
68
  });
144
69
  });
145
70
  }
71
+ // Spawn adb but return full streams and exit code so callers can implement fallbacks or stream output
72
+ export function spawnAdb(args, deviceId, options = {}) {
73
+ const adbArgs = getAdbArgs(args, deviceId);
74
+ return new Promise((resolve, reject) => {
75
+ const { timeout: customTimeout, ...spawnOptions } = options;
76
+ const child = spawn(ADB, adbArgs, spawnOptions);
77
+ let stdout = '';
78
+ let stderr = '';
79
+ if (child.stdout)
80
+ child.stdout.on('data', d => { stdout += d.toString(); });
81
+ if (child.stderr)
82
+ child.stderr.on('data', d => { stderr += d.toString(); });
83
+ const timeoutMs = getAdbTimeout(args, customTimeout);
84
+ const timeout = setTimeout(() => {
85
+ try {
86
+ child.kill();
87
+ }
88
+ catch { }
89
+ reject(new Error(`ADB command timed out after ${timeoutMs}ms: ${args.join(' ')}`));
90
+ }, timeoutMs);
91
+ child.on('close', (code) => {
92
+ clearTimeout(timeout);
93
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim(), code });
94
+ });
95
+ child.on('error', (err) => {
96
+ clearTimeout(timeout);
97
+ reject(err);
98
+ });
99
+ });
100
+ }
146
101
  export function getDeviceInfo(deviceId, metadata = {}) {
147
102
  return {
148
103
  platform: 'android',
@@ -169,7 +124,7 @@ export async function getAndroidDeviceMetadata(appId, deviceId) {
169
124
  resolvedDeviceId = deviceLines[0];
170
125
  }
171
126
  }
172
- catch (e) {
127
+ catch {
173
128
  // ignore and continue without resolvedDeviceId
174
129
  }
175
130
  }
@@ -182,7 +137,7 @@ export async function getAndroidDeviceMetadata(appId, deviceId) {
182
137
  const simulator = simOutput === '1';
183
138
  return { platform: 'android', id: resolvedDeviceId || 'default', osVersion, model, simulator };
184
139
  }
185
- catch (e) {
140
+ catch {
186
141
  return { platform: 'android', id: deviceId || 'default', osVersion: '', model: '', simulator: false };
187
142
  }
188
143
  }
@@ -219,7 +174,7 @@ export async function listAndroidDevices(appId) {
219
174
  }));
220
175
  return infos;
221
176
  }
222
- catch (e) {
177
+ catch {
223
178
  return [];
224
179
  }
225
180
  }
@@ -354,7 +309,7 @@ export async function startAndroidLogStream(packageName, level = 'error', device
354
309
  try {
355
310
  activeLogStreams.get(sessionId).proc.kill();
356
311
  }
357
- catch (e) { }
312
+ catch { }
358
313
  activeLogStreams.delete(sessionId);
359
314
  }
360
315
  // Start logcat process
@@ -381,14 +336,14 @@ export async function startAndroidLogStream(packageName, level = 'error', device
381
336
  stream.write(JSON.stringify(entry) + '\n');
382
337
  }
383
338
  });
384
- proc.on('close', (code) => {
339
+ proc.on('close', () => {
385
340
  stream.end();
386
341
  activeLogStreams.delete(sessionId);
387
342
  });
388
343
  activeLogStreams.set(sessionId, { proc, file });
389
344
  return { success: true, stream_started: true };
390
345
  }
391
- catch (err) {
346
+ catch {
392
347
  return { success: false, error: 'log_stream_start_failed' };
393
348
  }
394
349
  }
@@ -399,7 +354,7 @@ export async function stopAndroidLogStream(sessionId = 'default') {
399
354
  try {
400
355
  entry.proc.kill();
401
356
  }
402
- catch (e) { }
357
+ catch { }
403
358
  activeLogStreams.delete(sessionId);
404
359
  return { success: true };
405
360
  }
@@ -468,7 +423,7 @@ export async function readLogStreamLines(sessionId = 'default', limit = 100, sin
468
423
  const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false };
469
424
  return { entries, crash_summary };
470
425
  }
471
- catch (e) {
426
+ catch {
472
427
  return { entries: [], crash_summary: { crash_detected: false } };
473
428
  }
474
429
  }
@@ -166,7 +166,7 @@ export class iOSInteract {
166
166
  return { device, installed: true };
167
167
  }
168
168
  }
169
- catch (inner) {
169
+ catch {
170
170
  // fallthrough
171
171
  }
172
172
  return { device, installed: false, error: e instanceof Error ? e.message : String(e) };
@@ -233,8 +233,8 @@ export class iOSInteract {
233
233
  dataCleared: true
234
234
  };
235
235
  }
236
- catch (err) {
237
- throw new Error(`Failed to clear data for ${bundleId}: ${err instanceof Error ? err.message : String(err)}`);
236
+ catch (e) {
237
+ throw new Error(`Failed to clear data for ${bundleId}: ${e instanceof Error ? e.message : String(e)}`);
238
238
  }
239
239
  }
240
240
  }
@@ -119,10 +119,10 @@ export class iOSObserve {
119
119
  resolution: { width: 0, height: 0 },
120
120
  };
121
121
  }
122
- catch (err) {
122
+ catch (e) {
123
123
  // Ensure cleanup happens even on error
124
124
  await fs.rm(tmpFile).catch(() => { });
125
- throw new Error(`Failed to capture screenshot: ${err instanceof Error ? err.message : String(err)}`);
125
+ throw new Error(`Failed to capture screenshot: ${e instanceof Error ? e.message : String(e)}`);
126
126
  }
127
127
  }
128
128
  async getUITree(deviceId = "booted") {
@@ -174,8 +174,8 @@ export class iOSObserve {
174
174
  break; // Success
175
175
  }
176
176
  }
177
- catch (err) {
178
- console.error(`Attempt ${attempts} failed: ${err}`);
177
+ catch (e) {
178
+ console.error(`Attempt ${attempts} failed: ${e}`);
179
179
  }
180
180
  if (attempts === maxAttempts) {
181
181
  return {
package/dist/ios/utils.js CHANGED
@@ -106,7 +106,7 @@ export async function getIOSDeviceMetadata(deviceId = "booted") {
106
106
  }
107
107
  resolve(fallback);
108
108
  }
109
- catch (error) {
109
+ catch {
110
110
  resolve(fallback);
111
111
  }
112
112
  });
@@ -149,7 +149,7 @@ export async function listIOSDevices(appId) {
149
149
  }
150
150
  Promise.all(checks).then(() => resolve(out)).catch(() => resolve(out));
151
151
  }
152
- catch (e) {
152
+ catch {
153
153
  resolve([]);
154
154
  }
155
155
  });
@@ -167,7 +167,7 @@ export function _setIOSActiveLogStream(sessionId, file) {
167
167
  export function _clearIOSActiveLogStream(sessionId) {
168
168
  iosActiveLogStreams.delete(sessionId);
169
169
  }
170
- export async function startIOSLogStream(bundleId, level = 'error', deviceId = 'booted', sessionId = 'default') {
170
+ export async function startIOSLogStream(bundleId, deviceId = 'booted', sessionId = 'default') {
171
171
  try {
172
172
  // Build predicate to filter by process or subsystem
173
173
  const predicate = `process == "${bundleId}" or subsystem contains "${bundleId}"`;
@@ -176,7 +176,7 @@ export async function startIOSLogStream(bundleId, level = 'error', deviceId = 'b
176
176
  try {
177
177
  iosActiveLogStreams.get(sessionId).proc.kill();
178
178
  }
179
- catch (e) { }
179
+ catch { }
180
180
  iosActiveLogStreams.delete(sessionId);
181
181
  }
182
182
  // Start simctl log stream: xcrun simctl spawn <device> log stream --style syslog --predicate '<predicate>'
@@ -203,14 +203,14 @@ export async function startIOSLogStream(bundleId, level = 'error', deviceId = 'b
203
203
  stream.write(JSON.stringify(entry) + '\n');
204
204
  }
205
205
  });
206
- proc.on('close', (code) => {
206
+ proc.on('close', () => {
207
207
  stream.end();
208
208
  iosActiveLogStreams.delete(sessionId);
209
209
  });
210
210
  iosActiveLogStreams.set(sessionId, { proc, file });
211
211
  return { success: true, stream_started: true };
212
212
  }
213
- catch (err) {
213
+ catch {
214
214
  return { success: false, error: 'log_stream_start_failed' };
215
215
  }
216
216
  }
@@ -221,7 +221,7 @@ export async function stopIOSLogStream(sessionId = 'default') {
221
221
  try {
222
222
  entry.proc.kill();
223
223
  }
224
- catch (e) { }
224
+ catch { }
225
225
  iosActiveLogStreams.delete(sessionId);
226
226
  return { success: true };
227
227
  }
@@ -262,7 +262,7 @@ export async function readIOSLogStreamLines(sessionId = 'default', limit = 100,
262
262
  const crash_summary = crashEntry ? { crash_detected: true, exception: crashEntry.exception, sample: crashEntry.message } : { crash_detected: false };
263
263
  return { entries, crash_summary };
264
264
  }
265
- catch (e) {
265
+ catch {
266
266
  return { entries: [], crash_summary: { crash_detected: false } };
267
267
  }
268
268
  }