mobile-debug-mcp 0.24.1 → 0.24.2
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/dist/interact/index.js +120 -15
- package/dist/manage/android.js +24 -4
- package/dist/manage/ios.js +27 -3
- package/dist/server/common.js +4 -2
- package/dist/server/tool-handlers.js +19 -2
- package/dist/system/index.js +53 -2
- package/docs/CHANGELOG.md +5 -0
- package/docs/tools/interact.md +73 -7
- package/docs/tools/manage.md +16 -1
- package/docs/tools/system.md +23 -1
- package/package.json +1 -1
- package/src/interact/index.ts +121 -15
- package/src/manage/android.ts +24 -4
- package/src/manage/ios.ts +27 -3
- package/src/server/common.ts +8 -2
- package/src/server/tool-handlers.ts +19 -2
- package/src/system/index.ts +57 -2
- package/src/types.ts +36 -0
- package/test/unit/interact/expect_tools.test.ts +29 -2
- package/test/unit/interact/wait_for_screen_change.test.ts +7 -3
- package/test/unit/interact/wait_for_ui_selector_matching.test.ts +19 -0
- package/test/unit/server/response_shapes.test.ts +20 -3
- package/test/unit/system/get_system_status.test.ts +2 -0
- package/test/unit/system/system_status.test.ts +6 -0
package/docs/tools/manage.md
CHANGED
|
@@ -123,10 +123,23 @@ start_app response example:
|
|
|
123
123
|
"action_id": "start_app_1710000000000_1",
|
|
124
124
|
"timestamp": 1710000000000,
|
|
125
125
|
"action_type": "start_app",
|
|
126
|
+
"device": { "platform": "android", "id": "emulator-5554", "osVersion": "14", "model": "Pixel", "simulator": true },
|
|
126
127
|
"target": { "selector": { "appId": "com.example.app" }, "resolved": null },
|
|
127
128
|
"success": true,
|
|
128
129
|
"ui_fingerprint_before": "fp_before",
|
|
129
|
-
"ui_fingerprint_after": "fp_after"
|
|
130
|
+
"ui_fingerprint_after": "fp_after",
|
|
131
|
+
"details": {
|
|
132
|
+
"launch_time_ms": 1000,
|
|
133
|
+
"output": "Events injected: 1",
|
|
134
|
+
"device_id": "emulator-5554",
|
|
135
|
+
"observed_app": {
|
|
136
|
+
"appId": "com.example.app",
|
|
137
|
+
"package": "com.example.app",
|
|
138
|
+
"activity": "com.example.app.MainActivity",
|
|
139
|
+
"screen": "MainActivity",
|
|
140
|
+
"matchedTarget": true
|
|
141
|
+
}
|
|
142
|
+
}
|
|
130
143
|
}
|
|
131
144
|
```
|
|
132
145
|
|
|
@@ -137,5 +150,7 @@ terminate_app and reset_app_data return operation-specific lifecycle results ins
|
|
|
137
150
|
Notes:
|
|
138
151
|
|
|
139
152
|
- `start_app` and `restart_app` report execution success, not outcome correctness.
|
|
153
|
+
- Use `details.observed_app` as the quick decision signal for what the tool actually saw after launch.
|
|
154
|
+
- Android launch feedback usually includes package/activity matching; iOS launch feedback includes launch output and PID when available.
|
|
140
155
|
- When the landing screen is known, use `expect_screen` as the final verification step.
|
|
141
156
|
- If launch timing is uncertain, insert `wait_for_screen_change` before `expect_screen`.
|
package/docs/tools/system.md
CHANGED
|
@@ -16,6 +16,7 @@ Response (example):
|
|
|
16
16
|
```json
|
|
17
17
|
{
|
|
18
18
|
"success": true,
|
|
19
|
+
"status": "ready",
|
|
19
20
|
"adbAvailable": true,
|
|
20
21
|
"adbVersion": "8.1.0",
|
|
21
22
|
"devices": 1,
|
|
@@ -25,7 +26,26 @@ Response (example):
|
|
|
25
26
|
"issues": [],
|
|
26
27
|
"appInstalled": true,
|
|
27
28
|
"iosAvailable": true,
|
|
28
|
-
"iosDevices": 1
|
|
29
|
+
"iosDevices": 1,
|
|
30
|
+
"summary": {
|
|
31
|
+
"overall": "ready",
|
|
32
|
+
"android": {
|
|
33
|
+
"ready": true,
|
|
34
|
+
"summary": "1 Android device(s) connected; log access available",
|
|
35
|
+
"blockers": []
|
|
36
|
+
},
|
|
37
|
+
"ios": {
|
|
38
|
+
"ready": true,
|
|
39
|
+
"summary": "1 iOS simulator(s) booted",
|
|
40
|
+
"blockers": []
|
|
41
|
+
},
|
|
42
|
+
"gradle": {
|
|
43
|
+
"ready": true,
|
|
44
|
+
"summary": "No explicit Gradle JDK override detected",
|
|
45
|
+
"blockers": [],
|
|
46
|
+
"suggestedFixes": []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
29
49
|
}
|
|
30
50
|
```
|
|
31
51
|
|
|
@@ -39,6 +59,8 @@ Checks performed (fast, best-effort):
|
|
|
39
59
|
|
|
40
60
|
Behavior notes:
|
|
41
61
|
- Always returns structured JSON and never throws; any failures are surfaced in the `issues` array.
|
|
62
|
+
- `status` gives a quick overall gate: `ready`, `degraded`, or `blocked`.
|
|
63
|
+
- `summary.android`, `summary.ios`, and `summary.gradle` provide the fastest path to the actual blocker category.
|
|
42
64
|
- Designed to be fast (<~1s probes where possible); startup callers may prefer a `fastMode` variant that only checks existence.
|
|
43
65
|
- Useful to call at the start of an agent session to gate subsequent actions.
|
|
44
66
|
|
package/package.json
CHANGED
package/src/interact/index.ts
CHANGED
|
@@ -490,6 +490,12 @@ export class ToolsInteract {
|
|
|
490
490
|
|
|
491
491
|
static async waitForUIHandler({ selector, condition = 'exists', timeout_ms = 60000, poll_interval_ms = 300, match, retry = { max_attempts: 1, backoff_ms: 0 }, platform, deviceId }: { selector?: { text?: string, resource_id?: string, accessibility_id?: string, contains?: boolean }, condition?: 'exists'|'not_exists'|'visible'|'clickable', timeout_ms?: number, poll_interval_ms?: number, match?: { index?: number }, retry?: { max_attempts?: number, backoff_ms?: number }, platform?: 'android'|'ios', deviceId?: string }) {
|
|
492
492
|
const overallStart = Date.now()
|
|
493
|
+
const requestedIndex = typeof match?.index === 'number' ? match.index : null
|
|
494
|
+
const requested = {
|
|
495
|
+
selector: selector ?? {},
|
|
496
|
+
condition,
|
|
497
|
+
match: requestedIndex === null ? null : { index: requestedIndex }
|
|
498
|
+
}
|
|
493
499
|
|
|
494
500
|
// Validate selector: require at least one non-empty field (text, resource_id, or accessibility_id)
|
|
495
501
|
const hasText = typeof selector?.text === 'string' && selector.text.trim().length > 0;
|
|
@@ -503,18 +509,20 @@ export class ToolsInteract {
|
|
|
503
509
|
code: 'INVALID_SELECTOR',
|
|
504
510
|
message: 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id'
|
|
505
511
|
},
|
|
506
|
-
metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }
|
|
512
|
+
metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 },
|
|
513
|
+
requested,
|
|
514
|
+
observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null }
|
|
507
515
|
};
|
|
508
516
|
}
|
|
509
517
|
|
|
510
518
|
// Validate condition
|
|
511
519
|
if (!['exists','not_exists','visible','clickable'].includes(condition)) {
|
|
512
|
-
return { status: 'timeout', error: { code: 'INVALID_CONDITION', message: `Unsupported condition: ${condition}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } }
|
|
520
|
+
return { status: 'timeout', error: { code: 'INVALID_CONDITION', message: `Unsupported condition: ${condition}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }, requested, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } }
|
|
513
521
|
}
|
|
514
522
|
|
|
515
523
|
// Platform check
|
|
516
524
|
if (platform && !['android','ios'].includes(platform)) {
|
|
517
|
-
return { status: 'timeout', error: { code: 'PLATFORM_NOT_SUPPORTED', message: `Unsupported platform: ${platform}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 } }
|
|
525
|
+
return { status: 'timeout', error: { code: 'PLATFORM_NOT_SUPPORTED', message: `Unsupported platform: ${platform}` }, metrics: { latency_ms: Date.now() - overallStart, poll_count: 0, attempts: 0 }, requested, observed: { matched_count: 0, condition_satisfied: false, selected_index: null, last_matched_element: null } }
|
|
518
526
|
}
|
|
519
527
|
|
|
520
528
|
const effectivePoll = Math.max(50, Math.min(poll_interval_ms || 300, 2000))
|
|
@@ -523,6 +531,9 @@ export class ToolsInteract {
|
|
|
523
531
|
|
|
524
532
|
let attempts = 0
|
|
525
533
|
let totalPollCount = 0
|
|
534
|
+
let lastMatchedCount = 0
|
|
535
|
+
let lastMatchedElement: ActionTargetResolved | null = null
|
|
536
|
+
let lastConditionSatisfied = false
|
|
526
537
|
|
|
527
538
|
// Precompute normalized selector values and helpers (constant across polls)
|
|
528
539
|
const normalize = ToolsInteract._normalize
|
|
@@ -623,19 +634,29 @@ export class ToolsInteract {
|
|
|
623
634
|
} else conditionMet = false
|
|
624
635
|
}
|
|
625
636
|
|
|
637
|
+
const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android')
|
|
638
|
+
const resolvedDeviceId = tree?.device?.id || deviceId
|
|
639
|
+
lastMatchedCount = matchedCount
|
|
640
|
+
lastConditionSatisfied = conditionMet
|
|
641
|
+
lastMatchedElement = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null
|
|
642
|
+
|
|
626
643
|
if (conditionMet) {
|
|
627
644
|
const now = Date.now()
|
|
628
645
|
const latency_ms = now - overallStart
|
|
629
|
-
|
|
630
|
-
const resolvedPlatform = tree?.device?.platform === 'ios' ? 'ios' : (platform || 'android')
|
|
631
|
-
const resolvedDeviceId = tree?.device?.id || deviceId
|
|
632
|
-
const outEl = matchedElement ? ToolsInteract._buildResolvedElement(resolvedPlatform, resolvedDeviceId, matchedElement.el, matchedElement.idx) : null
|
|
646
|
+
const outEl = lastMatchedElement
|
|
633
647
|
|
|
634
648
|
return {
|
|
635
649
|
status: 'success',
|
|
636
650
|
matched: matchedCount,
|
|
637
651
|
element: outEl,
|
|
638
|
-
metrics: { latency_ms, poll_count: totalPollCount, attempts }
|
|
652
|
+
metrics: { latency_ms, poll_count: totalPollCount, attempts },
|
|
653
|
+
requested,
|
|
654
|
+
observed: {
|
|
655
|
+
matched_count: matchedCount,
|
|
656
|
+
condition_satisfied: true,
|
|
657
|
+
selected_index: outEl?.index ?? null,
|
|
658
|
+
last_matched_element: outEl
|
|
659
|
+
}
|
|
639
660
|
}
|
|
640
661
|
}
|
|
641
662
|
|
|
@@ -656,16 +677,38 @@ export class ToolsInteract {
|
|
|
656
677
|
|
|
657
678
|
// Final failure for this call
|
|
658
679
|
const elapsed = Date.now() - overallStart
|
|
680
|
+
const observed = {
|
|
681
|
+
matched_count: lastMatchedCount,
|
|
682
|
+
condition_satisfied: lastConditionSatisfied,
|
|
683
|
+
selected_index: lastMatchedElement?.index ?? null,
|
|
684
|
+
last_matched_element: lastMatchedElement
|
|
685
|
+
}
|
|
686
|
+
const matchNote = requestedIndex !== null && lastMatchedCount <= requestedIndex
|
|
687
|
+
? ` requested match.index=${requestedIndex} but observed ${lastMatchedCount} match(es)`
|
|
688
|
+
: ` observed ${lastMatchedCount} match(es)`
|
|
659
689
|
return {
|
|
660
690
|
status: 'timeout',
|
|
661
|
-
error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout` },
|
|
662
|
-
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts }
|
|
691
|
+
error: { code: 'ELEMENT_NOT_FOUND', message: `Condition ${condition} not satisfied within timeout;${matchNote}` },
|
|
692
|
+
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
|
|
693
|
+
requested,
|
|
694
|
+
observed
|
|
663
695
|
}
|
|
664
696
|
}
|
|
665
697
|
|
|
666
698
|
} catch (err) {
|
|
667
699
|
const elapsed = Date.now() - overallStart
|
|
668
|
-
return {
|
|
700
|
+
return {
|
|
701
|
+
status: 'timeout',
|
|
702
|
+
error: { code: 'INTERNAL_ERROR', message: err instanceof Error ? err.message : String(err) },
|
|
703
|
+
metrics: { latency_ms: elapsed, poll_count: totalPollCount, attempts },
|
|
704
|
+
requested,
|
|
705
|
+
observed: {
|
|
706
|
+
matched_count: lastMatchedCount,
|
|
707
|
+
condition_satisfied: false,
|
|
708
|
+
selected_index: lastMatchedElement?.index ?? null,
|
|
709
|
+
last_matched_element: lastMatchedElement
|
|
710
|
+
}
|
|
711
|
+
}
|
|
669
712
|
}
|
|
670
713
|
}
|
|
671
714
|
|
|
@@ -682,11 +725,13 @@ export class ToolsInteract {
|
|
|
682
725
|
static async waitForScreenChangeHandler({ platform, previousFingerprint, timeoutMs = 5000, pollIntervalMs = 300, deviceId }: { platform?: 'android' | 'ios', previousFingerprint: string, timeoutMs?: number, pollIntervalMs?: number, deviceId?: string }) {
|
|
683
726
|
const start = Date.now()
|
|
684
727
|
let lastFingerprint: string | null = null
|
|
728
|
+
let lastActivity: string | null = null
|
|
685
729
|
|
|
686
730
|
while (Date.now() - start < timeoutMs) {
|
|
687
731
|
try {
|
|
688
732
|
const res = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as ScreenFingerprintResponse | null
|
|
689
733
|
const fp = res?.fingerprint ?? null
|
|
734
|
+
lastActivity = (res as any)?.activity ?? lastActivity
|
|
690
735
|
if (fp === null || fp === undefined) {
|
|
691
736
|
lastFingerprint = null
|
|
692
737
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
|
|
@@ -701,8 +746,18 @@ export class ToolsInteract {
|
|
|
701
746
|
try {
|
|
702
747
|
const confirmRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }) as ScreenFingerprintResponse | null
|
|
703
748
|
const confirmFp = confirmRes?.fingerprint ?? null
|
|
749
|
+
lastActivity = (confirmRes as any)?.activity ?? lastActivity
|
|
704
750
|
if (confirmFp === fp) {
|
|
705
|
-
return {
|
|
751
|
+
return {
|
|
752
|
+
success: true,
|
|
753
|
+
previousFingerprint,
|
|
754
|
+
newFingerprint: fp,
|
|
755
|
+
elapsedMs: Date.now() - start,
|
|
756
|
+
observed_screen: {
|
|
757
|
+
fingerprint: fp,
|
|
758
|
+
activity: lastActivity
|
|
759
|
+
}
|
|
760
|
+
}
|
|
706
761
|
}
|
|
707
762
|
lastFingerprint = confirmFp
|
|
708
763
|
continue
|
|
@@ -713,7 +768,17 @@ export class ToolsInteract {
|
|
|
713
768
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
|
|
714
769
|
}
|
|
715
770
|
|
|
716
|
-
return {
|
|
771
|
+
return {
|
|
772
|
+
success: false,
|
|
773
|
+
reason: 'timeout',
|
|
774
|
+
previousFingerprint,
|
|
775
|
+
lastFingerprint,
|
|
776
|
+
elapsedMs: Date.now() - start,
|
|
777
|
+
observed_screen: {
|
|
778
|
+
fingerprint: lastFingerprint,
|
|
779
|
+
activity: lastActivity
|
|
780
|
+
}
|
|
781
|
+
}
|
|
717
782
|
}
|
|
718
783
|
|
|
719
784
|
static async expectScreenHandler({
|
|
@@ -749,13 +814,23 @@ export class ToolsInteract {
|
|
|
749
814
|
}
|
|
750
815
|
|
|
751
816
|
let success = false
|
|
817
|
+
let basis: 'fingerprint' | 'screen' | 'none' = 'none'
|
|
818
|
+
let reason = 'No fingerprint or screen expectation provided'
|
|
752
819
|
if (fingerprint) {
|
|
820
|
+
basis = 'fingerprint'
|
|
753
821
|
success = observedScreen.fingerprint === fingerprint
|
|
822
|
+
reason = success
|
|
823
|
+
? `observed fingerprint matches expected fingerprint ${fingerprint}`
|
|
824
|
+
: `expected fingerprint ${fingerprint} but observed ${observedScreen.fingerprint ?? 'null'}`
|
|
754
825
|
} else if (screen) {
|
|
826
|
+
basis = 'screen'
|
|
755
827
|
const candidates = new Set<string>()
|
|
756
828
|
if (observedScreen.screen) candidates.add(observedScreen.screen)
|
|
757
829
|
if (observedScreenLabel) candidates.add(observedScreenLabel)
|
|
758
830
|
success = candidates.has(screen)
|
|
831
|
+
reason = success
|
|
832
|
+
? `observed screen matches expected screen ${screen}`
|
|
833
|
+
: `expected screen ${screen} but observed ${observedScreenLabel ?? observedScreen.screen ?? 'null'}`
|
|
759
834
|
}
|
|
760
835
|
|
|
761
836
|
return {
|
|
@@ -765,7 +840,12 @@ export class ToolsInteract {
|
|
|
765
840
|
screen: observedScreenLabel
|
|
766
841
|
},
|
|
767
842
|
expected_screen: expectedScreen,
|
|
768
|
-
confidence: success ? 1 : 0
|
|
843
|
+
confidence: success ? 1 : 0,
|
|
844
|
+
comparison: {
|
|
845
|
+
basis,
|
|
846
|
+
matched: success,
|
|
847
|
+
reason
|
|
848
|
+
}
|
|
769
849
|
}
|
|
770
850
|
}
|
|
771
851
|
|
|
@@ -798,6 +878,7 @@ export class ToolsInteract {
|
|
|
798
878
|
success: true,
|
|
799
879
|
selector,
|
|
800
880
|
element_id: result.element.elementId ?? element_id ?? null,
|
|
881
|
+
expected_condition: 'visible',
|
|
801
882
|
element: {
|
|
802
883
|
elementId: result.element.elementId ?? null,
|
|
803
884
|
text: result.element.text ?? null,
|
|
@@ -806,7 +887,23 @@ export class ToolsInteract {
|
|
|
806
887
|
class: result.element.class ?? null,
|
|
807
888
|
bounds: result.element.bounds ?? null,
|
|
808
889
|
index: typeof result.element.index === 'number' ? result.element.index : null
|
|
809
|
-
}
|
|
890
|
+
},
|
|
891
|
+
observed: {
|
|
892
|
+
status: result.status,
|
|
893
|
+
matched_count: typeof result.matched === 'number' ? result.matched : result?.observed?.matched_count ?? null,
|
|
894
|
+
condition_satisfied: true,
|
|
895
|
+
selected_index: typeof result.element.index === 'number' ? result.element.index : null,
|
|
896
|
+
last_matched_element: {
|
|
897
|
+
elementId: result.element.elementId ?? null,
|
|
898
|
+
text: result.element.text ?? null,
|
|
899
|
+
resource_id: result.element.resource_id ?? null,
|
|
900
|
+
accessibility_id: result.element.accessibility_id ?? null,
|
|
901
|
+
class: result.element.class ?? null,
|
|
902
|
+
bounds: result.element.bounds ?? null,
|
|
903
|
+
index: typeof result.element.index === 'number' ? result.element.index : null
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
reason: 'selector is visible'
|
|
810
907
|
}
|
|
811
908
|
}
|
|
812
909
|
|
|
@@ -815,6 +912,15 @@ export class ToolsInteract {
|
|
|
815
912
|
success: false,
|
|
816
913
|
selector,
|
|
817
914
|
element_id: element_id ?? null,
|
|
915
|
+
expected_condition: 'visible',
|
|
916
|
+
observed: {
|
|
917
|
+
status: result?.status,
|
|
918
|
+
matched_count: result?.observed?.matched_count,
|
|
919
|
+
condition_satisfied: result?.observed?.condition_satisfied ?? false,
|
|
920
|
+
selected_index: result?.observed?.selected_index ?? null,
|
|
921
|
+
last_matched_element: result?.observed?.last_matched_element ?? null
|
|
922
|
+
},
|
|
923
|
+
reason: result?.error?.message ?? 'selector is not visible',
|
|
818
924
|
failure_code: errorCode,
|
|
819
925
|
retryable: errorCode === 'TIMEOUT'
|
|
820
926
|
}
|
package/src/manage/android.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { existsSync } from 'fs'
|
|
|
5
5
|
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from '../utils/android/utils.js'
|
|
6
6
|
import { execAdbWithDiagnostics } from '../utils/diagnostics.js'
|
|
7
7
|
import { detectJavaHome } from '../utils/java.js'
|
|
8
|
+
import { AndroidObserve } from '../observe/android.js'
|
|
8
9
|
import { InstallAppResponse, StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from '../types.js'
|
|
9
10
|
|
|
10
11
|
export class AndroidManage {
|
|
@@ -141,8 +142,21 @@ export class AndroidManage {
|
|
|
141
142
|
const metadata = await getAndroidDeviceMetadata(appId, deviceId)
|
|
142
143
|
const deviceInfo = getDeviceInfo(deviceId || 'default', metadata)
|
|
143
144
|
try {
|
|
144
|
-
await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId)
|
|
145
|
-
|
|
145
|
+
const output = await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId)
|
|
146
|
+
const current = await new AndroidObserve().getCurrentScreen(deviceId).catch(() => null)
|
|
147
|
+
return {
|
|
148
|
+
device: deviceInfo,
|
|
149
|
+
appStarted: true,
|
|
150
|
+
launchTimeMs: 1000,
|
|
151
|
+
output,
|
|
152
|
+
observedApp: {
|
|
153
|
+
appId,
|
|
154
|
+
package: current?.package ?? null,
|
|
155
|
+
activity: current?.activity ?? null,
|
|
156
|
+
screen: current?.shortActivity ?? current?.activity ?? null,
|
|
157
|
+
matchedTarget: current ? current.package === appId : null
|
|
158
|
+
}
|
|
159
|
+
}
|
|
146
160
|
} catch (e: unknown) {
|
|
147
161
|
const diag = execAdbWithDiagnostics(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId)
|
|
148
162
|
return { device: deviceInfo, appStarted: false, launchTimeMs: 0, error: e instanceof Error ? e.message : String(e), diagnostics: diag }
|
|
@@ -162,12 +176,18 @@ export class AndroidManage {
|
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
async restartApp(appId: string, deviceId?: string): Promise<RestartAppResponse> {
|
|
165
|
-
await this.terminateApp(appId, deviceId)
|
|
179
|
+
const terminateResult = await this.terminateApp(appId, deviceId)
|
|
166
180
|
const startResult = await this.startApp(appId, deviceId)
|
|
167
181
|
return {
|
|
168
182
|
device: startResult.device,
|
|
169
183
|
appRestarted: startResult.appStarted,
|
|
170
|
-
launchTimeMs: startResult.launchTimeMs
|
|
184
|
+
launchTimeMs: startResult.launchTimeMs,
|
|
185
|
+
output: startResult.output,
|
|
186
|
+
observedApp: startResult.observedApp,
|
|
187
|
+
terminatedBeforeRestart: terminateResult.appTerminated,
|
|
188
|
+
...(terminateResult.error ? { terminateError: terminateResult.error } : {}),
|
|
189
|
+
...(startResult.error ? { error: startResult.error } : {}),
|
|
190
|
+
...(startResult.diagnostics ? { diagnostics: startResult.diagnostics } : {})
|
|
171
191
|
}
|
|
172
192
|
}
|
|
173
193
|
|
package/src/manage/ios.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { promises as fs } from "fs"
|
|
|
2
2
|
import { spawn, spawnSync } from "child_process"
|
|
3
3
|
import { StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse, InstallAppResponse } from "../types.js"
|
|
4
4
|
import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validateBundleId, getIdbCmd, findAppBundle } from "../utils/ios/utils.js"
|
|
5
|
+
import { iOSObserve } from "../observe/ios.js"
|
|
5
6
|
import path from "path"
|
|
6
7
|
|
|
7
8
|
export class iOSManage {
|
|
@@ -301,7 +302,20 @@ export class iOSManage {
|
|
|
301
302
|
try {
|
|
302
303
|
const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId)
|
|
303
304
|
const device = await getIOSDeviceMetadata(deviceId)
|
|
304
|
-
|
|
305
|
+
const fingerprint = await new iOSObserve().getScreenFingerprint(deviceId).catch(() => null)
|
|
306
|
+
const pidMatch = result.output.match(/:\s*(\d+)\s*$/)
|
|
307
|
+
return {
|
|
308
|
+
device,
|
|
309
|
+
appStarted: !!result.output,
|
|
310
|
+
launchTimeMs: 1000,
|
|
311
|
+
output: result.output,
|
|
312
|
+
observedApp: {
|
|
313
|
+
appId: bundleId,
|
|
314
|
+
pid: pidMatch ? Number(pidMatch[1]) : null,
|
|
315
|
+
screen: fingerprint?.activity ?? null,
|
|
316
|
+
matchedTarget: null
|
|
317
|
+
}
|
|
318
|
+
}
|
|
305
319
|
} catch (e: unknown) {
|
|
306
320
|
const diag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId)
|
|
307
321
|
const device = await getIOSDeviceMetadata(deviceId)
|
|
@@ -323,9 +337,19 @@ export class iOSManage {
|
|
|
323
337
|
}
|
|
324
338
|
|
|
325
339
|
async restartApp(bundleId: string, deviceId: string = "booted"): Promise<RestartAppResponse> {
|
|
326
|
-
await this.terminateApp(bundleId, deviceId)
|
|
340
|
+
const terminateResult = await this.terminateApp(bundleId, deviceId)
|
|
327
341
|
const startResult = await this.startApp(bundleId, deviceId)
|
|
328
|
-
return {
|
|
342
|
+
return {
|
|
343
|
+
device: startResult.device,
|
|
344
|
+
appRestarted: startResult.appStarted,
|
|
345
|
+
launchTimeMs: startResult.launchTimeMs,
|
|
346
|
+
output: startResult.output,
|
|
347
|
+
observedApp: startResult.observedApp,
|
|
348
|
+
terminatedBeforeRestart: terminateResult.appTerminated,
|
|
349
|
+
...(terminateResult.error ? { terminateError: terminateResult.error } : {}),
|
|
350
|
+
...(startResult.error ? { error: startResult.error } : {}),
|
|
351
|
+
...(startResult.diagnostics ? { diagnostics: startResult.diagnostics } : {})
|
|
352
|
+
}
|
|
329
353
|
}
|
|
330
354
|
|
|
331
355
|
async resetAppData(bundleId: string, deviceId: string = "booted"): Promise<ResetAppDataResponse> {
|
package/src/server/common.ts
CHANGED
|
@@ -63,26 +63,31 @@ export function inferScrollFailure(message: string | undefined): { failureCode:
|
|
|
63
63
|
|
|
64
64
|
export function buildActionExecutionResult({
|
|
65
65
|
actionType,
|
|
66
|
+
device,
|
|
66
67
|
selector,
|
|
67
68
|
resolved,
|
|
68
69
|
success,
|
|
69
70
|
uiFingerprintBefore,
|
|
70
71
|
uiFingerprintAfter,
|
|
71
|
-
failure
|
|
72
|
+
failure,
|
|
73
|
+
details
|
|
72
74
|
}: {
|
|
73
75
|
actionType: string
|
|
76
|
+
device?: ActionExecutionResult['device']
|
|
74
77
|
selector: Record<string, unknown> | null
|
|
75
78
|
resolved?: Partial<ActionTargetResolved> | null
|
|
76
79
|
success: boolean
|
|
77
80
|
uiFingerprintBefore: string | null
|
|
78
81
|
uiFingerprintAfter: string | null
|
|
79
82
|
failure?: { failureCode: ActionFailureCode; retryable: boolean }
|
|
83
|
+
details?: Record<string, unknown>
|
|
80
84
|
}): ActionExecutionResult {
|
|
81
85
|
const timestamp = Date.now()
|
|
82
86
|
return {
|
|
83
87
|
action_id: nextActionId(actionType, timestamp),
|
|
84
88
|
timestamp,
|
|
85
89
|
action_type: actionType,
|
|
90
|
+
...(device ? { device } : {}),
|
|
86
91
|
target: {
|
|
87
92
|
selector,
|
|
88
93
|
resolved: normalizeResolvedTarget(resolved)
|
|
@@ -90,6 +95,7 @@ export function buildActionExecutionResult({
|
|
|
90
95
|
success,
|
|
91
96
|
...(failure ? { failure_code: failure.failureCode, retryable: failure.retryable } : {}),
|
|
92
97
|
ui_fingerprint_before: uiFingerprintBefore,
|
|
93
|
-
ui_fingerprint_after: uiFingerprintAfter
|
|
98
|
+
ui_fingerprint_after: uiFingerprintAfter,
|
|
99
|
+
...(details ? { details } : {})
|
|
94
100
|
}
|
|
95
101
|
}
|
|
@@ -27,11 +27,19 @@ async function handleStartApp(args: ToolCallArgs) {
|
|
|
27
27
|
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId)
|
|
28
28
|
return wrapResponse(buildActionExecutionResult({
|
|
29
29
|
actionType: 'start_app',
|
|
30
|
+
device: res.device,
|
|
30
31
|
selector: { appId },
|
|
31
32
|
success: !!res.appStarted,
|
|
32
33
|
uiFingerprintBefore,
|
|
33
34
|
uiFingerprintAfter,
|
|
34
|
-
failure: res.appStarted ? undefined : inferGenericFailure(
|
|
35
|
+
failure: res.appStarted ? undefined : inferGenericFailure(res.error),
|
|
36
|
+
details: {
|
|
37
|
+
launch_time_ms: res.launchTimeMs,
|
|
38
|
+
...(typeof res.output === 'string' ? { output: res.output } : {}),
|
|
39
|
+
...(res.device ? { device_id: res.device.id } : {}),
|
|
40
|
+
...(typeof res.error === 'string' ? { error: res.error } : {}),
|
|
41
|
+
...(res.observedApp ? { observed_app: res.observedApp } : {})
|
|
42
|
+
}
|
|
35
43
|
}))
|
|
36
44
|
}
|
|
37
45
|
|
|
@@ -50,11 +58,20 @@ async function handleRestartApp(args: ToolCallArgs) {
|
|
|
50
58
|
const uiFingerprintAfter = await captureActionFingerprint(platform, deviceId)
|
|
51
59
|
return wrapResponse(buildActionExecutionResult({
|
|
52
60
|
actionType: 'restart_app',
|
|
61
|
+
device: res.device,
|
|
53
62
|
selector: { appId },
|
|
54
63
|
success: !!res.appRestarted,
|
|
55
64
|
uiFingerprintBefore,
|
|
56
65
|
uiFingerprintAfter,
|
|
57
|
-
failure: res.appRestarted ? undefined : inferGenericFailure(
|
|
66
|
+
failure: res.appRestarted ? undefined : inferGenericFailure(res.error),
|
|
67
|
+
details: {
|
|
68
|
+
launch_time_ms: res.launchTimeMs,
|
|
69
|
+
...(typeof res.output === 'string' ? { output: res.output } : {}),
|
|
70
|
+
...(typeof res.terminatedBeforeRestart === 'boolean' ? { terminated_before_restart: res.terminatedBeforeRestart } : {}),
|
|
71
|
+
...(typeof res.terminateError === 'string' ? { terminate_error: res.terminateError } : {}),
|
|
72
|
+
...(typeof res.error === 'string' ? { error: res.error } : {}),
|
|
73
|
+
...(res.observedApp ? { observed_app: res.observedApp } : {})
|
|
74
|
+
}
|
|
58
75
|
}))
|
|
59
76
|
}
|
|
60
77
|
|
package/src/system/index.ts
CHANGED
|
@@ -10,8 +10,34 @@ export async function getSystemStatus() {
|
|
|
10
10
|
const issues = [...android.issues, ...ios.issues, ...(gradle.issues || [])]
|
|
11
11
|
|
|
12
12
|
const success = issues.length === 0
|
|
13
|
+
const androidReady = android.adbAvailable && android.devices > 0 && !android.issues.some((issue) => /unauthorized|offline/i.test(issue))
|
|
14
|
+
const iosReady = ios.iosAvailable && ios.iosDevices > 0
|
|
15
|
+
const gradleReady = (gradle.issues || []).length === 0
|
|
16
|
+
const overallStatus = success ? 'ready' : (androidReady || iosReady ? 'degraded' : 'blocked')
|
|
17
|
+
|
|
18
|
+
const androidSummary = !android.adbAvailable
|
|
19
|
+
? 'ADB unavailable'
|
|
20
|
+
: android.devices === 0
|
|
21
|
+
? 'ADB available but no Android devices connected'
|
|
22
|
+
: android.logsAvailable
|
|
23
|
+
? `${android.devices} Android device(s) connected; log access available`
|
|
24
|
+
: `${android.devices} Android device(s) connected; log access unavailable`
|
|
25
|
+
|
|
26
|
+
const iosSummary = !ios.iosAvailable
|
|
27
|
+
? 'xcrun unavailable'
|
|
28
|
+
: ios.iosDevices === 0
|
|
29
|
+
? 'xcrun available but no iOS simulators booted'
|
|
30
|
+
: `${ios.iosDevices} iOS simulator(s) booted`
|
|
31
|
+
|
|
32
|
+
const gradleSummary = !gradle.gradleJavaHome
|
|
33
|
+
? 'No explicit Gradle JDK override detected'
|
|
34
|
+
: gradleReady
|
|
35
|
+
? `Gradle JDK configured at ${gradle.gradleJavaHome}`
|
|
36
|
+
: `Gradle JDK override invalid: ${gradle.gradleJavaHome}`
|
|
37
|
+
|
|
13
38
|
return {
|
|
14
39
|
success,
|
|
40
|
+
status: overallStatus,
|
|
15
41
|
adbAvailable: android.adbAvailable,
|
|
16
42
|
adbVersion: android.adbVersion,
|
|
17
43
|
devices: android.devices,
|
|
@@ -25,9 +51,38 @@ export async function getSystemStatus() {
|
|
|
25
51
|
gradleJavaHome: gradle.gradleJavaHome,
|
|
26
52
|
gradleValid: gradle.gradleValid,
|
|
27
53
|
gradleFilesChecked: gradle.filesChecked,
|
|
28
|
-
gradleSuggestedFixes: gradle.suggestedFixes
|
|
54
|
+
gradleSuggestedFixes: gradle.suggestedFixes,
|
|
55
|
+
summary: {
|
|
56
|
+
overall: overallStatus,
|
|
57
|
+
android: {
|
|
58
|
+
ready: androidReady,
|
|
59
|
+
summary: androidSummary,
|
|
60
|
+
blockers: android.issues
|
|
61
|
+
},
|
|
62
|
+
ios: {
|
|
63
|
+
ready: iosReady,
|
|
64
|
+
summary: iosSummary,
|
|
65
|
+
blockers: ios.issues
|
|
66
|
+
},
|
|
67
|
+
gradle: {
|
|
68
|
+
ready: gradleReady,
|
|
69
|
+
summary: gradleSummary,
|
|
70
|
+
blockers: gradle.issues || [],
|
|
71
|
+
suggestedFixes: gradle.suggestedFixes || []
|
|
72
|
+
}
|
|
73
|
+
}
|
|
29
74
|
}
|
|
30
75
|
} catch (e: unknown) {
|
|
31
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
status: 'blocked',
|
|
79
|
+
issues: ['Internal error: ' + (e instanceof Error ? e.message : String(e))],
|
|
80
|
+
summary: {
|
|
81
|
+
overall: 'blocked',
|
|
82
|
+
android: { ready: false, summary: 'Android status unavailable', blockers: [] },
|
|
83
|
+
ios: { ready: false, summary: 'iOS status unavailable', blockers: [] },
|
|
84
|
+
gradle: { ready: false, summary: 'Gradle status unavailable', blockers: [], suggestedFixes: [] }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
32
87
|
}
|
|
33
88
|
}
|