@sensaiorg/adapter-android 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android-adapter.d.ts.map +1 -0
- package/dist/android-adapter.js +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/tools/accessibility.d.ts.map +1 -0
- package/dist/tools/accessibility.js +85 -0
- package/dist/tools/adb.d.ts.map +1 -0
- package/dist/tools/adb.js +66 -0
- package/dist/tools/app-state.d.ts.map +1 -0
- package/dist/tools/app-state.js +173 -0
- package/dist/tools/diagnose.d.ts.map +1 -0
- package/dist/tools/diagnose.js +128 -0
- package/dist/tools/hot-reload.d.ts.map +1 -0
- package/dist/tools/hot-reload.js +97 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +66 -0
- package/dist/tools/interaction.d.ts.map +1 -0
- package/dist/tools/interaction.js +395 -0
- package/dist/tools/logcat.d.ts.map +1 -0
- package/dist/tools/logcat.js +216 -0
- package/dist/tools/network.d.ts.map +1 -0
- package/dist/tools/network.js +123 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +143 -0
- package/dist/tools/recording.d.ts.map +1 -0
- package/dist/tools/recording.js +102 -0
- package/dist/tools/rn-tools.d.ts.map +1 -0
- package/dist/tools/rn-tools.js +120 -0
- package/dist/tools/smart-actions.d.ts.map +1 -0
- package/dist/tools/smart-actions.js +506 -0
- package/dist/tools/ui-tree.d.ts.map +1 -0
- package/dist/tools/ui-tree.js +226 -0
- package/dist/transport/adb-client.d.ts.map +1 -0
- package/dist/transport/adb-client.js +124 -0
- package/dist/transport/adb-client.test.d.ts.map +1 -0
- package/dist/transport/adb-client.test.js +153 -0
- package/dist/transport/agent-client.d.ts.map +1 -0
- package/dist/transport/agent-client.js +157 -0
- package/dist/transport/agent-client.test.d.ts.map +1 -0
- package/dist/transport/agent-client.test.js +199 -0
- package/dist/transport/connection-manager.d.ts.map +1 -0
- package/dist/transport/connection-manager.js +119 -0
- package/dist/util/logcat-parser.d.ts.map +1 -0
- package/dist/util/logcat-parser.js +79 -0
- package/dist/util/safety.d.ts.map +1 -0
- package/dist/util/safety.js +132 -0
- package/dist/util/safety.test.d.ts.map +1 -0
- package/dist/util/safety.test.js +205 -0
- package/dist/util/text-extractor.d.ts.map +1 -0
- package/dist/util/text-extractor.js +71 -0
- package/dist/util/ui-tree-cache.d.ts.map +1 -0
- package/dist/util/ui-tree-cache.js +46 -0
- package/dist/util/ui-tree-cache.test.d.ts.map +1 -0
- package/dist/util/ui-tree-cache.test.js +84 -0
- package/dist/util/ui-tree-parser.d.ts.map +1 -0
- package/dist/util/ui-tree-parser.js +123 -0
- package/dist/util/ui-tree-parser.test.d.ts.map +1 -0
- package/dist/util/ui-tree-parser.test.js +167 -0
- package/package.json +22 -0
- package/src/android-adapter.ts +124 -0
- package/src/index.ts +8 -0
- package/src/tools/accessibility.ts +94 -0
- package/src/tools/adb.ts +75 -0
- package/src/tools/app-state.ts +193 -0
- package/src/tools/diagnose.ts +146 -0
- package/src/tools/hot-reload.ts +103 -0
- package/src/tools/index.ts +66 -0
- package/src/tools/interaction.ts +448 -0
- package/src/tools/logcat.ts +252 -0
- package/src/tools/network.ts +145 -0
- package/src/tools/performance.ts +169 -0
- package/src/tools/recording.ts +123 -0
- package/src/tools/rn-tools.ts +143 -0
- package/src/tools/smart-actions.ts +593 -0
- package/src/tools/ui-tree.ts +258 -0
- package/src/transport/adb-client.test.ts +228 -0
- package/src/transport/adb-client.ts +139 -0
- package/src/transport/agent-client.test.ts +267 -0
- package/src/transport/agent-client.ts +188 -0
- package/src/transport/connection-manager.ts +140 -0
- package/src/util/logcat-parser.ts +94 -0
- package/src/util/safety.test.ts +251 -0
- package/src/util/safety.ts +143 -0
- package/src/util/text-extractor.ts +87 -0
- package/src/util/ui-tree-cache.test.ts +105 -0
- package/src/util/ui-tree-cache.ts +54 -0
- package/src/util/ui-tree-parser.test.ts +182 -0
- package/src/util/ui-tree-parser.ts +169 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Logcat Tools - Read and filter Android system logs.
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - get_logcat: Filtered log retrieval with level/tag/grep/since/maxLines
|
|
7
|
+
* - get_crash_info: Extract crash reports (JS exceptions, native crashes, ANRs)
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.registerLogcatTools = registerLogcatTools;
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
const logcat_parser_js_1 = require("../util/logcat-parser.js");
|
|
13
|
+
/** Tags commonly associated with React Native errors. */
|
|
14
|
+
const RN_ERROR_TAGS = [
|
|
15
|
+
"ReactNativeJS",
|
|
16
|
+
"ReactNative",
|
|
17
|
+
"hermes",
|
|
18
|
+
"HermesVM",
|
|
19
|
+
"com.facebook.react",
|
|
20
|
+
];
|
|
21
|
+
/** Tags for native Android crashes. */
|
|
22
|
+
const CRASH_TAGS = [
|
|
23
|
+
"AndroidRuntime",
|
|
24
|
+
"FATAL",
|
|
25
|
+
"DEBUG",
|
|
26
|
+
"tombstoned",
|
|
27
|
+
"crash_dump",
|
|
28
|
+
];
|
|
29
|
+
function registerLogcatTools(server, cm) {
|
|
30
|
+
/**
|
|
31
|
+
* get_logcat - Retrieve filtered Android logs.
|
|
32
|
+
*/
|
|
33
|
+
server.tool("get_logcat", "Get Android logcat entries with optional filtering by log level, tag, text pattern, and time range. Returns structured log entries.", {
|
|
34
|
+
level: zod_1.z
|
|
35
|
+
.enum(["V", "D", "I", "W", "E", "F"])
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Minimum log level (V=verbose, D=debug, I=info, W=warn, E=error, F=fatal)"),
|
|
38
|
+
tag: zod_1.z.string().optional().describe("Filter by tag (case-insensitive substring)"),
|
|
39
|
+
since: zod_1.z.string().optional().describe("Only logs since this time (e.g., '5s', '1m', '30m', '1h')"),
|
|
40
|
+
grep: zod_1.z.string().optional().describe("Regex pattern to filter log messages"),
|
|
41
|
+
maxLines: zod_1.z.number().optional().describe("Maximum number of entries to return (default: 200)"),
|
|
42
|
+
packageOnly: zod_1.z.boolean().optional().describe("Only show logs from the target package"),
|
|
43
|
+
deduplicate: zod_1.z.boolean().optional().describe("Collapse consecutive identical messages into '[repeated Nx] message' (default: true)"),
|
|
44
|
+
}, async (params) => {
|
|
45
|
+
try {
|
|
46
|
+
const maxLines = params.maxLines ?? 200;
|
|
47
|
+
const shouldDeduplicate = params.deduplicate !== false; // default: true
|
|
48
|
+
const targetPackage = process.env.TARGET_PACKAGE ?? "com.emudebug.target";
|
|
49
|
+
// Build logcat command
|
|
50
|
+
const cmdParts = ["logcat", "-d", "-v", "threadtime"];
|
|
51
|
+
// Time filter
|
|
52
|
+
if (params.since) {
|
|
53
|
+
cmdParts.push("-T", `'${params.since}'`);
|
|
54
|
+
}
|
|
55
|
+
// Package filter via PID
|
|
56
|
+
let pid = null;
|
|
57
|
+
if (params.packageOnly) {
|
|
58
|
+
try {
|
|
59
|
+
const pidOutput = await cm.adb.shell(`pidof ${targetPackage}`);
|
|
60
|
+
pid = pidOutput.trim();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Package might not be running
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const raw = await cm.adb.shell(cmdParts.join(" "), 15_000);
|
|
67
|
+
let entries = (0, logcat_parser_js_1.parseLogcat)(raw);
|
|
68
|
+
// Apply filters
|
|
69
|
+
if (pid) {
|
|
70
|
+
entries = entries.filter((e) => e.pid === pid);
|
|
71
|
+
}
|
|
72
|
+
if (params.level) {
|
|
73
|
+
entries = (0, logcat_parser_js_1.filterByLevel)(entries, params.level);
|
|
74
|
+
}
|
|
75
|
+
if (params.tag) {
|
|
76
|
+
entries = (0, logcat_parser_js_1.filterByTag)(entries, params.tag);
|
|
77
|
+
}
|
|
78
|
+
if (params.grep) {
|
|
79
|
+
entries = (0, logcat_parser_js_1.filterByGrep)(entries, params.grep);
|
|
80
|
+
}
|
|
81
|
+
// Deduplicate consecutive identical messages
|
|
82
|
+
let deduplicatedCount = 0;
|
|
83
|
+
if (shouldDeduplicate && entries.length > 0) {
|
|
84
|
+
const deduped = [];
|
|
85
|
+
let repeatCount = 1;
|
|
86
|
+
for (let i = 0; i < entries.length; i++) {
|
|
87
|
+
const next = entries[i + 1];
|
|
88
|
+
if (next && entries[i].message === next.message && entries[i].tag === next.tag) {
|
|
89
|
+
repeatCount++;
|
|
90
|
+
deduplicatedCount++;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
if (repeatCount > 1) {
|
|
94
|
+
deduped.push({
|
|
95
|
+
...entries[i],
|
|
96
|
+
message: `[repeated ${repeatCount}x] ${entries[i].message}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
deduped.push(entries[i]);
|
|
101
|
+
}
|
|
102
|
+
repeatCount = 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
entries = deduped;
|
|
106
|
+
}
|
|
107
|
+
// Limit results (take most recent)
|
|
108
|
+
const limited = entries.slice(-maxLines);
|
|
109
|
+
return {
|
|
110
|
+
content: [
|
|
111
|
+
{
|
|
112
|
+
type: "text",
|
|
113
|
+
text: JSON.stringify({
|
|
114
|
+
totalMatches: entries.length,
|
|
115
|
+
returned: limited.length,
|
|
116
|
+
truncated: entries.length > maxLines,
|
|
117
|
+
deduplicated: deduplicatedCount,
|
|
118
|
+
entries: limited,
|
|
119
|
+
}, null, 2),
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: `Error reading logcat: ${err instanceof Error ? err.message : String(err)}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
isError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
/**
|
|
137
|
+
* get_crash_info - Extract crash reports from logs.
|
|
138
|
+
*/
|
|
139
|
+
server.tool("get_crash_info", "Extract crash information from Android logs: JavaScript exceptions (React Native), native crashes (tombstones), and ANRs. Focuses on the target app.", {
|
|
140
|
+
since: zod_1.z.string().optional().describe("Only crashes since this time (e.g., '5m', '1h')"),
|
|
141
|
+
includeAllApps: zod_1.z.boolean().optional().describe("Include crashes from all apps, not just the target"),
|
|
142
|
+
}, async (params) => {
|
|
143
|
+
try {
|
|
144
|
+
const targetPackage = process.env.TARGET_PACKAGE ?? "com.emudebug.target";
|
|
145
|
+
// Get recent logcat
|
|
146
|
+
const cmdParts = ["logcat", "-d", "-v", "threadtime"];
|
|
147
|
+
if (params.since) {
|
|
148
|
+
cmdParts.push("-T", `'${params.since}'`);
|
|
149
|
+
}
|
|
150
|
+
const raw = await cm.adb.shell(cmdParts.join(" "), 15_000);
|
|
151
|
+
const allEntries = (0, logcat_parser_js_1.parseLogcat)(raw);
|
|
152
|
+
// Find target app PID(s) for filtering
|
|
153
|
+
let targetPids = new Set();
|
|
154
|
+
if (!params.includeAllApps) {
|
|
155
|
+
try {
|
|
156
|
+
const pidOutput = await cm.adb.shell(`pidof ${targetPackage}`);
|
|
157
|
+
pidOutput.trim().split(/\s+/).forEach((p) => targetPids.add(p));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// If no PID found, we'll still look for tagged entries
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Extract JS exceptions (React Native)
|
|
164
|
+
const jsErrors = allEntries.filter((e) => {
|
|
165
|
+
const isRnTag = RN_ERROR_TAGS.some((t) => e.tag.includes(t));
|
|
166
|
+
const isError = e.level === "E" || e.level === "F";
|
|
167
|
+
const isTargetPid = targetPids.size === 0 || targetPids.has(e.pid);
|
|
168
|
+
return isRnTag && isError && (params.includeAllApps || isTargetPid);
|
|
169
|
+
});
|
|
170
|
+
// Extract native crashes
|
|
171
|
+
const nativeCrashes = allEntries.filter((e) => {
|
|
172
|
+
const isCrashTag = CRASH_TAGS.some((t) => e.tag.includes(t));
|
|
173
|
+
const isError = e.level === "E" || e.level === "F";
|
|
174
|
+
return isCrashTag && isError;
|
|
175
|
+
});
|
|
176
|
+
// Extract ANRs
|
|
177
|
+
const anrs = allEntries.filter((e) => e.tag === "ActivityManager" && e.message.includes("ANR in"));
|
|
178
|
+
// Check for ANR traces file
|
|
179
|
+
let anrTraces = "";
|
|
180
|
+
if (anrs.length > 0) {
|
|
181
|
+
try {
|
|
182
|
+
anrTraces = await cm.adb.shell("cat /data/anr/traces.txt | head -100");
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
anrTraces = "(ANR traces not accessible without root)";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const result = {
|
|
189
|
+
summary: {
|
|
190
|
+
jsExceptions: jsErrors.length,
|
|
191
|
+
nativeCrashes: nativeCrashes.length,
|
|
192
|
+
anrs: anrs.length,
|
|
193
|
+
},
|
|
194
|
+
jsExceptions: jsErrors.map(logcat_parser_js_1.formatEntry),
|
|
195
|
+
nativeCrashes: nativeCrashes.map(logcat_parser_js_1.formatEntry),
|
|
196
|
+
anrs: anrs.map(logcat_parser_js_1.formatEntry),
|
|
197
|
+
anrTraces: anrTraces || undefined,
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
return {
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: "text",
|
|
208
|
+
text: `Error getting crash info: ${err instanceof Error ? err.message : String(err)}`,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
isError: true,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=logcat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/tools/network.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAG5E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,iBAAiB,GAAG,IAAI,CAiInF"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Network Tool - Inspect HTTP network traffic from the target app.
|
|
4
|
+
*
|
|
5
|
+
* Supports two modes:
|
|
6
|
+
* - "history": Retrieve past network requests (from agent buffer)
|
|
7
|
+
* - "capture": Start/stop live capture with filters
|
|
8
|
+
*
|
|
9
|
+
* Phase 1 fallback: parse logcat for OkHttp/Retrofit logging.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.registerNetworkTools = registerNetworkTools;
|
|
13
|
+
const zod_1 = require("zod");
|
|
14
|
+
const logcat_parser_js_1 = require("../util/logcat-parser.js");
|
|
15
|
+
function registerNetworkTools(server, cm) {
|
|
16
|
+
server.tool("get_network", "Inspect HTTP network traffic from the target app. In 'history' mode, returns recent requests. In 'capture' mode, starts/stops live capture. Supports URL and status code filtering.", {
|
|
17
|
+
mode: zod_1.z
|
|
18
|
+
.enum(["history", "capture_start", "capture_stop"])
|
|
19
|
+
.describe("history = past requests, capture_start = begin live capture, capture_stop = end and return captured"),
|
|
20
|
+
urlFilter: zod_1.z.string().optional().describe("Only include requests matching this URL pattern (substring)"),
|
|
21
|
+
statusFilter: zod_1.z
|
|
22
|
+
.number()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Only include responses with this HTTP status code"),
|
|
25
|
+
maxEntries: zod_1.z.number().optional().describe("Maximum entries to return (default: 50)"),
|
|
26
|
+
includeBody: zod_1.z.boolean().optional().describe("Include request/response bodies (default: false, can be large)"),
|
|
27
|
+
}, async (params) => {
|
|
28
|
+
const maxEntries = params.maxEntries ?? 50;
|
|
29
|
+
// Phase 2: full network interception via agent
|
|
30
|
+
if (cm.agent.isConnected()) {
|
|
31
|
+
try {
|
|
32
|
+
const result = await cm.agent.call("getNetwork", {
|
|
33
|
+
mode: params.mode,
|
|
34
|
+
urlFilter: params.urlFilter,
|
|
35
|
+
statusFilter: params.statusFilter,
|
|
36
|
+
maxEntries,
|
|
37
|
+
includeBody: params.includeBody ?? false,
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `Agent network error: ${err instanceof Error ? err.message : String(err)}. Falling back to logcat.`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
isError: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Phase 1: parse logcat for network-related logs
|
|
56
|
+
if (params.mode === "capture_start" || params.mode === "capture_stop") {
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: JSON.stringify({
|
|
62
|
+
error: "agent_not_connected",
|
|
63
|
+
message: "Live network capture requires the on-device agent (Phase 2). " +
|
|
64
|
+
"Use mode='history' to see network logs from logcat.",
|
|
65
|
+
}),
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
// Look for OkHttp, Retrofit, and generic HTTP logs
|
|
72
|
+
const raw = await cm.adb.shell("logcat -d -v threadtime", 15_000);
|
|
73
|
+
let entries = (0, logcat_parser_js_1.parseLogcat)(raw);
|
|
74
|
+
// Filter for network-related tags
|
|
75
|
+
const networkTags = ["OkHttp", "Retrofit", "HttpURLConnection", "NetworkClient", "API", "Volley"];
|
|
76
|
+
entries = entries.filter((e) => networkTags.some((tag) => e.tag.includes(tag)) ||
|
|
77
|
+
e.message.includes("HTTP") ||
|
|
78
|
+
e.message.includes("http://") ||
|
|
79
|
+
e.message.includes("https://"));
|
|
80
|
+
// Apply URL filter
|
|
81
|
+
if (params.urlFilter) {
|
|
82
|
+
entries = (0, logcat_parser_js_1.filterByGrep)(entries, params.urlFilter);
|
|
83
|
+
}
|
|
84
|
+
// Apply status filter by searching for the status code in the message
|
|
85
|
+
if (params.statusFilter) {
|
|
86
|
+
entries = entries.filter((e) => e.message.includes(String(params.statusFilter)));
|
|
87
|
+
}
|
|
88
|
+
const limited = entries.slice(-maxEntries);
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: "text",
|
|
93
|
+
text: JSON.stringify({
|
|
94
|
+
phase1Mode: true,
|
|
95
|
+
note: "Network data extracted from logcat. For full request/response inspection " +
|
|
96
|
+
"with headers and bodies, install the on-device agent (Phase 2). " +
|
|
97
|
+
"Ensure your app uses OkHttp logging interceptor for better logcat data.",
|
|
98
|
+
totalMatches: entries.length,
|
|
99
|
+
returned: limited.length,
|
|
100
|
+
entries: limited.map((e) => ({
|
|
101
|
+
timestamp: e.timestamp,
|
|
102
|
+
tag: e.tag,
|
|
103
|
+
message: e.message,
|
|
104
|
+
})),
|
|
105
|
+
}, null, 2),
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: `Error getting network data: ${err instanceof Error ? err.message : String(err)}`,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
isError: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=network.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance.d.ts","sourceRoot":"","sources":["../../src/tools/performance.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAgC5E,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,iBAAiB,GAAG,IAAI,CA8HvF"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Performance Tool - Monitor app performance metrics.
|
|
4
|
+
*
|
|
5
|
+
* Collects FPS, memory usage, CPU usage, and (via agent) React Native
|
|
6
|
+
* bridge metrics. Phase 1 uses ADB dumpsys; Phase 2 adds real-time
|
|
7
|
+
* frame timing and bridge throughput.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.registerPerformanceTools = registerPerformanceTools;
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
/** Parse `dumpsys meminfo <package>` output. */
|
|
13
|
+
function parseMeminfo(raw) {
|
|
14
|
+
const parseLine = (label) => {
|
|
15
|
+
const re = new RegExp(`${label}[:\\s]+(\\d+)`, "i");
|
|
16
|
+
const match = raw.match(re);
|
|
17
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
totalPss: parseLine("TOTAL PSS"),
|
|
21
|
+
javaHeap: parseLine("Java Heap"),
|
|
22
|
+
nativeHeap: parseLine("Native Heap"),
|
|
23
|
+
code: parseLine("Code"),
|
|
24
|
+
stack: parseLine("Stack"),
|
|
25
|
+
graphics: parseLine("Graphics"),
|
|
26
|
+
summary: raw.split("\n").slice(0, 20).join("\n"),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function registerPerformanceTools(server, cm) {
|
|
30
|
+
server.tool("get_performance", "Get performance metrics for the target app: memory usage, CPU, frame rate (FPS), and bridge metrics. Uses ADB dumpsys in Phase 1; on-device agent provides real-time FPS and bridge throughput in Phase 2.", {
|
|
31
|
+
metrics: zod_1.z
|
|
32
|
+
.array(zod_1.z.enum(["fps", "memory", "cpu", "bridge"]))
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Which metrics to collect (default: all)"),
|
|
35
|
+
durationSec: zod_1.z
|
|
36
|
+
.number()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Duration for FPS/CPU sampling in seconds (default: 2, max: 10)"),
|
|
39
|
+
}, async (params) => {
|
|
40
|
+
const metrics = params.metrics ?? ["fps", "memory", "cpu", "bridge"];
|
|
41
|
+
const targetPackage = process.env.TARGET_PACKAGE ?? "com.emudebug.target";
|
|
42
|
+
const durationSec = Math.min(params.durationSec ?? 2, 10);
|
|
43
|
+
// Phase 2: agent provides rich metrics
|
|
44
|
+
if (cm.agent.isConnected()) {
|
|
45
|
+
try {
|
|
46
|
+
const result = await cm.agent.call("getPerformance", {
|
|
47
|
+
metrics,
|
|
48
|
+
durationSec,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Fall through to ADB
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Phase 1: ADB-based metrics
|
|
59
|
+
const result = {};
|
|
60
|
+
// Memory via dumpsys
|
|
61
|
+
if (metrics.includes("memory")) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = await cm.adb.shell(`dumpsys meminfo ${targetPackage}`);
|
|
64
|
+
result.memory = parseMeminfo(raw);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
result.memory = {
|
|
68
|
+
error: `Failed to get memory info: ${err instanceof Error ? err.message : String(err)}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// CPU via top (single snapshot)
|
|
73
|
+
if (metrics.includes("cpu")) {
|
|
74
|
+
try {
|
|
75
|
+
const raw = await cm.adb.shell(`top -b -n 1 -d ${durationSec} | grep -i "${targetPackage}"`);
|
|
76
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
77
|
+
const cpuEntries = lines.map((line) => {
|
|
78
|
+
const parts = line.trim().split(/\s+/);
|
|
79
|
+
return {
|
|
80
|
+
pid: parts[0],
|
|
81
|
+
cpu: parts.length > 8 ? parts[8] : "?",
|
|
82
|
+
mem: parts.length > 9 ? parts[9] : "?",
|
|
83
|
+
raw: line.trim(),
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
result.cpu = { processes: cpuEntries };
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
result.cpu = {
|
|
90
|
+
error: `Failed to get CPU info: ${err instanceof Error ? err.message : String(err)}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// FPS via dumpsys gfxinfo
|
|
95
|
+
if (metrics.includes("fps")) {
|
|
96
|
+
try {
|
|
97
|
+
// Reset gfxinfo, wait, then read
|
|
98
|
+
await cm.adb.shell(`dumpsys gfxinfo ${targetPackage} reset`);
|
|
99
|
+
// Brief wait for frame data to accumulate
|
|
100
|
+
await new Promise((r) => setTimeout(r, durationSec * 1000));
|
|
101
|
+
const raw = await cm.adb.shell(`dumpsys gfxinfo ${targetPackage}`);
|
|
102
|
+
// Parse frame stats
|
|
103
|
+
const totalFramesMatch = raw.match(/Total frames rendered:\s*(\d+)/);
|
|
104
|
+
const jankyMatch = raw.match(/Janky frames:\s*(\d+)/);
|
|
105
|
+
const p50Match = raw.match(/50th percentile:\s*(\d+)ms/);
|
|
106
|
+
const p90Match = raw.match(/90th percentile:\s*(\d+)ms/);
|
|
107
|
+
const p95Match = raw.match(/95th percentile:\s*(\d+)ms/);
|
|
108
|
+
const p99Match = raw.match(/99th percentile:\s*(\d+)ms/);
|
|
109
|
+
const totalFrames = totalFramesMatch ? parseInt(totalFramesMatch[1], 10) : 0;
|
|
110
|
+
const jankyFrames = jankyMatch ? parseInt(jankyMatch[1], 10) : 0;
|
|
111
|
+
result.fps = {
|
|
112
|
+
totalFrames,
|
|
113
|
+
jankyFrames,
|
|
114
|
+
jankyPercentage: totalFrames > 0 ? ((jankyFrames / totalFrames) * 100).toFixed(1) + "%" : "N/A",
|
|
115
|
+
percentiles: {
|
|
116
|
+
p50: p50Match ? `${p50Match[1]}ms` : "N/A",
|
|
117
|
+
p90: p90Match ? `${p90Match[1]}ms` : "N/A",
|
|
118
|
+
p95: p95Match ? `${p95Match[1]}ms` : "N/A",
|
|
119
|
+
p99: p99Match ? `${p99Match[1]}ms` : "N/A",
|
|
120
|
+
},
|
|
121
|
+
sampleDurationSec: durationSec,
|
|
122
|
+
note: "For real-time FPS monitoring, install the on-device agent (Phase 2).",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
result.fps = {
|
|
127
|
+
error: `Failed to get FPS data: ${err instanceof Error ? err.message : String(err)}`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Bridge metrics require agent
|
|
132
|
+
if (metrics.includes("bridge")) {
|
|
133
|
+
result.bridge = {
|
|
134
|
+
note: "React Native bridge metrics require the on-device agent (Phase 2). " +
|
|
135
|
+
"Use get_rn_bridge for logcat-based bridge inspection.",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=performance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recording.d.ts","sourceRoot":"","sources":["../../src/tools/recording.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAS5E,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,iBAAiB,GAAG,IAAI,CAuGrF"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Recording Tools - Screen recording for Android devices.
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - start_recording: Begin screen recording
|
|
7
|
+
* - stop_recording: Stop and retrieve recording as base64 MP4
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.registerRecordingTools = registerRecordingTools;
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
const promises_1 = require("node:fs/promises");
|
|
13
|
+
const node_os_1 = require("node:os");
|
|
14
|
+
const node_path_1 = require("node:path");
|
|
15
|
+
/** Track active recording state. */
|
|
16
|
+
let recordingActive = false;
|
|
17
|
+
const DEVICE_RECORDING_PATH = "/sdcard/sensai_recording.mp4";
|
|
18
|
+
function registerRecordingTools(server, cm) {
|
|
19
|
+
/**
|
|
20
|
+
* start_recording - Begin screen recording on the Android device.
|
|
21
|
+
*/
|
|
22
|
+
server.tool("start_recording", "Start recording the Android device screen. Recording runs in the background. Use stop_recording to finish and retrieve the video. Max duration 180 seconds.", {
|
|
23
|
+
maxDurationSec: zod_1.z.number().optional().describe("Maximum recording duration in seconds (default: 60, max: 180)"),
|
|
24
|
+
}, async ({ maxDurationSec }) => {
|
|
25
|
+
if (recordingActive) {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Recording already in progress. Call stop_recording first." }) }],
|
|
28
|
+
isError: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const duration = Math.min(maxDurationSec ?? 60, 180);
|
|
32
|
+
try {
|
|
33
|
+
// Remove any leftover recording file
|
|
34
|
+
await cm.adb.shell(`rm -f ${DEVICE_RECORDING_PATH}`).catch(() => { });
|
|
35
|
+
// Start recording in background (screenrecord exits after duration or when killed)
|
|
36
|
+
// We run it detached so it doesn't block
|
|
37
|
+
cm.adb.shell(`screenrecord --time-limit ${duration} ${DEVICE_RECORDING_PATH}`).catch(() => {
|
|
38
|
+
// Recording finished or was killed — expected
|
|
39
|
+
recordingActive = false;
|
|
40
|
+
});
|
|
41
|
+
recordingActive = true;
|
|
42
|
+
return {
|
|
43
|
+
content: [{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: JSON.stringify({ ok: true, maxDurationSec: duration, path: DEVICE_RECORDING_PATH }),
|
|
46
|
+
}],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
recordingActive = false;
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: `start_recording failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* stop_recording - Stop the active recording and retrieve the video file.
|
|
59
|
+
*/
|
|
60
|
+
server.tool("stop_recording", "Stop the active screen recording and retrieve the MP4 video. Returns the video as base64 data, or saves to a local path if specified.", {
|
|
61
|
+
savePath: zod_1.z.string().optional().describe("Local path to save the MP4 file (optional). If not provided, returns base64."),
|
|
62
|
+
}, async ({ savePath }) => {
|
|
63
|
+
try {
|
|
64
|
+
// Kill screenrecord process (this causes it to finalize the MP4)
|
|
65
|
+
await cm.adb.shell("pkill -SIGINT screenrecord").catch(() => { });
|
|
66
|
+
// Wait for file to be finalized
|
|
67
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
68
|
+
recordingActive = false;
|
|
69
|
+
// Pull the file from device
|
|
70
|
+
const localPath = savePath ?? (0, node_path_1.join)((0, node_os_1.tmpdir)(), `sensai-recording-${Date.now()}.mp4`);
|
|
71
|
+
await cm.adb.pull(DEVICE_RECORDING_PATH, localPath);
|
|
72
|
+
// Clean up device file
|
|
73
|
+
await cm.adb.shell(`rm -f ${DEVICE_RECORDING_PATH}`).catch(() => { });
|
|
74
|
+
if (savePath) {
|
|
75
|
+
// User wants it saved to a path
|
|
76
|
+
return {
|
|
77
|
+
content: [{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: JSON.stringify({ ok: true, savedTo: localPath }),
|
|
80
|
+
}],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Return as base64
|
|
84
|
+
const data = await (0, promises_1.readFile)(localPath);
|
|
85
|
+
await (0, promises_1.unlink)(localPath).catch(() => { });
|
|
86
|
+
return {
|
|
87
|
+
content: [{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: JSON.stringify({ ok: true, format: "mp4", sizeBytes: data.length, base64: data.toString("base64") }),
|
|
90
|
+
}],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
recordingActive = false;
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: `stop_recording failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
97
|
+
isError: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=recording.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rn-tools.d.ts","sourceRoot":"","sources":["../../src/tools/rn-tools.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAoB5E,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,iBAAiB,GAAG,IAAI,CAgH9E"}
|