@ggboi360/mobile-dev-mcp 0.1.0 ā 0.2.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/LICENSE +35 -40
- package/LICENSE-ELASTIC +83 -0
- package/LICENSE-MIT +43 -0
- package/README.md +86 -17
- package/dist/index.js +1401 -46
- package/dist/index.js.map +1 -1
- package/dist/license.d.ts +2 -2
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +21 -3
- package/dist/license.js.map +1 -1
- package/dist/license.test.js +4 -4
- package/dist/license.test.js.map +1 -1
- package/package.json +11 -3
package/dist/index.js
CHANGED
|
@@ -682,6 +682,245 @@ const coreTools = [
|
|
|
682
682
|
required: ["query"],
|
|
683
683
|
},
|
|
684
684
|
},
|
|
685
|
+
// === NETWORK INSPECTION TOOLS ===
|
|
686
|
+
{
|
|
687
|
+
name: "get_network_requests",
|
|
688
|
+
description: "[PRO] Get recent network requests from app logs. Parses fetch/XHR requests from React Native logs.",
|
|
689
|
+
inputSchema: {
|
|
690
|
+
type: "object",
|
|
691
|
+
properties: {
|
|
692
|
+
lines: {
|
|
693
|
+
type: "number",
|
|
694
|
+
description: "Number of log lines to search through (default: 200)",
|
|
695
|
+
default: 200,
|
|
696
|
+
},
|
|
697
|
+
filter: {
|
|
698
|
+
type: "string",
|
|
699
|
+
description: "Filter by URL pattern or method (e.g., 'api', 'POST')",
|
|
700
|
+
},
|
|
701
|
+
device: {
|
|
702
|
+
type: "string",
|
|
703
|
+
description: "Specific device ID (optional)",
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
name: "start_network_monitoring",
|
|
710
|
+
description: "[PRO] Start real-time network request monitoring. Captures all HTTP/HTTPS traffic in background.",
|
|
711
|
+
inputSchema: {
|
|
712
|
+
type: "object",
|
|
713
|
+
properties: {
|
|
714
|
+
device: {
|
|
715
|
+
type: "string",
|
|
716
|
+
description: "Specific device ID (optional)",
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
name: "stop_network_monitoring",
|
|
723
|
+
description: "[PRO] Stop network monitoring and get summary of captured requests.",
|
|
724
|
+
inputSchema: {
|
|
725
|
+
type: "object",
|
|
726
|
+
properties: {},
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: "get_network_stats",
|
|
731
|
+
description: "[PRO] Get device network statistics including data usage, active connections, and WiFi info.",
|
|
732
|
+
inputSchema: {
|
|
733
|
+
type: "object",
|
|
734
|
+
properties: {
|
|
735
|
+
device: {
|
|
736
|
+
type: "string",
|
|
737
|
+
description: "Specific device ID (optional)",
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
name: "analyze_request",
|
|
744
|
+
description: "[PRO] Analyze a specific network request by index from captured requests. Shows headers, body, timing.",
|
|
745
|
+
inputSchema: {
|
|
746
|
+
type: "object",
|
|
747
|
+
properties: {
|
|
748
|
+
index: {
|
|
749
|
+
type: "number",
|
|
750
|
+
description: "Request index from get_network_requests or monitoring",
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
required: ["index"],
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
// === EXPO DEVTOOLS INTEGRATION ===
|
|
757
|
+
{
|
|
758
|
+
name: "check_expo_status",
|
|
759
|
+
description: "[PRO] Check Expo dev server status. Shows bundler status, dev client connection, and tunnel URLs.",
|
|
760
|
+
inputSchema: {
|
|
761
|
+
type: "object",
|
|
762
|
+
properties: {
|
|
763
|
+
port: {
|
|
764
|
+
type: "number",
|
|
765
|
+
description: "Expo bundler port (default: 8081 for Expo SDK 49+, 19000 for older)",
|
|
766
|
+
default: 8081,
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
name: "get_expo_config",
|
|
773
|
+
description: "[PRO] Get Expo project configuration from app.json or app.config.js. Shows app name, version, plugins, and more.",
|
|
774
|
+
inputSchema: {
|
|
775
|
+
type: "object",
|
|
776
|
+
properties: {
|
|
777
|
+
projectPath: {
|
|
778
|
+
type: "string",
|
|
779
|
+
description: "Path to Expo project directory (default: current directory)",
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: "expo_dev_menu",
|
|
786
|
+
description: "[PRO] Open the Expo developer menu on the connected device. Equivalent to shaking the device or pressing 'd' in terminal.",
|
|
787
|
+
inputSchema: {
|
|
788
|
+
type: "object",
|
|
789
|
+
properties: {
|
|
790
|
+
device: {
|
|
791
|
+
type: "string",
|
|
792
|
+
description: "Specific device ID (optional)",
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
name: "expo_reload",
|
|
799
|
+
description: "[PRO] Trigger a reload of the Expo app. Refreshes the JavaScript bundle without a full rebuild.",
|
|
800
|
+
inputSchema: {
|
|
801
|
+
type: "object",
|
|
802
|
+
properties: {
|
|
803
|
+
device: {
|
|
804
|
+
type: "string",
|
|
805
|
+
description: "Specific device ID (optional)",
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: "get_eas_builds",
|
|
812
|
+
description: "[PRO] Get recent EAS (Expo Application Services) build status. Shows build history for your project.",
|
|
813
|
+
inputSchema: {
|
|
814
|
+
type: "object",
|
|
815
|
+
properties: {
|
|
816
|
+
platform: {
|
|
817
|
+
type: "string",
|
|
818
|
+
enum: ["android", "ios", "all"],
|
|
819
|
+
description: "Platform to show builds for (default: all)",
|
|
820
|
+
default: "all",
|
|
821
|
+
},
|
|
822
|
+
limit: {
|
|
823
|
+
type: "number",
|
|
824
|
+
description: "Maximum number of builds to show (default: 5)",
|
|
825
|
+
default: 5,
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
// === PERFORMANCE METRICS TOOLS ===
|
|
831
|
+
{
|
|
832
|
+
name: "get_cpu_usage",
|
|
833
|
+
description: "[PRO] Get CPU usage for device or specific app. Shows per-core and per-process CPU consumption.",
|
|
834
|
+
inputSchema: {
|
|
835
|
+
type: "object",
|
|
836
|
+
properties: {
|
|
837
|
+
packageName: {
|
|
838
|
+
type: "string",
|
|
839
|
+
description: "App package name to filter (optional, shows all if not specified)",
|
|
840
|
+
},
|
|
841
|
+
device: {
|
|
842
|
+
type: "string",
|
|
843
|
+
description: "Specific device ID (optional)",
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
name: "get_memory_usage",
|
|
850
|
+
description: "[PRO] Get memory usage for a specific app. Shows heap, native, graphics, and total memory consumption.",
|
|
851
|
+
inputSchema: {
|
|
852
|
+
type: "object",
|
|
853
|
+
properties: {
|
|
854
|
+
packageName: {
|
|
855
|
+
type: "string",
|
|
856
|
+
description: "App package name (required)",
|
|
857
|
+
},
|
|
858
|
+
device: {
|
|
859
|
+
type: "string",
|
|
860
|
+
description: "Specific device ID (optional)",
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
required: ["packageName"],
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
name: "get_fps_stats",
|
|
868
|
+
description: "[PRO] Get frame rendering statistics (FPS). Shows jank frames, slow renders, and frame timing histogram.",
|
|
869
|
+
inputSchema: {
|
|
870
|
+
type: "object",
|
|
871
|
+
properties: {
|
|
872
|
+
packageName: {
|
|
873
|
+
type: "string",
|
|
874
|
+
description: "App package name (required)",
|
|
875
|
+
},
|
|
876
|
+
device: {
|
|
877
|
+
type: "string",
|
|
878
|
+
description: "Specific device ID (optional)",
|
|
879
|
+
},
|
|
880
|
+
reset: {
|
|
881
|
+
type: "boolean",
|
|
882
|
+
description: "Reset stats before measuring (default: false)",
|
|
883
|
+
default: false,
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
required: ["packageName"],
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
name: "get_battery_stats",
|
|
891
|
+
description: "[PRO] Get battery consumption statistics. Shows power usage by app and component.",
|
|
892
|
+
inputSchema: {
|
|
893
|
+
type: "object",
|
|
894
|
+
properties: {
|
|
895
|
+
packageName: {
|
|
896
|
+
type: "string",
|
|
897
|
+
description: "App package name to filter (optional)",
|
|
898
|
+
},
|
|
899
|
+
device: {
|
|
900
|
+
type: "string",
|
|
901
|
+
description: "Specific device ID (optional)",
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
name: "get_performance_snapshot",
|
|
908
|
+
description: "[PRO] Get a comprehensive performance snapshot including CPU, memory, FPS, and battery stats for an app.",
|
|
909
|
+
inputSchema: {
|
|
910
|
+
type: "object",
|
|
911
|
+
properties: {
|
|
912
|
+
packageName: {
|
|
913
|
+
type: "string",
|
|
914
|
+
description: "App package name (required)",
|
|
915
|
+
},
|
|
916
|
+
device: {
|
|
917
|
+
type: "string",
|
|
918
|
+
description: "Specific device ID (optional)",
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
required: ["packageName"],
|
|
922
|
+
},
|
|
923
|
+
},
|
|
685
924
|
];
|
|
686
925
|
// Combine core tools with license tools
|
|
687
926
|
const tools = [...coreTools, ...licenseTools];
|
|
@@ -2077,59 +2316,1112 @@ async function searchReactComponents(query, port = 8097) {
|
|
|
2077
2316
|
}
|
|
2078
2317
|
}
|
|
2079
2318
|
// ============================================================================
|
|
2080
|
-
//
|
|
2319
|
+
// NETWORK INSPECTION
|
|
2081
2320
|
// ============================================================================
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
// Handle tool listing
|
|
2091
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2092
|
-
tools,
|
|
2093
|
-
}));
|
|
2094
|
-
// Handle tool execution
|
|
2095
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2096
|
-
const { name, arguments: args } = request.params;
|
|
2321
|
+
// Network monitoring state
|
|
2322
|
+
let networkMonitorProcess = null;
|
|
2323
|
+
let networkRequestBuffer = [];
|
|
2324
|
+
const MAX_NETWORK_REQUESTS = 100;
|
|
2325
|
+
async function getNetworkRequests(lines = 200, filter, device) {
|
|
2326
|
+
const check = await requireAdvanced("get_network_requests");
|
|
2327
|
+
if (!check.allowed)
|
|
2328
|
+
return check.message;
|
|
2097
2329
|
try {
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2330
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
2331
|
+
// Get logs and filter for network-related entries
|
|
2332
|
+
// React Native logs network requests with various patterns
|
|
2333
|
+
const { stdout } = await execAsync(`adb ${deviceFlag} logcat -d -t ${lines} *:V`, { maxBuffer: 5 * 1024 * 1024 });
|
|
2334
|
+
const requests = [];
|
|
2335
|
+
const logLines = stdout.split("\n");
|
|
2336
|
+
// Patterns to match network requests
|
|
2337
|
+
const patterns = [
|
|
2338
|
+
// OkHttp pattern
|
|
2339
|
+
/OkHttp.*?(GET|POST|PUT|DELETE|PATCH)\s+(.+)/i,
|
|
2340
|
+
// React Native fetch pattern
|
|
2341
|
+
/\[fetch\].*?(GET|POST|PUT|DELETE|PATCH)\s+(.+)/i,
|
|
2342
|
+
// Generic HTTP pattern
|
|
2343
|
+
/HTTP.*?(GET|POST|PUT|DELETE|PATCH)\s+(https?:\/\/\S+)/i,
|
|
2344
|
+
// XMLHttpRequest pattern
|
|
2345
|
+
/XMLHttpRequest.*?(GET|POST|PUT|DELETE|PATCH)\s+(.+)/i,
|
|
2346
|
+
// Network response pattern
|
|
2347
|
+
/(\d{3})\s+(https?:\/\/\S+).*?(\d+ms|\d+\.\d+s)/i,
|
|
2348
|
+
];
|
|
2349
|
+
let index = 0;
|
|
2350
|
+
for (const line of logLines) {
|
|
2351
|
+
for (const pattern of patterns) {
|
|
2352
|
+
const match = line.match(pattern);
|
|
2353
|
+
if (match) {
|
|
2354
|
+
const method = match[1]?.toUpperCase() || "GET";
|
|
2355
|
+
const url = match[2] || "";
|
|
2356
|
+
// Apply filter if provided
|
|
2357
|
+
if (filter) {
|
|
2358
|
+
const filterLower = filter.toLowerCase();
|
|
2359
|
+
if (!method.toLowerCase().includes(filterLower) &&
|
|
2360
|
+
!url.toLowerCase().includes(filterLower)) {
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
requests.push({
|
|
2365
|
+
index: index++,
|
|
2366
|
+
timestamp: new Date().toISOString(),
|
|
2367
|
+
method,
|
|
2368
|
+
url: url.substring(0, 200),
|
|
2369
|
+
status: match[1]?.match(/^\d{3}$/) ? parseInt(match[1]) : undefined,
|
|
2370
|
+
});
|
|
2371
|
+
break;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2102
2374
|
}
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2375
|
+
if (requests.length === 0) {
|
|
2376
|
+
let result = `š” Network Requests\n${"ā".repeat(50)}\n\nNo network requests found in recent logs.\n\nTips:\n- Make sure your app is making network requests\n- React Native logs network activity in development mode\n- Try increasing the lines parameter`;
|
|
2377
|
+
if (check.message)
|
|
2378
|
+
result += `\n\n${check.message}`;
|
|
2379
|
+
return result;
|
|
2380
|
+
}
|
|
2381
|
+
const resultLines = [];
|
|
2382
|
+
resultLines.push("š” Network Requests");
|
|
2383
|
+
resultLines.push("ā".repeat(50));
|
|
2384
|
+
resultLines.push(`\nFound ${requests.length} request(s):\n`);
|
|
2385
|
+
for (const req of requests.slice(0, 30)) {
|
|
2386
|
+
const statusIcon = req.status
|
|
2387
|
+
? (req.status >= 200 && req.status < 300 ? "ā
" : "ā")
|
|
2388
|
+
: "ā³";
|
|
2389
|
+
resultLines.push(`[${req.index}] ${statusIcon} ${req.method} ${req.url.substring(0, 60)}${req.url.length > 60 ? "..." : ""}`);
|
|
2390
|
+
if (req.status) {
|
|
2391
|
+
resultLines.push(` Status: ${req.status}`);
|
|
2108
2392
|
}
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2393
|
+
}
|
|
2394
|
+
if (requests.length > 30) {
|
|
2395
|
+
resultLines.push(`\n... and ${requests.length - 30} more requests`);
|
|
2396
|
+
}
|
|
2397
|
+
resultLines.push("\nUse 'start_network_monitoring' for real-time capture");
|
|
2398
|
+
let result = resultLines.join("\n");
|
|
2399
|
+
if (check.message)
|
|
2400
|
+
result += `\n\n${check.message}`;
|
|
2401
|
+
return result;
|
|
2402
|
+
}
|
|
2403
|
+
catch (error) {
|
|
2404
|
+
return `Error getting network requests: ${error.message}`;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
async function startNetworkMonitoring(device) {
|
|
2408
|
+
const check = await requireAdvanced("start_network_monitoring");
|
|
2409
|
+
if (!check.allowed)
|
|
2410
|
+
return check.message;
|
|
2411
|
+
if (networkMonitorProcess) {
|
|
2412
|
+
return "Network monitoring is already running. Use 'stop_network_monitoring' first.";
|
|
2413
|
+
}
|
|
2414
|
+
networkRequestBuffer = [];
|
|
2415
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
2416
|
+
// Start logcat with network-related filters
|
|
2417
|
+
networkMonitorProcess = spawn("adb", [
|
|
2418
|
+
...deviceFlag.split(" ").filter(s => s),
|
|
2419
|
+
"logcat",
|
|
2420
|
+
"-v", "time",
|
|
2421
|
+
"OkHttp:V",
|
|
2422
|
+
"Retrofit:V",
|
|
2423
|
+
"ReactNativeJS:V",
|
|
2424
|
+
"*:S"
|
|
2425
|
+
]);
|
|
2426
|
+
let requestIndex = 0;
|
|
2427
|
+
networkMonitorProcess.stdout?.on("data", (data) => {
|
|
2428
|
+
const lines = data.toString().split("\n");
|
|
2429
|
+
for (const line of lines) {
|
|
2430
|
+
// Parse network request patterns
|
|
2431
|
+
const httpMatch = line.match(/(GET|POST|PUT|DELETE|PATCH)\s+(https?:\/\/\S+)/i);
|
|
2432
|
+
if (httpMatch) {
|
|
2433
|
+
networkRequestBuffer.push({
|
|
2434
|
+
index: requestIndex++,
|
|
2435
|
+
timestamp: new Date().toISOString(),
|
|
2436
|
+
method: httpMatch[1].toUpperCase(),
|
|
2437
|
+
url: httpMatch[2],
|
|
2438
|
+
});
|
|
2439
|
+
if (networkRequestBuffer.length > MAX_NETWORK_REQUESTS) {
|
|
2440
|
+
networkRequestBuffer = networkRequestBuffer.slice(-MAX_NETWORK_REQUESTS);
|
|
2441
|
+
}
|
|
2112
2442
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
mimeType: result.mimeType,
|
|
2121
|
-
},
|
|
2122
|
-
];
|
|
2123
|
-
// Add trial warning if present
|
|
2124
|
-
if (result.trialMessage) {
|
|
2125
|
-
content.push({ type: "text", text: result.trialMessage });
|
|
2126
|
-
}
|
|
2127
|
-
return { content };
|
|
2443
|
+
// Parse response status
|
|
2444
|
+
const statusMatch = line.match(/<--\s*(\d{3})\s+(https?:\/\/\S+)/i);
|
|
2445
|
+
if (statusMatch) {
|
|
2446
|
+
const url = statusMatch[2];
|
|
2447
|
+
const req = networkRequestBuffer.find(r => r.url === url && !r.status);
|
|
2448
|
+
if (req) {
|
|
2449
|
+
req.status = parseInt(statusMatch[1]);
|
|
2128
2450
|
}
|
|
2129
|
-
return { content: [{ type: "text", text: result.error }] };
|
|
2130
2451
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
networkMonitorProcess.on("error", (err) => {
|
|
2455
|
+
console.error("Network monitor error:", err);
|
|
2456
|
+
});
|
|
2457
|
+
let result = `š” Network Monitoring Started\n${"ā".repeat(50)}\n\nCapturing HTTP/HTTPS requests in background.\n\nUse 'stop_network_monitoring' to stop and see captured requests.\nUse 'get_network_requests' to see current buffer.`;
|
|
2458
|
+
if (check.message)
|
|
2459
|
+
result += `\n\n${check.message}`;
|
|
2460
|
+
return result;
|
|
2461
|
+
}
|
|
2462
|
+
async function stopNetworkMonitoring() {
|
|
2463
|
+
const check = await requireAdvanced("stop_network_monitoring");
|
|
2464
|
+
if (!check.allowed)
|
|
2465
|
+
return check.message;
|
|
2466
|
+
if (networkMonitorProcess) {
|
|
2467
|
+
networkMonitorProcess.kill();
|
|
2468
|
+
networkMonitorProcess = null;
|
|
2469
|
+
}
|
|
2470
|
+
const requests = networkRequestBuffer;
|
|
2471
|
+
const summary = {
|
|
2472
|
+
total: requests.length,
|
|
2473
|
+
successful: requests.filter(r => r.status && r.status >= 200 && r.status < 300).length,
|
|
2474
|
+
failed: requests.filter(r => r.status && (r.status < 200 || r.status >= 300)).length,
|
|
2475
|
+
pending: requests.filter(r => !r.status).length,
|
|
2476
|
+
};
|
|
2477
|
+
const resultLines = [];
|
|
2478
|
+
resultLines.push("š” Network Monitoring Stopped");
|
|
2479
|
+
resultLines.push("ā".repeat(50));
|
|
2480
|
+
resultLines.push(`\nSummary:`);
|
|
2481
|
+
resultLines.push(` Total requests: ${summary.total}`);
|
|
2482
|
+
resultLines.push(` Successful (2xx): ${summary.successful}`);
|
|
2483
|
+
resultLines.push(` Failed: ${summary.failed}`);
|
|
2484
|
+
resultLines.push(` Pending: ${summary.pending}`);
|
|
2485
|
+
if (requests.length > 0) {
|
|
2486
|
+
resultLines.push(`\nRecent requests:`);
|
|
2487
|
+
for (const req of requests.slice(-10)) {
|
|
2488
|
+
const statusIcon = req.status
|
|
2489
|
+
? (req.status >= 200 && req.status < 300 ? "ā
" : "ā")
|
|
2490
|
+
: "ā³";
|
|
2491
|
+
resultLines.push(`[${req.index}] ${statusIcon} ${req.method} ${req.url.substring(0, 50)}...`);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
let result = resultLines.join("\n");
|
|
2495
|
+
if (check.message)
|
|
2496
|
+
result += `\n\n${check.message}`;
|
|
2497
|
+
return result;
|
|
2498
|
+
}
|
|
2499
|
+
async function getNetworkStats(device) {
|
|
2500
|
+
const check = await requireAdvanced("get_network_stats");
|
|
2501
|
+
if (!check.allowed)
|
|
2502
|
+
return check.message;
|
|
2503
|
+
try {
|
|
2504
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
2505
|
+
// Get various network stats
|
|
2506
|
+
const [wifiInfo, netStats, activeConns] = await Promise.all([
|
|
2507
|
+
execAsync(`adb ${deviceFlag} shell dumpsys wifi | head -50`)
|
|
2508
|
+
.then(r => r.stdout)
|
|
2509
|
+
.catch(() => "N/A"),
|
|
2510
|
+
execAsync(`adb ${deviceFlag} shell cat /proc/net/dev`)
|
|
2511
|
+
.then(r => r.stdout)
|
|
2512
|
+
.catch(() => "N/A"),
|
|
2513
|
+
execAsync(`adb ${deviceFlag} shell netstat -an | head -30`)
|
|
2514
|
+
.then(r => r.stdout)
|
|
2515
|
+
.catch(() => "N/A"),
|
|
2516
|
+
]);
|
|
2517
|
+
const resultLines = [];
|
|
2518
|
+
resultLines.push("š Network Statistics");
|
|
2519
|
+
resultLines.push("ā".repeat(50));
|
|
2520
|
+
// Parse WiFi info
|
|
2521
|
+
const ssidMatch = wifiInfo.match(/mWifiInfo.*?SSID:\s*([^,]+)/);
|
|
2522
|
+
const rssiMatch = wifiInfo.match(/RSSI:\s*(-?\d+)/);
|
|
2523
|
+
const linkSpeedMatch = wifiInfo.match(/Link speed:\s*(\d+)/);
|
|
2524
|
+
resultLines.push("\nš¶ WiFi Info:");
|
|
2525
|
+
if (ssidMatch)
|
|
2526
|
+
resultLines.push(` SSID: ${ssidMatch[1].trim()}`);
|
|
2527
|
+
if (rssiMatch)
|
|
2528
|
+
resultLines.push(` Signal: ${rssiMatch[1]} dBm`);
|
|
2529
|
+
if (linkSpeedMatch)
|
|
2530
|
+
resultLines.push(` Speed: ${linkSpeedMatch[1]} Mbps`);
|
|
2531
|
+
// Parse network interface stats
|
|
2532
|
+
resultLines.push("\nš Interface Stats:");
|
|
2533
|
+
const devLines = netStats.split("\n").filter(l => l.includes("wlan") || l.includes("rmnet"));
|
|
2534
|
+
for (const line of devLines.slice(0, 5)) {
|
|
2535
|
+
const parts = line.trim().split(/\s+/);
|
|
2536
|
+
if (parts.length >= 10) {
|
|
2537
|
+
const iface = parts[0].replace(":", "");
|
|
2538
|
+
const rxBytes = parseInt(parts[1]) || 0;
|
|
2539
|
+
const txBytes = parseInt(parts[9]) || 0;
|
|
2540
|
+
resultLines.push(` ${iface}: RX ${formatBytes(rxBytes)}, TX ${formatBytes(txBytes)}`);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
// Parse active connections
|
|
2544
|
+
resultLines.push("\nš Active Connections:");
|
|
2545
|
+
const connLines = activeConns.split("\n")
|
|
2546
|
+
.filter(l => l.includes("ESTABLISHED") || l.includes("TIME_WAIT"))
|
|
2547
|
+
.slice(0, 8);
|
|
2548
|
+
if (connLines.length > 0) {
|
|
2549
|
+
for (const line of connLines) {
|
|
2550
|
+
const parts = line.trim().split(/\s+/);
|
|
2551
|
+
if (parts.length >= 5) {
|
|
2552
|
+
resultLines.push(` ${parts[3]} -> ${parts[4]} (${parts[5] || ""})`);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
else {
|
|
2557
|
+
resultLines.push(" No active connections found");
|
|
2558
|
+
}
|
|
2559
|
+
let result = resultLines.join("\n");
|
|
2560
|
+
if (check.message)
|
|
2561
|
+
result += `\n\n${check.message}`;
|
|
2562
|
+
return result;
|
|
2563
|
+
}
|
|
2564
|
+
catch (error) {
|
|
2565
|
+
return `Error getting network stats: ${error.message}`;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
function formatBytes(bytes) {
|
|
2569
|
+
if (bytes < 1024)
|
|
2570
|
+
return `${bytes} B`;
|
|
2571
|
+
if (bytes < 1024 * 1024)
|
|
2572
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
2573
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
2574
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
2575
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
2576
|
+
}
|
|
2577
|
+
async function analyzeRequest(index) {
|
|
2578
|
+
const check = await requireAdvanced("analyze_request");
|
|
2579
|
+
if (!check.allowed)
|
|
2580
|
+
return check.message;
|
|
2581
|
+
const request = networkRequestBuffer.find(r => r.index === index);
|
|
2582
|
+
if (!request) {
|
|
2583
|
+
let result = `Request #${index} not found.\n\nUse 'get_network_requests' or 'start_network_monitoring' to capture requests first.`;
|
|
2584
|
+
if (check.message)
|
|
2585
|
+
result += `\n\n${check.message}`;
|
|
2586
|
+
return result;
|
|
2587
|
+
}
|
|
2588
|
+
const resultLines = [];
|
|
2589
|
+
resultLines.push(`š Request Analysis #${index}`);
|
|
2590
|
+
resultLines.push("ā".repeat(50));
|
|
2591
|
+
resultLines.push(`\nMethod: ${request.method}`);
|
|
2592
|
+
resultLines.push(`URL: ${request.url}`);
|
|
2593
|
+
resultLines.push(`Timestamp: ${request.timestamp}`);
|
|
2594
|
+
if (request.status) {
|
|
2595
|
+
const statusText = request.status >= 200 && request.status < 300 ? "Success" : "Failed";
|
|
2596
|
+
resultLines.push(`Status: ${request.status} (${statusText})`);
|
|
2597
|
+
}
|
|
2598
|
+
else {
|
|
2599
|
+
resultLines.push("Status: Pending/Unknown");
|
|
2600
|
+
}
|
|
2601
|
+
if (request.duration) {
|
|
2602
|
+
resultLines.push(`Duration: ${request.duration}ms`);
|
|
2603
|
+
}
|
|
2604
|
+
if (request.size) {
|
|
2605
|
+
resultLines.push(`Size: ${request.size}`);
|
|
2606
|
+
}
|
|
2607
|
+
if (request.headers && Object.keys(request.headers).length > 0) {
|
|
2608
|
+
resultLines.push("\nš Headers:");
|
|
2609
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
2610
|
+
resultLines.push(` ${key}: ${value}`);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
if (request.body) {
|
|
2614
|
+
resultLines.push("\nš¤ Request Body:");
|
|
2615
|
+
resultLines.push(` ${request.body.substring(0, 500)}${request.body.length > 500 ? "..." : ""}`);
|
|
2616
|
+
}
|
|
2617
|
+
if (request.response) {
|
|
2618
|
+
resultLines.push("\nš„ Response:");
|
|
2619
|
+
resultLines.push(` ${request.response.substring(0, 500)}${request.response.length > 500 ? "..." : ""}`);
|
|
2620
|
+
}
|
|
2621
|
+
let result = resultLines.join("\n");
|
|
2622
|
+
if (check.message)
|
|
2623
|
+
result += `\n\n${check.message}`;
|
|
2624
|
+
return result;
|
|
2625
|
+
}
|
|
2626
|
+
// ============================================================================
|
|
2627
|
+
// EXPO DEVTOOLS INTEGRATION
|
|
2628
|
+
// ============================================================================
|
|
2629
|
+
async function checkExpoStatus(port = 8081) {
|
|
2630
|
+
const check = await requireAdvanced("check_expo_status");
|
|
2631
|
+
if (!check.allowed)
|
|
2632
|
+
return check.message;
|
|
2633
|
+
const resultLines = [];
|
|
2634
|
+
resultLines.push("š± Expo Dev Server Status");
|
|
2635
|
+
resultLines.push("ā".repeat(50));
|
|
2636
|
+
// Check multiple ports that Expo might use
|
|
2637
|
+
const portsToCheck = [
|
|
2638
|
+
{ port, name: "Metro Bundler" },
|
|
2639
|
+
{ port: 19000, name: "Legacy Bundler" },
|
|
2640
|
+
{ port: 19001, name: "WebSocket" },
|
|
2641
|
+
{ port: 19002, name: "DevTools UI" },
|
|
2642
|
+
];
|
|
2643
|
+
let anyRunning = false;
|
|
2644
|
+
for (const { port: p, name } of portsToCheck) {
|
|
2645
|
+
try {
|
|
2646
|
+
const response = await new Promise((resolve, reject) => {
|
|
2647
|
+
const req = http.request({ hostname: "localhost", port: p, path: "/status", method: "GET", timeout: 2000 }, (res) => {
|
|
2648
|
+
let data = "";
|
|
2649
|
+
res.on("data", (chunk) => (data += chunk));
|
|
2650
|
+
res.on("end", () => resolve(data));
|
|
2651
|
+
});
|
|
2652
|
+
req.on("error", reject);
|
|
2653
|
+
req.on("timeout", () => reject(new Error("timeout")));
|
|
2654
|
+
req.end();
|
|
2655
|
+
});
|
|
2656
|
+
resultLines.push(`\nā
${name} (port ${p}): Running`);
|
|
2657
|
+
if (response && response.length > 0 && response.length < 200) {
|
|
2658
|
+
resultLines.push(` Status: ${response.trim()}`);
|
|
2659
|
+
}
|
|
2660
|
+
anyRunning = true;
|
|
2661
|
+
}
|
|
2662
|
+
catch {
|
|
2663
|
+
// Port not responding, check if it's a different status endpoint
|
|
2664
|
+
try {
|
|
2665
|
+
// Try the root endpoint for bundler status
|
|
2666
|
+
const response = await new Promise((resolve, reject) => {
|
|
2667
|
+
const req = http.request({ hostname: "localhost", port: p, path: "/", method: "GET", timeout: 1000 }, (res) => {
|
|
2668
|
+
let data = "";
|
|
2669
|
+
res.on("data", (chunk) => (data += chunk));
|
|
2670
|
+
res.on("end", () => resolve(`HTTP ${res.statusCode}`));
|
|
2671
|
+
});
|
|
2672
|
+
req.on("error", reject);
|
|
2673
|
+
req.on("timeout", () => reject(new Error("timeout")));
|
|
2674
|
+
req.end();
|
|
2675
|
+
});
|
|
2676
|
+
resultLines.push(`\nā
${name} (port ${p}): Running (${response})`);
|
|
2677
|
+
anyRunning = true;
|
|
2678
|
+
}
|
|
2679
|
+
catch {
|
|
2680
|
+
resultLines.push(`\nāŖ ${name} (port ${p}): Not running`);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
// Get Expo CLI version if available
|
|
2685
|
+
try {
|
|
2686
|
+
const { stdout: expoVersion } = await execAsync("npx expo --version", { timeout: 5000 });
|
|
2687
|
+
resultLines.push(`\nš¦ Expo CLI: v${expoVersion.trim()}`);
|
|
2688
|
+
}
|
|
2689
|
+
catch {
|
|
2690
|
+
resultLines.push("\nš¦ Expo CLI: Not detected");
|
|
2691
|
+
}
|
|
2692
|
+
// Check if any device is connected
|
|
2693
|
+
try {
|
|
2694
|
+
const { stdout: devices } = await execAsync("adb devices");
|
|
2695
|
+
const deviceLines = devices.split("\n").filter(line => line.includes("\tdevice"));
|
|
2696
|
+
if (deviceLines.length > 0) {
|
|
2697
|
+
resultLines.push(`\nš± Connected devices: ${deviceLines.length}`);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
catch {
|
|
2701
|
+
// ADB not available
|
|
2702
|
+
}
|
|
2703
|
+
if (!anyRunning) {
|
|
2704
|
+
resultLines.push("\nā ļø No Expo dev server detected. Start with:");
|
|
2705
|
+
resultLines.push(" npx expo start");
|
|
2706
|
+
}
|
|
2707
|
+
let result = resultLines.join("\n");
|
|
2708
|
+
if (check.message)
|
|
2709
|
+
result += `\n\n${check.message}`;
|
|
2710
|
+
return result;
|
|
2711
|
+
}
|
|
2712
|
+
async function getExpoConfig(projectPath) {
|
|
2713
|
+
const check = await requireAdvanced("get_expo_config");
|
|
2714
|
+
if (!check.allowed)
|
|
2715
|
+
return check.message;
|
|
2716
|
+
const cwd = projectPath || process.cwd();
|
|
2717
|
+
const resultLines = [];
|
|
2718
|
+
resultLines.push("š Expo Project Configuration");
|
|
2719
|
+
resultLines.push("ā".repeat(50));
|
|
2720
|
+
// Try to read app.json
|
|
2721
|
+
const appJsonPath = path.join(cwd, "app.json");
|
|
2722
|
+
const appConfigJsPath = path.join(cwd, "app.config.js");
|
|
2723
|
+
const appConfigTsPath = path.join(cwd, "app.config.ts");
|
|
2724
|
+
let config = null;
|
|
2725
|
+
if (fs.existsSync(appJsonPath)) {
|
|
2726
|
+
try {
|
|
2727
|
+
const content = fs.readFileSync(appJsonPath, "utf-8");
|
|
2728
|
+
config = JSON.parse(content);
|
|
2729
|
+
resultLines.push(`\nSource: app.json`);
|
|
2730
|
+
}
|
|
2731
|
+
catch (err) {
|
|
2732
|
+
resultLines.push(`\nā Error reading app.json: ${err}`);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
else if (fs.existsSync(appConfigJsPath)) {
|
|
2736
|
+
resultLines.push(`\nSource: app.config.js (dynamic config)`);
|
|
2737
|
+
// Can't easily evaluate JS config, suggest using expo config
|
|
2738
|
+
try {
|
|
2739
|
+
const { stdout } = await execAsync("npx expo config --json", { cwd, timeout: 10000 });
|
|
2740
|
+
config = JSON.parse(stdout);
|
|
2741
|
+
}
|
|
2742
|
+
catch {
|
|
2743
|
+
resultLines.push("ā ļø Run 'npx expo config' to see resolved config");
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
else if (fs.existsSync(appConfigTsPath)) {
|
|
2747
|
+
resultLines.push(`\nSource: app.config.ts (TypeScript config)`);
|
|
2748
|
+
try {
|
|
2749
|
+
const { stdout } = await execAsync("npx expo config --json", { cwd, timeout: 10000 });
|
|
2750
|
+
config = JSON.parse(stdout);
|
|
2751
|
+
}
|
|
2752
|
+
catch {
|
|
2753
|
+
resultLines.push("ā ļø Run 'npx expo config' to see resolved config");
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
else {
|
|
2757
|
+
resultLines.push(`\nā No Expo config found in: ${cwd}`);
|
|
2758
|
+
resultLines.push("\nLooking for: app.json, app.config.js, or app.config.ts");
|
|
2759
|
+
let result = resultLines.join("\n");
|
|
2760
|
+
if (check.message)
|
|
2761
|
+
result += `\n\n${check.message}`;
|
|
2762
|
+
return result;
|
|
2763
|
+
}
|
|
2764
|
+
if (config) {
|
|
2765
|
+
const expo = config.expo || config;
|
|
2766
|
+
resultLines.push(`\nš± App Name: ${expo.name || "N/A"}`);
|
|
2767
|
+
resultLines.push(`š Slug: ${expo.slug || "N/A"}`);
|
|
2768
|
+
resultLines.push(`š¦ Version: ${expo.version || "N/A"}`);
|
|
2769
|
+
resultLines.push(`š§ SDK Version: ${expo.sdkVersion || "N/A"}`);
|
|
2770
|
+
if (expo.ios) {
|
|
2771
|
+
resultLines.push(`\nš iOS:`);
|
|
2772
|
+
resultLines.push(` Bundle ID: ${expo.ios.bundleIdentifier || "N/A"}`);
|
|
2773
|
+
if (expo.ios.buildNumber)
|
|
2774
|
+
resultLines.push(` Build: ${expo.ios.buildNumber}`);
|
|
2775
|
+
}
|
|
2776
|
+
if (expo.android) {
|
|
2777
|
+
resultLines.push(`\nš¤ Android:`);
|
|
2778
|
+
resultLines.push(` Package: ${expo.android.package || "N/A"}`);
|
|
2779
|
+
if (expo.android.versionCode)
|
|
2780
|
+
resultLines.push(` Version Code: ${expo.android.versionCode}`);
|
|
2781
|
+
}
|
|
2782
|
+
if (expo.plugins && expo.plugins.length > 0) {
|
|
2783
|
+
resultLines.push(`\nš Plugins (${expo.plugins.length}):`);
|
|
2784
|
+
for (const plugin of expo.plugins.slice(0, 10)) {
|
|
2785
|
+
const pluginName = Array.isArray(plugin) ? plugin[0] : plugin;
|
|
2786
|
+
resultLines.push(` - ${pluginName}`);
|
|
2787
|
+
}
|
|
2788
|
+
if (expo.plugins.length > 10) {
|
|
2789
|
+
resultLines.push(` ... and ${expo.plugins.length - 10} more`);
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
if (expo.extra) {
|
|
2793
|
+
resultLines.push(`\nš Extra Config: ${JSON.stringify(expo.extra).substring(0, 100)}...`);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
let result = resultLines.join("\n");
|
|
2797
|
+
if (check.message)
|
|
2798
|
+
result += `\n\n${check.message}`;
|
|
2799
|
+
return result;
|
|
2800
|
+
}
|
|
2801
|
+
async function expoDevMenu(device) {
|
|
2802
|
+
const check = await requireAdvanced("expo_dev_menu");
|
|
2803
|
+
if (!check.allowed)
|
|
2804
|
+
return check.message;
|
|
2805
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
2806
|
+
try {
|
|
2807
|
+
// The developer menu in React Native/Expo is triggered by shaking the device
|
|
2808
|
+
// We can simulate this with a combination of keyboard events or the menu keycode
|
|
2809
|
+
// KEYCODE_MENU = 82 opens the dev menu on most RN/Expo apps
|
|
2810
|
+
await execAsync(`adb ${deviceFlag} shell input keyevent 82`);
|
|
2811
|
+
let result = `ā
Developer menu triggered
|
|
2812
|
+
|
|
2813
|
+
The Expo/React Native developer menu should now be visible.
|
|
2814
|
+
|
|
2815
|
+
Available options typically include:
|
|
2816
|
+
- Reload
|
|
2817
|
+
- Go to Expo Home
|
|
2818
|
+
- Toggle Inspector
|
|
2819
|
+
- Toggle Performance Monitor
|
|
2820
|
+
- Show Element Inspector
|
|
2821
|
+
- Open JS Debugger
|
|
2822
|
+
- Fast Refresh settings`;
|
|
2823
|
+
if (check.message)
|
|
2824
|
+
result += `\n\n${check.message}`;
|
|
2825
|
+
return result;
|
|
2826
|
+
}
|
|
2827
|
+
catch (error) {
|
|
2828
|
+
let result = `Error opening dev menu: ${error.message}\n\nMake sure a device is connected: adb devices`;
|
|
2829
|
+
if (check.message)
|
|
2830
|
+
result += `\n\n${check.message}`;
|
|
2831
|
+
return result;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
async function expoReload(device) {
|
|
2835
|
+
const check = await requireAdvanced("expo_reload");
|
|
2836
|
+
if (!check.allowed)
|
|
2837
|
+
return check.message;
|
|
2838
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
2839
|
+
try {
|
|
2840
|
+
// Method 1: Send reload broadcast to Expo/RN
|
|
2841
|
+
// Double-tap R in dev menu typically reloads
|
|
2842
|
+
// KEYCODE_R = 46, but we need to simulate the reload command
|
|
2843
|
+
// First try to use the reload command via adb
|
|
2844
|
+
// Many Expo apps respond to the "RR" double-tap
|
|
2845
|
+
await execAsync(`adb ${deviceFlag} shell input keyevent 82`); // Open menu
|
|
2846
|
+
await new Promise(resolve => setTimeout(resolve, 300)); // Wait for menu
|
|
2847
|
+
await execAsync(`adb ${deviceFlag} shell input text r`); // Press 'r' to reload
|
|
2848
|
+
// Alternative: Send the reload intent directly if app supports it
|
|
2849
|
+
try {
|
|
2850
|
+
// Try sending a reload broadcast (works on some Expo setups)
|
|
2851
|
+
await execAsync(`adb ${deviceFlag} shell am broadcast -a "com.facebook.react.reload"`);
|
|
2852
|
+
}
|
|
2853
|
+
catch {
|
|
2854
|
+
// Not all apps support this broadcast
|
|
2855
|
+
}
|
|
2856
|
+
let result = `š Reload triggered!
|
|
2857
|
+
|
|
2858
|
+
The app should now be reloading its JavaScript bundle.
|
|
2859
|
+
|
|
2860
|
+
If the app didn't reload, try:
|
|
2861
|
+
1. Press 'r' twice quickly in the Metro terminal
|
|
2862
|
+
2. Use expo_dev_menu and select "Reload"
|
|
2863
|
+
3. Shake the device to open dev menu`;
|
|
2864
|
+
if (check.message)
|
|
2865
|
+
result += `\n\n${check.message}`;
|
|
2866
|
+
return result;
|
|
2867
|
+
}
|
|
2868
|
+
catch (error) {
|
|
2869
|
+
let result = `Error triggering reload: ${error.message}\n\nMake sure a device is connected: adb devices`;
|
|
2870
|
+
if (check.message)
|
|
2871
|
+
result += `\n\n${check.message}`;
|
|
2872
|
+
return result;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
async function getEasBuilds(platform = "all", limit = 5) {
|
|
2876
|
+
const check = await requireAdvanced("get_eas_builds");
|
|
2877
|
+
if (!check.allowed)
|
|
2878
|
+
return check.message;
|
|
2879
|
+
const resultLines = [];
|
|
2880
|
+
resultLines.push("šļø EAS Build Status");
|
|
2881
|
+
resultLines.push("ā".repeat(50));
|
|
2882
|
+
try {
|
|
2883
|
+
// Check if eas-cli is available
|
|
2884
|
+
await execAsync("npx eas-cli --version", { timeout: 5000 });
|
|
2885
|
+
// Get build list
|
|
2886
|
+
const platformFlag = platform !== "all" ? `--platform ${platform}` : "";
|
|
2887
|
+
const { stdout } = await execAsync(`npx eas-cli build:list ${platformFlag} --limit ${limit} --json --non-interactive`, { timeout: 30000 });
|
|
2888
|
+
let builds = [];
|
|
2889
|
+
try {
|
|
2890
|
+
builds = JSON.parse(stdout);
|
|
2891
|
+
}
|
|
2892
|
+
catch {
|
|
2893
|
+
// Sometimes eas-cli outputs extra text before JSON
|
|
2894
|
+
const jsonMatch = stdout.match(/\[[\s\S]*\]/);
|
|
2895
|
+
if (jsonMatch) {
|
|
2896
|
+
builds = JSON.parse(jsonMatch[0]);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
if (builds.length === 0) {
|
|
2900
|
+
resultLines.push("\nNo builds found.");
|
|
2901
|
+
resultLines.push("\nTo create a build:");
|
|
2902
|
+
resultLines.push(" npx eas-cli build --platform android");
|
|
2903
|
+
resultLines.push(" npx eas-cli build --platform ios");
|
|
2904
|
+
}
|
|
2905
|
+
else {
|
|
2906
|
+
resultLines.push(`\nShowing ${builds.length} most recent builds:\n`);
|
|
2907
|
+
for (const build of builds) {
|
|
2908
|
+
const platformEmoji = build.platform === "ANDROID" ? "š¤" : "š";
|
|
2909
|
+
const statusEmoji = build.status === "FINISHED" ? "ā
" :
|
|
2910
|
+
build.status === "IN_PROGRESS" ? "š" :
|
|
2911
|
+
build.status === "ERRORED" ? "ā" : "ā³";
|
|
2912
|
+
resultLines.push(`${statusEmoji} ${platformEmoji} ${build.platform}`);
|
|
2913
|
+
resultLines.push(` Build ID: ${build.id}`);
|
|
2914
|
+
resultLines.push(` Status: ${build.status}`);
|
|
2915
|
+
resultLines.push(` Profile: ${build.buildProfile || "N/A"}`);
|
|
2916
|
+
resultLines.push(` Started: ${new Date(build.createdAt).toLocaleString()}`);
|
|
2917
|
+
if (build.status === "FINISHED" && build.artifacts?.buildUrl) {
|
|
2918
|
+
resultLines.push(` š„ Download: ${build.artifacts.buildUrl}`);
|
|
2919
|
+
}
|
|
2920
|
+
if (build.error) {
|
|
2921
|
+
resultLines.push(` ā Error: ${build.error.message || build.error}`);
|
|
2922
|
+
}
|
|
2923
|
+
resultLines.push("");
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
catch (error) {
|
|
2928
|
+
if (error.message?.includes("not found") || error.message?.includes("ENOENT")) {
|
|
2929
|
+
resultLines.push("\nā EAS CLI not found");
|
|
2930
|
+
resultLines.push("\nInstall with: npm install -g eas-cli");
|
|
2931
|
+
resultLines.push("Then login: npx eas-cli login");
|
|
2932
|
+
}
|
|
2933
|
+
else if (error.message?.includes("not logged in") || error.message?.includes("AUTH")) {
|
|
2934
|
+
resultLines.push("\nā Not logged in to EAS");
|
|
2935
|
+
resultLines.push("\nLogin with: npx eas-cli login");
|
|
2936
|
+
}
|
|
2937
|
+
else {
|
|
2938
|
+
resultLines.push(`\nā Error: ${error.message}`);
|
|
2939
|
+
resultLines.push("\nMake sure you're in an Expo project directory with eas.json");
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
let result = resultLines.join("\n");
|
|
2943
|
+
if (check.message)
|
|
2944
|
+
result += `\n\n${check.message}`;
|
|
2945
|
+
return result;
|
|
2946
|
+
}
|
|
2947
|
+
// ============================================================================
|
|
2948
|
+
// PERFORMANCE METRICS
|
|
2949
|
+
// ============================================================================
|
|
2950
|
+
async function getCpuUsage(packageName, device) {
|
|
2951
|
+
const check = await requireAdvanced("get_cpu_usage");
|
|
2952
|
+
if (!check.allowed)
|
|
2953
|
+
return check.message;
|
|
2954
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
2955
|
+
try {
|
|
2956
|
+
const resultLines = [];
|
|
2957
|
+
resultLines.push("š CPU Usage");
|
|
2958
|
+
resultLines.push("ā".repeat(50));
|
|
2959
|
+
// Get overall CPU info
|
|
2960
|
+
const { stdout: cpuInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys cpuinfo`, { maxBuffer: 2 * 1024 * 1024 });
|
|
2961
|
+
// Parse CPU usage
|
|
2962
|
+
const lines = cpuInfo.split("\n");
|
|
2963
|
+
const totalMatch = lines.find(line => line.includes("TOTAL"));
|
|
2964
|
+
if (totalMatch) {
|
|
2965
|
+
resultLines.push(`\n${totalMatch.trim()}`);
|
|
2966
|
+
}
|
|
2967
|
+
// If package specified, filter for that app
|
|
2968
|
+
if (packageName) {
|
|
2969
|
+
resultLines.push(`\nš± App: ${packageName}`);
|
|
2970
|
+
const appLines = lines.filter(line => line.toLowerCase().includes(packageName.toLowerCase()));
|
|
2971
|
+
if (appLines.length > 0) {
|
|
2972
|
+
appLines.forEach(line => {
|
|
2973
|
+
resultLines.push(` ${line.trim()}`);
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
else {
|
|
2977
|
+
resultLines.push(" App not found in CPU stats (may not be running)");
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
else {
|
|
2981
|
+
// Show top processes
|
|
2982
|
+
resultLines.push("\nš Top Processes:");
|
|
2983
|
+
const processLines = lines
|
|
2984
|
+
.filter(line => line.includes("%") && !line.includes("TOTAL"))
|
|
2985
|
+
.slice(0, 10);
|
|
2986
|
+
processLines.forEach(line => {
|
|
2987
|
+
resultLines.push(` ${line.trim()}`);
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
// Get per-core usage
|
|
2991
|
+
try {
|
|
2992
|
+
const { stdout: coreInfo } = await execAsync(`adb ${deviceFlag} shell cat /proc/stat`, { timeout: 5000 });
|
|
2993
|
+
const coreLines = coreInfo.split("\n").filter(line => line.startsWith("cpu"));
|
|
2994
|
+
if (coreLines.length > 1) {
|
|
2995
|
+
resultLines.push(`\nš» CPU Cores: ${coreLines.length - 1}`);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
catch {
|
|
2999
|
+
// Core info not available
|
|
3000
|
+
}
|
|
3001
|
+
let result = resultLines.join("\n");
|
|
3002
|
+
if (check.message)
|
|
3003
|
+
result += `\n\n${check.message}`;
|
|
3004
|
+
return result;
|
|
3005
|
+
}
|
|
3006
|
+
catch (error) {
|
|
3007
|
+
let result = `Error getting CPU usage: ${error.message}`;
|
|
3008
|
+
if (check.message)
|
|
3009
|
+
result += `\n\n${check.message}`;
|
|
3010
|
+
return result;
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
async function getMemoryUsage(packageName, device) {
|
|
3014
|
+
const check = await requireAdvanced("get_memory_usage");
|
|
3015
|
+
if (!check.allowed)
|
|
3016
|
+
return check.message;
|
|
3017
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
3018
|
+
try {
|
|
3019
|
+
const resultLines = [];
|
|
3020
|
+
resultLines.push("š§ Memory Usage");
|
|
3021
|
+
resultLines.push("ā".repeat(50));
|
|
3022
|
+
resultLines.push(`\nš± App: ${packageName}\n`);
|
|
3023
|
+
// Get detailed memory info for the app
|
|
3024
|
+
const { stdout: memInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys meminfo ${packageName}`, { maxBuffer: 2 * 1024 * 1024 });
|
|
3025
|
+
// Parse key memory metrics
|
|
3026
|
+
const lines = memInfo.split("\n");
|
|
3027
|
+
// Find summary section
|
|
3028
|
+
const summaryStart = lines.findIndex(line => line.includes("App Summary"));
|
|
3029
|
+
if (summaryStart !== -1) {
|
|
3030
|
+
resultLines.push("š App Summary:");
|
|
3031
|
+
for (let i = summaryStart + 1; i < Math.min(summaryStart + 10, lines.length); i++) {
|
|
3032
|
+
const line = lines[i].trim();
|
|
3033
|
+
if (line && !line.startsWith("--")) {
|
|
3034
|
+
resultLines.push(` ${line}`);
|
|
3035
|
+
}
|
|
3036
|
+
if (line.includes("TOTAL"))
|
|
3037
|
+
break;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
// Find PSS and heap info
|
|
3041
|
+
const pssMatch = memInfo.match(/TOTAL PSS:\s+(\d+)/i) || memInfo.match(/TOTAL:\s+(\d+)/);
|
|
3042
|
+
const heapMatch = memInfo.match(/Native Heap:\s+(\d+)/);
|
|
3043
|
+
const javaHeapMatch = memInfo.match(/Java Heap:\s+(\d+)/);
|
|
3044
|
+
if (pssMatch || heapMatch) {
|
|
3045
|
+
resultLines.push("\nš Key Metrics:");
|
|
3046
|
+
if (pssMatch) {
|
|
3047
|
+
const pssKb = parseInt(pssMatch[1]);
|
|
3048
|
+
resultLines.push(` Total PSS: ${(pssKb / 1024).toFixed(1)} MB`);
|
|
3049
|
+
}
|
|
3050
|
+
if (javaHeapMatch) {
|
|
3051
|
+
const heapKb = parseInt(javaHeapMatch[1]);
|
|
3052
|
+
resultLines.push(` Java Heap: ${(heapKb / 1024).toFixed(1)} MB`);
|
|
3053
|
+
}
|
|
3054
|
+
if (heapMatch) {
|
|
3055
|
+
const nativeKb = parseInt(heapMatch[1]);
|
|
3056
|
+
resultLines.push(` Native Heap: ${(nativeKb / 1024).toFixed(1)} MB`);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
// Memory warnings
|
|
3060
|
+
if (memInfo.includes("Low memory")) {
|
|
3061
|
+
resultLines.push("\nā ļø Warning: Device is in low memory state");
|
|
3062
|
+
}
|
|
3063
|
+
let result = resultLines.join("\n");
|
|
3064
|
+
if (check.message)
|
|
3065
|
+
result += `\n\n${check.message}`;
|
|
3066
|
+
return result;
|
|
3067
|
+
}
|
|
3068
|
+
catch (error) {
|
|
3069
|
+
if (error.message?.includes("No process found")) {
|
|
3070
|
+
let result = `App '${packageName}' is not running.\n\nStart the app first, then check memory usage.`;
|
|
3071
|
+
if (check.message)
|
|
3072
|
+
result += `\n\n${check.message}`;
|
|
3073
|
+
return result;
|
|
3074
|
+
}
|
|
3075
|
+
let result = `Error getting memory usage: ${error.message}`;
|
|
3076
|
+
if (check.message)
|
|
3077
|
+
result += `\n\n${check.message}`;
|
|
3078
|
+
return result;
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
async function getFpsStats(packageName, device, reset = false) {
|
|
3082
|
+
const check = await requireAdvanced("get_fps_stats");
|
|
3083
|
+
if (!check.allowed)
|
|
3084
|
+
return check.message;
|
|
3085
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
3086
|
+
try {
|
|
3087
|
+
const resultLines = [];
|
|
3088
|
+
resultLines.push("š® Frame Rendering Stats (FPS)");
|
|
3089
|
+
resultLines.push("ā".repeat(50));
|
|
3090
|
+
resultLines.push(`\nš± App: ${packageName}\n`);
|
|
3091
|
+
// Reset stats if requested
|
|
3092
|
+
if (reset) {
|
|
3093
|
+
await execAsync(`adb ${deviceFlag} shell dumpsys gfxinfo ${packageName} reset`);
|
|
3094
|
+
resultLines.push("š Stats reset. Interact with the app, then run this tool again.\n");
|
|
3095
|
+
}
|
|
3096
|
+
// Get graphics info
|
|
3097
|
+
const { stdout: gfxInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys gfxinfo ${packageName}`, { maxBuffer: 2 * 1024 * 1024 });
|
|
3098
|
+
const lines = gfxInfo.split("\n");
|
|
3099
|
+
// Parse total frames
|
|
3100
|
+
const totalMatch = gfxInfo.match(/Total frames rendered:\s*(\d+)/);
|
|
3101
|
+
const jankyMatch = gfxInfo.match(/Janky frames:\s*(\d+)\s*\(([^)]+)\)/);
|
|
3102
|
+
const percentile50 = gfxInfo.match(/50th percentile:\s*(\d+)ms/);
|
|
3103
|
+
const percentile90 = gfxInfo.match(/90th percentile:\s*(\d+)ms/);
|
|
3104
|
+
const percentile95 = gfxInfo.match(/95th percentile:\s*(\d+)ms/);
|
|
3105
|
+
const percentile99 = gfxInfo.match(/99th percentile:\s*(\d+)ms/);
|
|
3106
|
+
if (totalMatch) {
|
|
3107
|
+
const totalFrames = parseInt(totalMatch[1]);
|
|
3108
|
+
resultLines.push(`š Total Frames: ${totalFrames}`);
|
|
3109
|
+
if (jankyMatch) {
|
|
3110
|
+
const jankyFrames = parseInt(jankyMatch[1]);
|
|
3111
|
+
const jankyPercent = jankyMatch[2];
|
|
3112
|
+
resultLines.push(`š Janky Frames: ${jankyFrames} (${jankyPercent})`);
|
|
3113
|
+
// Calculate smooth FPS estimate
|
|
3114
|
+
const smoothFrames = totalFrames - jankyFrames;
|
|
3115
|
+
const smoothPercent = ((smoothFrames / totalFrames) * 100).toFixed(1);
|
|
3116
|
+
resultLines.push(`⨠Smooth Frames: ${smoothFrames} (${smoothPercent}%)`);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
// Frame timing percentiles
|
|
3120
|
+
if (percentile50 || percentile90) {
|
|
3121
|
+
resultLines.push("\nā±ļø Frame Time Percentiles:");
|
|
3122
|
+
if (percentile50)
|
|
3123
|
+
resultLines.push(` 50th: ${percentile50[1]}ms`);
|
|
3124
|
+
if (percentile90)
|
|
3125
|
+
resultLines.push(` 90th: ${percentile90[1]}ms`);
|
|
3126
|
+
if (percentile95)
|
|
3127
|
+
resultLines.push(` 95th: ${percentile95[1]}ms`);
|
|
3128
|
+
if (percentile99)
|
|
3129
|
+
resultLines.push(` 99th: ${percentile99[1]}ms`);
|
|
3130
|
+
}
|
|
3131
|
+
// Look for slow frames breakdown
|
|
3132
|
+
const slowUIMatch = gfxInfo.match(/Number Slow UI thread:\s*(\d+)/);
|
|
3133
|
+
const slowBitmapMatch = gfxInfo.match(/Number Slow bitmap uploads:\s*(\d+)/);
|
|
3134
|
+
const slowDrawMatch = gfxInfo.match(/Number Slow issue draw commands:\s*(\d+)/);
|
|
3135
|
+
if (slowUIMatch || slowBitmapMatch || slowDrawMatch) {
|
|
3136
|
+
resultLines.push("\nš Slow Frame Analysis:");
|
|
3137
|
+
if (slowUIMatch && parseInt(slowUIMatch[1]) > 0) {
|
|
3138
|
+
resultLines.push(` Slow UI thread: ${slowUIMatch[1]}`);
|
|
3139
|
+
}
|
|
3140
|
+
if (slowBitmapMatch && parseInt(slowBitmapMatch[1]) > 0) {
|
|
3141
|
+
resultLines.push(` Slow bitmap uploads: ${slowBitmapMatch[1]}`);
|
|
3142
|
+
}
|
|
3143
|
+
if (slowDrawMatch && parseInt(slowDrawMatch[1]) > 0) {
|
|
3144
|
+
resultLines.push(` Slow draw commands: ${slowDrawMatch[1]}`);
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
// Performance recommendation
|
|
3148
|
+
if (jankyMatch) {
|
|
3149
|
+
const jankyPercent = parseFloat(jankyMatch[2]);
|
|
3150
|
+
if (jankyPercent > 10) {
|
|
3151
|
+
resultLines.push("\nā ļø Performance Issue: High jank percentage");
|
|
3152
|
+
resultLines.push(" Consider optimizing heavy UI operations");
|
|
3153
|
+
}
|
|
3154
|
+
else if (jankyPercent < 5) {
|
|
3155
|
+
resultLines.push("\nā
Good Performance: Low jank percentage");
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
if (!totalMatch) {
|
|
3159
|
+
resultLines.push("No frame data available.");
|
|
3160
|
+
resultLines.push("Make sure the app is visible and interact with it first.");
|
|
3161
|
+
}
|
|
3162
|
+
let result = resultLines.join("\n");
|
|
3163
|
+
if (check.message)
|
|
3164
|
+
result += `\n\n${check.message}`;
|
|
3165
|
+
return result;
|
|
3166
|
+
}
|
|
3167
|
+
catch (error) {
|
|
3168
|
+
let result = `Error getting FPS stats: ${error.message}`;
|
|
3169
|
+
if (check.message)
|
|
3170
|
+
result += `\n\n${check.message}`;
|
|
3171
|
+
return result;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
async function getBatteryStats(packageName, device) {
|
|
3175
|
+
const check = await requireAdvanced("get_battery_stats");
|
|
3176
|
+
if (!check.allowed)
|
|
3177
|
+
return check.message;
|
|
3178
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
3179
|
+
try {
|
|
3180
|
+
const resultLines = [];
|
|
3181
|
+
resultLines.push("š Battery Statistics");
|
|
3182
|
+
resultLines.push("ā".repeat(50));
|
|
3183
|
+
// Get current battery level and status
|
|
3184
|
+
const { stdout: batteryInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys battery`, { timeout: 5000 });
|
|
3185
|
+
const levelMatch = batteryInfo.match(/level:\s*(\d+)/);
|
|
3186
|
+
const statusMatch = batteryInfo.match(/status:\s*(\d+)/);
|
|
3187
|
+
const healthMatch = batteryInfo.match(/health:\s*(\d+)/);
|
|
3188
|
+
const tempMatch = batteryInfo.match(/temperature:\s*(\d+)/);
|
|
3189
|
+
resultLines.push("\nš Current Status:");
|
|
3190
|
+
if (levelMatch) {
|
|
3191
|
+
resultLines.push(` Battery Level: ${levelMatch[1]}%`);
|
|
3192
|
+
}
|
|
3193
|
+
if (statusMatch) {
|
|
3194
|
+
const statuses = ["Unknown", "Charging", "Discharging", "Not charging", "Full"];
|
|
3195
|
+
const status = statuses[parseInt(statusMatch[1])] || "Unknown";
|
|
3196
|
+
resultLines.push(` Status: ${status}`);
|
|
3197
|
+
}
|
|
3198
|
+
if (tempMatch) {
|
|
3199
|
+
const temp = parseInt(tempMatch[1]) / 10;
|
|
3200
|
+
resultLines.push(` Temperature: ${temp}°C`);
|
|
3201
|
+
}
|
|
3202
|
+
// Get battery stats (power consumption)
|
|
3203
|
+
if (packageName) {
|
|
3204
|
+
resultLines.push(`\nš± App: ${packageName}`);
|
|
3205
|
+
try {
|
|
3206
|
+
const { stdout: appStats } = await execAsync(`adb ${deviceFlag} shell dumpsys batterystats ${packageName}`, { maxBuffer: 5 * 1024 * 1024, timeout: 10000 });
|
|
3207
|
+
// Parse power usage
|
|
3208
|
+
const powerMatch = appStats.match(/Estimated power use \(mAh\)[\s\S]*?Uid \S+:\s*([\d.]+)/);
|
|
3209
|
+
if (powerMatch) {
|
|
3210
|
+
resultLines.push(` Power Used: ${powerMatch[1]} mAh`);
|
|
3211
|
+
}
|
|
3212
|
+
// CPU time
|
|
3213
|
+
const cpuMatch = appStats.match(/Total cpu time:\s*u=(\d+)ms\s*s=(\d+)ms/);
|
|
3214
|
+
if (cpuMatch) {
|
|
3215
|
+
const userMs = parseInt(cpuMatch[1]);
|
|
3216
|
+
const sysMs = parseInt(cpuMatch[2]);
|
|
3217
|
+
resultLines.push(` CPU Time: ${((userMs + sysMs) / 1000).toFixed(1)}s (user: ${(userMs / 1000).toFixed(1)}s, sys: ${(sysMs / 1000).toFixed(1)}s)`);
|
|
3218
|
+
}
|
|
3219
|
+
// Network usage
|
|
3220
|
+
const networkMatch = appStats.match(/Network:\s*([\d.]+)\s*MB\s*received,\s*([\d.]+)\s*MB\s*transmitted/i);
|
|
3221
|
+
if (networkMatch) {
|
|
3222
|
+
resultLines.push(` Network: ${networkMatch[1]} MB ā, ${networkMatch[2]} MB ā`);
|
|
3223
|
+
}
|
|
3224
|
+
// Wakelock time
|
|
3225
|
+
const wakelockMatch = appStats.match(/Wake lock\s+\S+\s+.*?realtime=(\d+)/);
|
|
3226
|
+
if (wakelockMatch) {
|
|
3227
|
+
const wakeSec = parseInt(wakelockMatch[1]) / 1000;
|
|
3228
|
+
if (wakeSec > 0) {
|
|
3229
|
+
resultLines.push(` Wake Lock: ${wakeSec.toFixed(1)}s`);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
catch {
|
|
3234
|
+
resultLines.push(" Detailed stats not available for this app");
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
else {
|
|
3238
|
+
// Show top battery consumers
|
|
3239
|
+
resultLines.push("\nš Top Power Consumers:");
|
|
3240
|
+
try {
|
|
3241
|
+
const { stdout: stats } = await execAsync(`adb ${deviceFlag} shell dumpsys batterystats`, { maxBuffer: 10 * 1024 * 1024, timeout: 15000 });
|
|
3242
|
+
// Parse estimated power section
|
|
3243
|
+
const powerSection = stats.match(/Estimated power use[\s\S]*?(?=\n\n|\nStatistics)/);
|
|
3244
|
+
if (powerSection) {
|
|
3245
|
+
const powerLines = powerSection[0].split("\n").slice(1, 8);
|
|
3246
|
+
powerLines.forEach(line => {
|
|
3247
|
+
const trimmed = line.trim();
|
|
3248
|
+
if (trimmed && !trimmed.startsWith("Estimated")) {
|
|
3249
|
+
resultLines.push(` ${trimmed}`);
|
|
3250
|
+
}
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
catch {
|
|
3255
|
+
resultLines.push(" Could not retrieve detailed battery stats");
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
let result = resultLines.join("\n");
|
|
3259
|
+
if (check.message)
|
|
3260
|
+
result += `\n\n${check.message}`;
|
|
3261
|
+
return result;
|
|
3262
|
+
}
|
|
3263
|
+
catch (error) {
|
|
3264
|
+
let result = `Error getting battery stats: ${error.message}`;
|
|
3265
|
+
if (check.message)
|
|
3266
|
+
result += `\n\n${check.message}`;
|
|
3267
|
+
return result;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
async function getPerformanceSnapshot(packageName, device) {
|
|
3271
|
+
const check = await requireAdvanced("get_performance_snapshot");
|
|
3272
|
+
if (!check.allowed)
|
|
3273
|
+
return check.message;
|
|
3274
|
+
const deviceFlag = device ? `-s ${device}` : "";
|
|
3275
|
+
try {
|
|
3276
|
+
const resultLines = [];
|
|
3277
|
+
resultLines.push("š Performance Snapshot");
|
|
3278
|
+
resultLines.push("ā".repeat(50));
|
|
3279
|
+
resultLines.push(`\nš± App: ${packageName}`);
|
|
3280
|
+
resultLines.push(`ā° Time: ${new Date().toLocaleString()}\n`);
|
|
3281
|
+
// CPU Usage
|
|
3282
|
+
try {
|
|
3283
|
+
const { stdout: cpuInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys cpuinfo | grep -i "${packageName}"`, { timeout: 5000 });
|
|
3284
|
+
if (cpuInfo.trim()) {
|
|
3285
|
+
resultLines.push("š» CPU:");
|
|
3286
|
+
cpuInfo.split("\n").slice(0, 3).forEach(line => {
|
|
3287
|
+
if (line.trim())
|
|
3288
|
+
resultLines.push(` ${line.trim()}`);
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
catch {
|
|
3293
|
+
resultLines.push("š» CPU: N/A");
|
|
3294
|
+
}
|
|
3295
|
+
// Memory Usage
|
|
3296
|
+
try {
|
|
3297
|
+
const { stdout: memInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys meminfo ${packageName}`, { maxBuffer: 2 * 1024 * 1024, timeout: 5000 });
|
|
3298
|
+
const pssMatch = memInfo.match(/TOTAL PSS:\s*(\d+)/i) || memInfo.match(/TOTAL:\s+(\d+)/);
|
|
3299
|
+
if (pssMatch) {
|
|
3300
|
+
const pssMb = (parseInt(pssMatch[1]) / 1024).toFixed(1);
|
|
3301
|
+
resultLines.push(`\nš§ Memory: ${pssMb} MB (PSS)`);
|
|
3302
|
+
}
|
|
3303
|
+
const heapMatch = memInfo.match(/Java Heap:\s+(\d+)/);
|
|
3304
|
+
const nativeMatch = memInfo.match(/Native Heap:\s+(\d+)/);
|
|
3305
|
+
if (heapMatch) {
|
|
3306
|
+
resultLines.push(` Java Heap: ${(parseInt(heapMatch[1]) / 1024).toFixed(1)} MB`);
|
|
3307
|
+
}
|
|
3308
|
+
if (nativeMatch) {
|
|
3309
|
+
resultLines.push(` Native: ${(parseInt(nativeMatch[1]) / 1024).toFixed(1)} MB`);
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
catch {
|
|
3313
|
+
resultLines.push("\nš§ Memory: App not running or no data");
|
|
3314
|
+
}
|
|
3315
|
+
// FPS Stats
|
|
3316
|
+
try {
|
|
3317
|
+
const { stdout: gfxInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys gfxinfo ${packageName}`, { maxBuffer: 1024 * 1024, timeout: 5000 });
|
|
3318
|
+
const totalMatch = gfxInfo.match(/Total frames rendered:\s*(\d+)/);
|
|
3319
|
+
const jankyMatch = gfxInfo.match(/Janky frames:\s*(\d+)\s*\(([^)]+)\)/);
|
|
3320
|
+
if (totalMatch) {
|
|
3321
|
+
resultLines.push(`\nš® Frames: ${totalMatch[1]} rendered`);
|
|
3322
|
+
if (jankyMatch) {
|
|
3323
|
+
resultLines.push(` Janky: ${jankyMatch[1]} (${jankyMatch[2]})`);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
catch {
|
|
3328
|
+
resultLines.push("\nš® Frames: No data");
|
|
3329
|
+
}
|
|
3330
|
+
// Battery
|
|
3331
|
+
try {
|
|
3332
|
+
const { stdout: batteryInfo } = await execAsync(`adb ${deviceFlag} shell dumpsys battery`, { timeout: 3000 });
|
|
3333
|
+
const levelMatch = batteryInfo.match(/level:\s*(\d+)/);
|
|
3334
|
+
const tempMatch = batteryInfo.match(/temperature:\s*(\d+)/);
|
|
3335
|
+
if (levelMatch) {
|
|
3336
|
+
resultLines.push(`\nš Battery: ${levelMatch[1]}%`);
|
|
3337
|
+
if (tempMatch) {
|
|
3338
|
+
resultLines.push(` Temperature: ${parseInt(tempMatch[1]) / 10}°C`);
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
catch {
|
|
3343
|
+
resultLines.push("\nš Battery: N/A");
|
|
3344
|
+
}
|
|
3345
|
+
// Network usage from app
|
|
3346
|
+
try {
|
|
3347
|
+
const { stdout: netStats } = await execAsync(`adb ${deviceFlag} shell cat /proc/net/xt_qtaguid/stats`, { timeout: 3000 });
|
|
3348
|
+
// Just note that network is available
|
|
3349
|
+
if (netStats.includes(packageName)) {
|
|
3350
|
+
resultLines.push("\nš” Network: Active");
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
catch {
|
|
3354
|
+
// Network stats not available
|
|
3355
|
+
}
|
|
3356
|
+
resultLines.push("\n" + "ā".repeat(50));
|
|
3357
|
+
resultLines.push("Use individual tools for detailed metrics:");
|
|
3358
|
+
resultLines.push(" get_cpu_usage, get_memory_usage, get_fps_stats, get_battery_stats");
|
|
3359
|
+
let result = resultLines.join("\n");
|
|
3360
|
+
if (check.message)
|
|
3361
|
+
result += `\n\n${check.message}`;
|
|
3362
|
+
return result;
|
|
3363
|
+
}
|
|
3364
|
+
catch (error) {
|
|
3365
|
+
let result = `Error getting performance snapshot: ${error.message}`;
|
|
3366
|
+
if (check.message)
|
|
3367
|
+
result += `\n\n${check.message}`;
|
|
3368
|
+
return result;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
// ============================================================================
|
|
3372
|
+
// MCP SERVER SETUP
|
|
3373
|
+
// ============================================================================
|
|
3374
|
+
const server = new Server({
|
|
3375
|
+
name: "claude-mobile-dev-mcp",
|
|
3376
|
+
version: "0.1.0",
|
|
3377
|
+
}, {
|
|
3378
|
+
capabilities: {
|
|
3379
|
+
tools: {},
|
|
3380
|
+
},
|
|
3381
|
+
});
|
|
3382
|
+
// Handle tool listing
|
|
3383
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3384
|
+
tools,
|
|
3385
|
+
}));
|
|
3386
|
+
// Handle tool execution
|
|
3387
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3388
|
+
const { name, arguments: args } = request.params;
|
|
3389
|
+
try {
|
|
3390
|
+
// License tools
|
|
3391
|
+
if (name === "get_license_status" || name === "set_license_key") {
|
|
3392
|
+
const result = await handleLicenseTool(name, args || {});
|
|
3393
|
+
return { content: [{ type: "text", text: result }] };
|
|
3394
|
+
}
|
|
3395
|
+
// Core tools
|
|
3396
|
+
switch (name) {
|
|
3397
|
+
case "get_metro_logs": {
|
|
3398
|
+
const result = await getMetroLogs(args?.lines, args?.filter);
|
|
3399
|
+
return { content: [{ type: "text", text: result }] };
|
|
3400
|
+
}
|
|
3401
|
+
case "get_adb_logs": {
|
|
3402
|
+
const result = await getAdbLogs(args?.lines, args?.filter, args?.level);
|
|
3403
|
+
return { content: [{ type: "text", text: result }] };
|
|
3404
|
+
}
|
|
3405
|
+
case "screenshot_emulator": {
|
|
3406
|
+
const result = await screenshotEmulator(args?.device);
|
|
3407
|
+
if (result.success && result.data) {
|
|
3408
|
+
const content = [
|
|
3409
|
+
{
|
|
3410
|
+
type: "image",
|
|
3411
|
+
data: result.data,
|
|
3412
|
+
mimeType: result.mimeType,
|
|
3413
|
+
},
|
|
3414
|
+
];
|
|
3415
|
+
// Add trial warning if present
|
|
3416
|
+
if (result.trialMessage) {
|
|
3417
|
+
content.push({ type: "text", text: result.trialMessage });
|
|
3418
|
+
}
|
|
3419
|
+
return { content };
|
|
3420
|
+
}
|
|
3421
|
+
return { content: [{ type: "text", text: result.error }] };
|
|
3422
|
+
}
|
|
3423
|
+
case "list_devices": {
|
|
3424
|
+
const result = await listDevices();
|
|
2133
3425
|
return { content: [{ type: "text", text: result }] };
|
|
2134
3426
|
}
|
|
2135
3427
|
case "check_metro_status": {
|
|
@@ -2289,6 +3581,69 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2289
3581
|
const result = await searchReactComponents(args?.query, args?.port);
|
|
2290
3582
|
return { content: [{ type: "text", text: result }] };
|
|
2291
3583
|
}
|
|
3584
|
+
// NETWORK INSPECTION TOOLS
|
|
3585
|
+
case "get_network_requests": {
|
|
3586
|
+
const result = await getNetworkRequests(args?.lines, args?.filter, args?.device);
|
|
3587
|
+
return { content: [{ type: "text", text: result }] };
|
|
3588
|
+
}
|
|
3589
|
+
case "start_network_monitoring": {
|
|
3590
|
+
const result = await startNetworkMonitoring(args?.device);
|
|
3591
|
+
return { content: [{ type: "text", text: result }] };
|
|
3592
|
+
}
|
|
3593
|
+
case "stop_network_monitoring": {
|
|
3594
|
+
const result = await stopNetworkMonitoring();
|
|
3595
|
+
return { content: [{ type: "text", text: result }] };
|
|
3596
|
+
}
|
|
3597
|
+
case "get_network_stats": {
|
|
3598
|
+
const result = await getNetworkStats(args?.device);
|
|
3599
|
+
return { content: [{ type: "text", text: result }] };
|
|
3600
|
+
}
|
|
3601
|
+
case "analyze_request": {
|
|
3602
|
+
const result = await analyzeRequest(args?.index);
|
|
3603
|
+
return { content: [{ type: "text", text: result }] };
|
|
3604
|
+
}
|
|
3605
|
+
// EXPO DEVTOOLS TOOLS
|
|
3606
|
+
case "check_expo_status": {
|
|
3607
|
+
const result = await checkExpoStatus(args?.port);
|
|
3608
|
+
return { content: [{ type: "text", text: result }] };
|
|
3609
|
+
}
|
|
3610
|
+
case "get_expo_config": {
|
|
3611
|
+
const result = await getExpoConfig(args?.projectPath);
|
|
3612
|
+
return { content: [{ type: "text", text: result }] };
|
|
3613
|
+
}
|
|
3614
|
+
case "expo_dev_menu": {
|
|
3615
|
+
const result = await expoDevMenu(args?.device);
|
|
3616
|
+
return { content: [{ type: "text", text: result }] };
|
|
3617
|
+
}
|
|
3618
|
+
case "expo_reload": {
|
|
3619
|
+
const result = await expoReload(args?.device);
|
|
3620
|
+
return { content: [{ type: "text", text: result }] };
|
|
3621
|
+
}
|
|
3622
|
+
case "get_eas_builds": {
|
|
3623
|
+
const result = await getEasBuilds(args?.platform, args?.limit);
|
|
3624
|
+
return { content: [{ type: "text", text: result }] };
|
|
3625
|
+
}
|
|
3626
|
+
// PERFORMANCE METRICS TOOLS
|
|
3627
|
+
case "get_cpu_usage": {
|
|
3628
|
+
const result = await getCpuUsage(args?.packageName, args?.device);
|
|
3629
|
+
return { content: [{ type: "text", text: result }] };
|
|
3630
|
+
}
|
|
3631
|
+
case "get_memory_usage": {
|
|
3632
|
+
const result = await getMemoryUsage(args?.packageName, args?.device);
|
|
3633
|
+
return { content: [{ type: "text", text: result }] };
|
|
3634
|
+
}
|
|
3635
|
+
case "get_fps_stats": {
|
|
3636
|
+
const result = await getFpsStats(args?.packageName, args?.device, args?.reset);
|
|
3637
|
+
return { content: [{ type: "text", text: result }] };
|
|
3638
|
+
}
|
|
3639
|
+
case "get_battery_stats": {
|
|
3640
|
+
const result = await getBatteryStats(args?.packageName, args?.device);
|
|
3641
|
+
return { content: [{ type: "text", text: result }] };
|
|
3642
|
+
}
|
|
3643
|
+
case "get_performance_snapshot": {
|
|
3644
|
+
const result = await getPerformanceSnapshot(args?.packageName, args?.device);
|
|
3645
|
+
return { content: [{ type: "text", text: result }] };
|
|
3646
|
+
}
|
|
2292
3647
|
default:
|
|
2293
3648
|
return {
|
|
2294
3649
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|