@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.
Files changed (89) hide show
  1. package/dist/android-adapter.d.ts.map +1 -0
  2. package/dist/android-adapter.js +89 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +12 -0
  5. package/dist/tools/accessibility.d.ts.map +1 -0
  6. package/dist/tools/accessibility.js +85 -0
  7. package/dist/tools/adb.d.ts.map +1 -0
  8. package/dist/tools/adb.js +66 -0
  9. package/dist/tools/app-state.d.ts.map +1 -0
  10. package/dist/tools/app-state.js +173 -0
  11. package/dist/tools/diagnose.d.ts.map +1 -0
  12. package/dist/tools/diagnose.js +128 -0
  13. package/dist/tools/hot-reload.d.ts.map +1 -0
  14. package/dist/tools/hot-reload.js +97 -0
  15. package/dist/tools/index.d.ts.map +1 -0
  16. package/dist/tools/index.js +66 -0
  17. package/dist/tools/interaction.d.ts.map +1 -0
  18. package/dist/tools/interaction.js +395 -0
  19. package/dist/tools/logcat.d.ts.map +1 -0
  20. package/dist/tools/logcat.js +216 -0
  21. package/dist/tools/network.d.ts.map +1 -0
  22. package/dist/tools/network.js +123 -0
  23. package/dist/tools/performance.d.ts.map +1 -0
  24. package/dist/tools/performance.js +143 -0
  25. package/dist/tools/recording.d.ts.map +1 -0
  26. package/dist/tools/recording.js +102 -0
  27. package/dist/tools/rn-tools.d.ts.map +1 -0
  28. package/dist/tools/rn-tools.js +120 -0
  29. package/dist/tools/smart-actions.d.ts.map +1 -0
  30. package/dist/tools/smart-actions.js +506 -0
  31. package/dist/tools/ui-tree.d.ts.map +1 -0
  32. package/dist/tools/ui-tree.js +226 -0
  33. package/dist/transport/adb-client.d.ts.map +1 -0
  34. package/dist/transport/adb-client.js +124 -0
  35. package/dist/transport/adb-client.test.d.ts.map +1 -0
  36. package/dist/transport/adb-client.test.js +153 -0
  37. package/dist/transport/agent-client.d.ts.map +1 -0
  38. package/dist/transport/agent-client.js +157 -0
  39. package/dist/transport/agent-client.test.d.ts.map +1 -0
  40. package/dist/transport/agent-client.test.js +199 -0
  41. package/dist/transport/connection-manager.d.ts.map +1 -0
  42. package/dist/transport/connection-manager.js +119 -0
  43. package/dist/util/logcat-parser.d.ts.map +1 -0
  44. package/dist/util/logcat-parser.js +79 -0
  45. package/dist/util/safety.d.ts.map +1 -0
  46. package/dist/util/safety.js +132 -0
  47. package/dist/util/safety.test.d.ts.map +1 -0
  48. package/dist/util/safety.test.js +205 -0
  49. package/dist/util/text-extractor.d.ts.map +1 -0
  50. package/dist/util/text-extractor.js +71 -0
  51. package/dist/util/ui-tree-cache.d.ts.map +1 -0
  52. package/dist/util/ui-tree-cache.js +46 -0
  53. package/dist/util/ui-tree-cache.test.d.ts.map +1 -0
  54. package/dist/util/ui-tree-cache.test.js +84 -0
  55. package/dist/util/ui-tree-parser.d.ts.map +1 -0
  56. package/dist/util/ui-tree-parser.js +123 -0
  57. package/dist/util/ui-tree-parser.test.d.ts.map +1 -0
  58. package/dist/util/ui-tree-parser.test.js +167 -0
  59. package/package.json +22 -0
  60. package/src/android-adapter.ts +124 -0
  61. package/src/index.ts +8 -0
  62. package/src/tools/accessibility.ts +94 -0
  63. package/src/tools/adb.ts +75 -0
  64. package/src/tools/app-state.ts +193 -0
  65. package/src/tools/diagnose.ts +146 -0
  66. package/src/tools/hot-reload.ts +103 -0
  67. package/src/tools/index.ts +66 -0
  68. package/src/tools/interaction.ts +448 -0
  69. package/src/tools/logcat.ts +252 -0
  70. package/src/tools/network.ts +145 -0
  71. package/src/tools/performance.ts +169 -0
  72. package/src/tools/recording.ts +123 -0
  73. package/src/tools/rn-tools.ts +143 -0
  74. package/src/tools/smart-actions.ts +593 -0
  75. package/src/tools/ui-tree.ts +258 -0
  76. package/src/transport/adb-client.test.ts +228 -0
  77. package/src/transport/adb-client.ts +139 -0
  78. package/src/transport/agent-client.test.ts +267 -0
  79. package/src/transport/agent-client.ts +188 -0
  80. package/src/transport/connection-manager.ts +140 -0
  81. package/src/util/logcat-parser.ts +94 -0
  82. package/src/util/safety.test.ts +251 -0
  83. package/src/util/safety.ts +143 -0
  84. package/src/util/text-extractor.ts +87 -0
  85. package/src/util/ui-tree-cache.test.ts +105 -0
  86. package/src/util/ui-tree-cache.ts +54 -0
  87. package/src/util/ui-tree-parser.test.ts +182 -0
  88. package/src/util/ui-tree-parser.ts +169 -0
  89. 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"}