mobile-debug-mcp 0.20.0 → 0.21.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.
package/README.md CHANGED
@@ -2,7 +2,10 @@
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
- > **Note:** iOS support is limited currently, Please use with caution and report any issues.
5
+ > **Note:**
6
+ > * iOS only tested on simulator.
7
+ > * Flutter iOS projects not fetching logs
8
+ > * React native not tested
6
9
 
7
10
  ## Requirements
8
11
 
@@ -28,10 +31,9 @@ You will need to add ADB_PATH for Android and XCRUN_PATH and IDB_PATH for iOS.
28
31
 
29
32
  ## Usage
30
33
 
31
- Example:
32
- After a crash tell the agent the following:
33
-
34
- I have a crash on the app, can you diagnose it, fix and validate using the mcp tools available
34
+ Examples:
35
+ * I have a crash on the app, can you diagnose it, fix and validate using the mcp tools available
36
+ * Add a button, hook into the repository and confirm API request successful
35
37
 
36
38
  ## Docs
37
39
 
@@ -3,7 +3,6 @@ import { iOSInteract } from './ios.js';
3
3
  export { AndroidInteract, iOSInteract };
4
4
  import { resolveTargetDevice } from '../utils/resolve-device.js';
5
5
  import { ToolsObserve } from '../observe/index.js';
6
- const STABLE_IDLE_MS = 1000;
7
6
  export class ToolsInteract {
8
7
  static async getInteractionService(platform, deviceId) {
9
8
  const effectivePlatform = platform || 'android';
@@ -259,167 +258,204 @@ export class ToolsInteract {
259
258
  }
260
259
  return { success: false, reason: 'timeout', lastFingerprint, elapsedMs: Date.now() - start };
261
260
  }
262
- static async observeUntilHandler({ type, query, timeoutMs = 5000, pollIntervalMs = 200, platform, deviceId }) {
261
+ static async observeUntilHandler({ type = 'ui', query, timeoutMs = 30000, pollIntervalMs = 300, includeSnapshotOnFailure = true, match = 'present', stability_ms = 700, observationDelayMs = 0, platform, deviceId }) {
263
262
  const start = Date.now();
264
263
  const deadline = start + (timeoutMs || 0);
265
264
  const q = (query === null || query === undefined) ? '' : String(query);
266
- // Baseline state
265
+ // Clamp polling interval to 250-500ms for consistent behavior
266
+ const pollInterval = Math.max(250, Math.min(pollIntervalMs || 300, 500));
267
+ // Baseline state (fetch in parallel but bound to short timeouts so observation starts promptly)
267
268
  let initialFingerprint = null;
268
- try {
269
- const fpRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
270
- initialFingerprint = fpRes?.fingerprint ?? null;
271
- }
272
- catch (err) {
273
- console.error('observeUntil: error getting initial fingerprint', err);
274
- initialFingerprint = null;
275
- }
276
- // For logs, capture a baseline snapshot (count or last line) to avoid matching historical lines
277
269
  let baselineLastLine = null;
278
270
  try {
279
- const gl = await ToolsObserve.getLogsHandler({ platform, deviceId, lines: 200 });
280
- const logsArr = Array.isArray(gl.logs) ? gl.logs : [];
281
- baselineLastLine = logsArr.length ? logsArr[logsArr.length - 1] : null;
271
+ const fpPromise = ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
272
+ const logsPromise = ToolsObserve.getLogsHandler({ platform, deviceId, lines: 200 });
273
+ const withTimeout = (p, ms) => Promise.race([p, new Promise(resolve => setTimeout(() => resolve(null), ms))]);
274
+ const [fpRes, gl] = await Promise.all([withTimeout(fpPromise, 300), withTimeout(logsPromise, 500)]);
275
+ if (fpRes && typeof fpRes === 'object')
276
+ initialFingerprint = fpRes.fingerprint ?? null;
277
+ if (gl) {
278
+ const logsArr = Array.isArray(gl.logs) ? gl.logs : [];
279
+ baselineLastLine = logsArr.length ? logsArr[logsArr.length - 1] : null;
280
+ }
282
281
  }
283
282
  catch (err) {
284
- // non-fatal but surface warning to aid debugging
285
283
  try {
286
- console.warn('observeUntil: failed to get baseline logs (non-fatal):', err instanceof Error ? err.message : String(err));
284
+ console.warn('observeUntil: failed to get baseline data (non-fatal):', err instanceof Error ? err.message : String(err));
287
285
  }
288
286
  catch { }
289
287
  }
288
+ // Network-based waiting removed. Rely on UI and screen fingerprints for determinism.
290
289
  let lastChangeAt = Date.now();
291
290
  let prevFingerprint = initialFingerprint;
292
291
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
292
+ // Optional initial observation delay requested by caller
293
+ if (typeof observationDelayMs === 'number' && observationDelayMs > 0) {
294
+ try {
295
+ console.log(`observeUntil: delaying observation for ${observationDelayMs}ms`);
296
+ }
297
+ catch { }
298
+ await sleep(observationDelayMs);
299
+ }
293
300
  // Telemetry
294
301
  let pollCount = 0;
295
- let timeToMatch = null;
302
+ let matchedAt = null;
303
+ let lastObservedState = null;
304
+ let stableDuration = 0;
296
305
  let matchSource = null;
297
306
  while (Date.now() <= deadline) {
298
307
  pollCount++;
299
- try {
300
- if (type === 'ui') {
301
- // fast findElement with short timeout to avoid blocking
308
+ const now = Date.now();
309
+ // Evaluate condition per type
310
+ if (type === 'ui') {
311
+ try {
312
+ // Lightweight UI check: fetch UI tree and perform a normalized substring match to reduce overhead
302
313
  try {
303
- const found = await ToolsInteract.findElementHandler({ query: q, exact: false, timeoutMs: Math.min(500, timeoutMs || 500), platform, deviceId });
304
- if (found && found.found) {
305
- timeToMatch = Date.now() - start;
306
- // determine matchSource heuristics
307
- const el = found.element || {};
308
- if (el && el.resourceId && String(el.resourceId).toLowerCase().includes(q.toLowerCase()))
309
- matchSource = 'ui-resourceId';
310
- else if (el && el.text && String(el.text).toLowerCase() === q.toLowerCase())
311
- matchSource = 'ui-exact';
312
- else
313
- matchSource = 'ui-partial';
314
- return { success: true, type: 'ui', matched: true, details: `UI element matched '${q}'`, timestamp: Date.now(), element: found.element, telemetry: { pollCount, timeToMatch, elapsedMs: Date.now() - start, matchSource } };
315
- }
316
- }
317
- catch (err) {
318
- console.error('observeUntil(ui) find error:', err);
319
- }
320
- }
321
- else if (type === 'log') {
322
- try {
323
- // Try reading from active stream first
324
- const stream = await ToolsObserve.readLogStreamHandler({ platform, sessionId: 'default', limit: 200 });
325
- const entries = (stream && Array.isArray(stream.entries)) ? stream.entries : [];
326
- for (const ent of entries) {
327
- const msg = ent && (ent.message || ent.msg || ent) ? (ent.message || ent.msg || ent) : '';
328
- if (q && String(msg).includes(q)) {
329
- timeToMatch = Date.now() - start;
330
- matchSource = 'log-stream';
331
- return { success: true, type: 'log', matched: true, details: `Log matched '${q}'`, timestamp: Date.now(), log: { message: msg, raw: ent }, telemetry: { pollCount, timeToMatch, elapsedMs: Date.now() - start, matchSource } };
314
+ // Bound the UI tree fetch to avoid long blocking calls; prefer quick failure over hanging a poll
315
+ const withTimeout = (p, ms) => Promise.race([p, new Promise(resolve => setTimeout(() => resolve(null), ms))]);
316
+ const tree = await withTimeout(ToolsObserve.getUITreeHandler({ platform, deviceId }), Math.min(pollInterval, 500));
317
+ const elems = Array.isArray(tree && tree.elements) ? tree.elements : [];
318
+ const qnorm = q.toLowerCase();
319
+ let matched = null;
320
+ for (const el of elems) {
321
+ try {
322
+ const txt = ((el && (el.text || el.label || el.value || el.contentDescription || el.accessibilityLabel)) || '');
323
+ if (!txt)
324
+ continue;
325
+ if (String(txt).toLowerCase().includes(qnorm)) {
326
+ matched = el;
327
+ break;
328
+ }
329
+ }
330
+ catch {
331
+ continue;
332
332
  }
333
333
  }
334
- // Fallback to snapshot logs
335
- const gl = await ToolsObserve.getLogsHandler({ platform, deviceId, lines: 200 });
336
- const logsArr = Array.isArray(gl && gl.logs) ? gl.logs : [];
337
- // Only consider new lines after baselineLastLine when possible
338
- let startIndex = 0;
339
- if (baselineLastLine) {
340
- const idx = logsArr.lastIndexOf(baselineLastLine);
341
- startIndex = idx >= 0 ? idx + 1 : 0;
342
- }
343
- for (let i = startIndex; i < logsArr.length; i++) {
344
- const line = logsArr[i];
345
- if (q && String(line).includes(q)) {
346
- timeToMatch = Date.now() - start;
347
- matchSource = 'log-snapshot';
348
- return { success: true, type: 'log', matched: true, details: `Log matched '${q}'`, timestamp: Date.now(), log: { message: line }, telemetry: { pollCount, timeToMatch, elapsedMs: Date.now() - start, matchSource } };
334
+ const isPresent = !!matched;
335
+ const conditionTrue = (match === 'present') ? isPresent : !isPresent;
336
+ if (conditionTrue) {
337
+ if (matchedAt === null)
338
+ matchedAt = Date.now();
339
+ stableDuration = Date.now() - matchedAt;
340
+ lastObservedState = true;
341
+ if (stableDuration >= stability_ms) {
342
+ matchSource = 'ui-tree-' + (match === 'present' ? 'present' : 'absent');
343
+ const element = isPresent ? matched : null;
344
+ const now2 = Date.now();
345
+ return { success: true, condition: match, query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: stableDuration, matchedElement: element, matchSource, timestamp: now2, type: 'ui', observed_state: lastObservedState ?? null };
349
346
  }
350
347
  }
348
+ else {
349
+ matchedAt = null;
350
+ stableDuration = 0;
351
+ lastObservedState = false;
352
+ }
351
353
  }
352
354
  catch (err) {
353
- console.error('observeUntil(log) error:', err);
355
+ console.error('observeUntil(ui) tree error:', err);
354
356
  }
355
357
  }
356
- else if (type === 'screen') {
357
- try {
358
- const fpRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
359
- const fp = fpRes?.fingerprint ?? null;
360
- if (fp !== null && fp !== undefined && fp !== initialFingerprint) {
361
- if (q) {
362
- // optionally validate query against new screen context
363
- try {
364
- const found = await ToolsInteract.findElementHandler({ query: q, exact: false, timeoutMs: Math.min(500, timeoutMs || 500), platform, deviceId });
365
- if (found && found.found) {
366
- timeToMatch = Date.now() - start;
367
- matchSource = 'screen-validated-ui';
368
- return { success: true, type: 'screen', matched: true, details: `Screen changed and query matched on new screen`, timestamp: Date.now(), newFingerprint: fp, element: found.element, telemetry: { pollCount, timeToMatch, elapsedMs: Date.now() - start, matchSource } };
369
- }
370
- }
371
- catch (err) {
372
- console.error('observeUntil(screen) find error:', err);
373
- }
374
- // If query provided but not matched yet, continue polling until timeout
375
- }
376
- else {
377
- timeToMatch = Date.now() - start;
378
- matchSource = 'screen-fingerprint';
379
- return { success: true, type: 'screen', matched: true, details: 'Screen fingerprint changed', timestamp: Date.now(), newFingerprint: fp, telemetry: { pollCount, timeToMatch, elapsedMs: Date.now() - start, matchSource } };
380
- }
358
+ catch (err) {
359
+ console.error('observeUntil(ui) find error:', err);
360
+ }
361
+ }
362
+ else if (type === 'log') {
363
+ try {
364
+ // Logs: presence semantics only (match 'present'). Stability not applicable (immediate)
365
+ const stream = await ToolsObserve.readLogStreamHandler({ platform, sessionId: 'default', limit: 200 });
366
+ const entries = (stream && Array.isArray(stream.entries)) ? stream.entries : [];
367
+ for (const ent of entries) {
368
+ const msg = ent && (ent.message || ent.msg || ent) ? (ent.message || ent.msg || ent) : '';
369
+ if (q && String(msg).includes(q)) {
370
+ const now2 = Date.now();
371
+ return { success: true, condition: 'present', query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: 0, matchedLog: { message: msg, raw: ent }, matchSource: 'log-stream', timestamp: now2, type: 'log', observed_state: true };
381
372
  }
382
373
  }
383
- catch (err) {
384
- console.error('observeUntil(screen) error:', err);
374
+ const gl = await ToolsObserve.getLogsHandler({ platform, deviceId, lines: 200 });
375
+ const logsArr = Array.isArray(gl && gl.logs) ? gl.logs : [];
376
+ let startIndex = 0;
377
+ if (baselineLastLine) {
378
+ const idx = logsArr.lastIndexOf(baselineLastLine);
379
+ startIndex = idx >= 0 ? idx + 1 : 0;
380
+ }
381
+ for (let i = startIndex; i < logsArr.length; i++) {
382
+ const line = logsArr[i];
383
+ if (q && String(line).includes(q)) {
384
+ const now2 = Date.now();
385
+ return { success: true, condition: 'present', query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: 0, matchedLog: { message: line }, matchSource: 'log-snapshot', timestamp: now2, type: 'log', observed_state: true };
386
+ }
385
387
  }
386
388
  }
387
- else if (type === 'idle') {
388
- try {
389
- const fpRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
390
- const fp = fpRes?.fingerprint ?? null;
391
- if (fp !== prevFingerprint) {
392
- prevFingerprint = fp;
393
- lastChangeAt = Date.now();
389
+ catch (err) {
390
+ console.error('observeUntil(log) error:', err);
391
+ }
392
+ }
393
+ else if (type === 'screen') {
394
+ try {
395
+ const fpRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
396
+ const fp = fpRes?.fingerprint ?? null;
397
+ if (fp !== null && fp !== undefined && fp !== initialFingerprint) {
398
+ // when screen changed, require stability_ms where fingerprint remains the same
399
+ if (matchedAt === null)
400
+ matchedAt = now;
401
+ const confirmFp = (await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }))?.fingerprint ?? null;
402
+ if (confirmFp === fp) {
403
+ stableDuration = Date.now() - matchedAt;
404
+ lastObservedState = true;
405
+ if (stableDuration >= stability_ms) {
406
+ const now2 = Date.now();
407
+ return { success: true, condition: 'present', query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: stableDuration, newFingerprint: fp, matchSource: 'screen-fingerprint', timestamp: now2, type: 'screen', observed_state: lastObservedState ?? null };
408
+ }
394
409
  }
395
410
  else {
396
- if (Date.now() - lastChangeAt >= STABLE_IDLE_MS) {
397
- timeToMatch = Date.now() - start;
398
- matchSource = 'idle-stable';
399
- return { success: true, type: 'idle', matched: true, details: `UI stable for ${STABLE_IDLE_MS}ms`, timestamp: Date.now(), fingerprint: fp, telemetry: { pollCount, timeToMatch, elapsedMs: Date.now() - start, matchSource } };
400
- }
411
+ matchedAt = null;
412
+ stableDuration = 0;
413
+ lastObservedState = false;
401
414
  }
402
415
  }
403
- catch (err) {
404
- console.error('observeUntil(idle) error:', err);
405
- }
416
+ }
417
+ catch (err) {
418
+ console.error('observeUntil(screen) error:', err);
406
419
  }
407
420
  }
408
- catch (err) {
409
- console.error('observeUntil: unexpected error', err);
421
+ else if (type === 'idle') {
422
+ try {
423
+ const fpRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
424
+ const fp = fpRes?.fingerprint ?? null;
425
+ if (fp !== prevFingerprint) {
426
+ prevFingerprint = fp;
427
+ lastChangeAt = Date.now();
428
+ matchedAt = null;
429
+ stableDuration = 0;
430
+ lastObservedState = false;
431
+ }
432
+ else {
433
+ const idleMs = Date.now() - lastChangeAt;
434
+ lastObservedState = true;
435
+ if (idleMs >= stability_ms) {
436
+ const now2 = Date.now();
437
+ return { success: true, condition: 'present', query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: idleMs, matchSource: 'idle-stable', timestamp: now2, type: 'idle', observed_state: lastObservedState ?? null };
438
+ }
439
+ }
440
+ }
441
+ catch (err) {
442
+ console.error('observeUntil(idle) error:', err);
443
+ }
410
444
  }
411
445
  // Respect poll interval and avoid tight loop
412
- await sleep(pollIntervalMs || 200);
446
+ await sleep(pollInterval);
413
447
  }
414
- // On timeout, capture a failure snapshot to aid debugging (best-effort)
448
+ // On timeout, optionally capture a failure snapshot to aid debugging (best-effort)
415
449
  let snapshot = null;
416
- try {
417
- snapshot = await ToolsObserve.captureDebugSnapshotHandler({ reason: `observe_until timeout for ${type}`, includeLogs: true, platform, deviceId });
418
- }
419
- catch (err) {
420
- snapshot = { error: err instanceof Error ? err.message : String(err) };
450
+ if (includeSnapshotOnFailure) {
451
+ try {
452
+ snapshot = await ToolsObserve.captureDebugSnapshotHandler({ reason: `observe_until timeout for ${type}`, includeLogs: true, platform, deviceId });
453
+ }
454
+ catch (err) {
455
+ snapshot = { error: err instanceof Error ? err.message : String(err) };
456
+ }
421
457
  }
422
458
  const elapsed = Date.now() - start;
423
- return { success: false, error: 'Timeout waiting for condition', type, timeoutMs, telemetry: { pollCount, elapsedMs: elapsed, matchSource: null }, snapshot };
459
+ return { success: false, condition: match, query: q, poll_count: pollCount, duration_ms: elapsed, stable_duration_ms: stableDuration, error: 'Timeout waiting for condition', snapshot, observed_state: lastObservedState ?? null };
424
460
  }
425
461
  }
@@ -64,10 +64,13 @@ export class AndroidManage {
64
64
  const spawnOpts = { cwd: apkPath, env };
65
65
  if (useWrapper) {
66
66
  await fs.chmod(wrapperPath, 0o755).catch(() => { });
67
+ // Run wrapper directly to avoid shell splitting of args
68
+ spawnOpts.shell = false;
69
+ }
70
+ else {
71
+ // Execute gradle directly without a shell so paths with spaces are preserved
67
72
  spawnOpts.shell = false;
68
73
  }
69
- else
70
- spawnOpts.shell = true;
71
74
  const proc = spawn(execCmd, gradleArgs, spawnOpts);
72
75
  let stderr = '';
73
76
  await new Promise((resolve, reject) => {
@@ -0,0 +1,43 @@
1
+ import { ToolsObserve } from '../observe/index.js'
2
+ import { ToolsInteract } from '../interact/index.js'
3
+
4
+ async function main() {
5
+ const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
6
+ const device = process.argv.includes('--device') ? process.argv[process.argv.indexOf('--device')+1] : undefined
7
+
8
+ console.log('Tap then capture UI tree snapshots')
9
+ try {
10
+ // Tap generate
11
+ const found = await (await import('../interact/index.js')).ToolsInteract.findElementHandler({ query: 'Generate Session', platform, timeoutMs: 3000, exact: true, deviceId: device })
12
+ console.log('Found generate?', !!(found && found.found), JSON.stringify(found && found.element || {}))
13
+ if (found && found.found && found.element && found.element.tapCoordinates) {
14
+ await (await import('../interact/index.js')).ToolsInteract.tapHandler({ platform, x: found.element.tapCoordinates.x, y: found.element.tapCoordinates.y, deviceId: device })
15
+ console.log('Tapped Generate at', found.element.tapCoordinates)
16
+ } else {
17
+ console.log('Could not find generate element — aborting capture')
18
+ process.exit(2)
19
+ }
20
+
21
+ // Capture UI trees and screenshot repeatedly for 20s
22
+ const start = Date.now()
23
+ const outDir = '/tmp/mcp_ui_captures'
24
+ try { await import('fs').then(fs=>fs.promises.mkdir(outDir,{recursive:true})) } catch(e){}
25
+ let i = 0
26
+ while (Date.now() - start < 20000) {
27
+ const snap = await ToolsObserve.getUITreeHandler({ platform, deviceId: device })
28
+ const fname = `${outDir}/ui_${Date.now()}_${i}.json`
29
+ await import('fs').then(fs=>fs.promises.writeFile(fname, JSON.stringify(snap,null,2)))
30
+ console.log('Saved', fname)
31
+ i++
32
+ await new Promise(r=>setTimeout(r,500))
33
+ }
34
+
35
+ console.log('Capture complete')
36
+ process.exit(0)
37
+ } catch (e) {
38
+ console.error('Error during capture:', e)
39
+ process.exit(1)
40
+ }
41
+ }
42
+
43
+ main()
@@ -0,0 +1,18 @@
1
+ import { ToolsInteract } from '../interact/index.js'
2
+
3
+ async function main() {
4
+ const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
5
+ const device = process.argv.includes('--device') ? process.argv[process.argv.indexOf('--device')+1] : undefined
6
+ const timeout = parseInt(process.argv.includes('--timeout') ? process.argv[process.argv.indexOf('--timeout')+1] : '120000')
7
+ console.log('Running observeUntil for Play session, platform=', platform, 'device=', device)
8
+ try {
9
+ const res = await ToolsInteract.observeUntilHandler({ type: 'ui', query: 'Play session', match: 'present', timeoutMs: timeout, pollIntervalMs: 300, stability_ms: 700, platform, deviceId: device })
10
+ console.log('observeUntil result:', JSON.stringify(res, null, 2))
11
+ process.exit(res && res.success ? 0 : 2)
12
+ } catch (e) {
13
+ console.error('Error running observeUntil:', e)
14
+ process.exit(1)
15
+ }
16
+ }
17
+
18
+ main()
@@ -0,0 +1,38 @@
1
+ import { ToolsObserve } from '../observe/index.js'
2
+
3
+ async function main() {
4
+ const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
5
+ const device = process.argv.includes('--device') ? process.argv[process.argv.indexOf('--device')+1] : undefined
6
+ const timeout = parseInt(process.argv.includes('--timeout') ? process.argv[process.argv.indexOf('--timeout')+1] : '120000')
7
+ const start = Date.now()
8
+ const out = []
9
+ console.log('Searching UI trees for elements containing "play" (case-insensitive)')
10
+ while (Date.now() - start < timeout) {
11
+ try {
12
+ const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId: device })
13
+ const elements = Array.isArray((tree && tree.elements)) ? tree.elements : []
14
+ const matches = []
15
+ const norm = (s)=> s ? String(s).toLowerCase() : ''
16
+ for (const el of elements) {
17
+ const text = norm(el.text)
18
+ const content = norm(el.contentDescription || el.contentDesc || el.accessibilityLabel)
19
+ const rid = norm(el.resourceId || el.id)
20
+ const cls = norm(el.type || el.class)
21
+ if (text.includes('play') || content.includes('play') || rid.includes('play') || cls.includes('play') || text.includes('session') || content.includes('session')) {
22
+ matches.push({ el: el, foundIn: { text: text.includes('play')? 'text': text.includes('session')? 'text-session': null } })
23
+ }
24
+ }
25
+ if (matches.length) {
26
+ console.log('Matches found:', JSON.stringify(matches, null, 2))
27
+ out.push({ ts: Date.now(), matches })
28
+ break
29
+ }
30
+ } catch (e) { console.error('UITree fetch err:', e) }
31
+ await new Promise(r=>setTimeout(r, 500))
32
+ }
33
+ console.log('Search complete. Results count=', out.length)
34
+ if (out.length > 0) process.exit(0)
35
+ process.exit(2)
36
+ }
37
+
38
+ main()
@@ -0,0 +1,20 @@
1
+ import { ToolsObserve } from '../observe/index.js'
2
+
3
+ async function main() {
4
+ const argv = process.argv.slice(2)
5
+ const opts = {}
6
+ for (let i = 0; i < argv.length; i++) {
7
+ if (argv[i] === '--platform') opts.platform = argv[++i]
8
+ else if (argv[i] === '--deviceId') opts.deviceId = argv[++i]
9
+ }
10
+ try {
11
+ const tree = await ToolsObserve.getUITreeHandler({ platform: opts.platform || 'android', deviceId: opts.deviceId })
12
+ console.log(JSON.stringify(tree, null, 2))
13
+ process.exit(0)
14
+ } catch (e) {
15
+ console.error('failed to get ui tree:', e instanceof Error ? e.message : String(e))
16
+ process.exit(2)
17
+ }
18
+ }
19
+
20
+ main()
@@ -0,0 +1,32 @@
1
+ import { ToolsObserve } from '../observe/index.js'
2
+ import { ToolsInteract } from '../interact/index.js'
3
+
4
+ async function main() {
5
+ console.log('Starting log stream for com.ideamechanics.modul8')
6
+ try {
7
+ const start = await ToolsObserve.startLogStreamHandler({ platform: 'ios', packageName: 'com.ideamechanics.modul8', sessionId: 'test-session' })
8
+ console.log('startLogStream result:', JSON.stringify(start))
9
+ } catch (e) {
10
+ console.error('startLogStream failed:', e instanceof Error ? e.message : String(e))
11
+ }
12
+
13
+ console.log('\nPlease press the Generate Session button in the app now. Observing for network idle (15s timeout)...')
14
+
15
+ try {
16
+ const res = await ToolsInteract.observeUntilHandler({ type: 'network_idle', timeoutMs: 15000, pollIntervalMs: 300, platform: 'ios' })
17
+ console.log('observeUntil result:')
18
+ console.log(JSON.stringify(res, null, 2))
19
+ } catch (e) {
20
+ console.error('observeUntil failed:', e instanceof Error ? e.message : String(e))
21
+ }
22
+
23
+ console.log('\nStopping log stream (best-effort)')
24
+ try {
25
+ const stop = await ToolsObserve.stopLogStreamHandler({ platform: 'ios', sessionId: 'test-session' })
26
+ console.log('stopLogStream result:', JSON.stringify(stop))
27
+ } catch (e) { console.error('stopLogStream failed:', e) }
28
+
29
+ process.exit(0)
30
+ }
31
+
32
+ main().catch(err => { console.error(err); process.exit(1) })
@@ -0,0 +1,90 @@
1
+ import { ToolsObserve } from '../observe/index.js'
2
+ import { ToolsInteract } from '../interact/index.js'
3
+
4
+ async function main() {
5
+ const bundle = 'com.ideamechanics.modul8'
6
+ const sessionId = 'press-test'
7
+ console.log('Starting log stream for', bundle)
8
+ try {
9
+ const start = await ToolsObserve.startLogStreamHandler({ platform: 'ios', packageName: bundle, sessionId })
10
+ console.log('startLogStream result:', JSON.stringify(start))
11
+ } catch (e) {
12
+ console.error('startLogStream failed:', e instanceof Error ? e.message : String(e))
13
+ }
14
+
15
+ // Try to find the Generate Session element
16
+ console.log('\nSearching for "Generate Session" UI element...')
17
+ let el = null
18
+ try {
19
+ const found = await ToolsInteract.findElementHandler({ query: 'Generate Session', exact: false, timeoutMs: 3000, platform: 'ios' })
20
+ const foundFlag = found && typeof (found).found !== 'undefined' ? found.found : false
21
+ console.log('findElementHandler:', foundFlag ? 'found' : 'not found')
22
+ if (foundFlag) {
23
+ el = found.element
24
+ console.log('Matched element:', JSON.stringify(el))
25
+ }
26
+ } catch (e) {
27
+ console.error('findElementHandler error:', e instanceof Error ? e.message : String(e))
28
+ }
29
+
30
+ // If found, tap it. Otherwise, try tapping a best-effort coordinate in center of screen
31
+ try {
32
+ if (el && el.tapCoordinates) {
33
+ console.log('Tapping matched element at', JSON.stringify(el.tapCoordinates))
34
+ await ToolsInteract.tapHandler({ platform: 'ios', x: el.tapCoordinates.x, y: el.tapCoordinates.y })
35
+ } else {
36
+ console.log('Element not found; attempting center tap as fallback')
37
+ // attempt a center tap (may be harmless)
38
+ await ToolsInteract.tapHandler({ platform: 'ios', x: 200, y: 400 })
39
+ }
40
+ } catch (e) {
41
+ console.error('Tap failed:', e instanceof Error ? e.message : String(e))
42
+ }
43
+
44
+ console.log('\nObserving until network_idle (30s timeout)')
45
+ // Start a parallel log monitor to print network-like lines as they appear
46
+ // Print all log lines originating from the app bundle for inspection
47
+ let seenIds = new Set()
48
+ let monitorActive = true
49
+ const monitor = (async () => {
50
+ while (monitorActive) {
51
+ try {
52
+ const stream = await ToolsObserve.readLogStreamHandler({ platform: 'ios', sessionId, limit: 200 })
53
+ const entries = (stream && Array.isArray(stream.entries)) ? stream.entries : []
54
+ for (const ent of entries) {
55
+ const id = JSON.stringify(ent).slice(0,200)
56
+ if (seenIds.has(id)) continue
57
+ seenIds.add(id)
58
+ const msg = ent && (ent.message || ent.msg || ent) ? (ent.message || ent.msg || ent) : ''
59
+ // Print only lines that reference the app bundle
60
+ if (String(msg).includes('Modul8') || String(msg).includes('modul8') || (ent && ent.process && String(ent.process).toLowerCase().includes('modul8'))) {
61
+ console.log('[APP LOG]', new Date().toISOString(), JSON.stringify({ message: msg }))
62
+ }
63
+ }
64
+ } catch (e) { console.error('log monitor error:', e instanceof Error ? e.message : String(e)) }
65
+ await new Promise(r => setTimeout(r, 300))
66
+ }
67
+ })()
68
+
69
+ let observeResult = null
70
+ try {
71
+ observeResult = await ToolsInteract.observeUntilHandler({ type: 'network_idle', timeoutMs: 30000, pollIntervalMs: 300, platform: 'ios', includeSnapshotOnFailure: true })
72
+ console.log('observeUntil result:')
73
+ console.log(JSON.stringify(observeResult, null, 2))
74
+ } catch (e) {
75
+ console.error('observeUntil failed:', e instanceof Error ? e.message : String(e))
76
+ }
77
+
78
+ // Stop monitor
79
+ monitorActive = false
80
+
81
+ console.log('\nStopping log stream (best-effort)')
82
+ try {
83
+ const stop = await ToolsObserve.stopLogStreamHandler({ platform: 'ios', sessionId })
84
+ console.log('stopLogStream result:', JSON.stringify(stop))
85
+ } catch (e) { console.error('stopLogStream failed:', e) }
86
+
87
+ process.exit(0)
88
+ }
89
+
90
+ main().catch(err => { console.error(err); process.exit(1) })