@ceraph/react-native-mcp 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ejike Eze / IkeStudios
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # @ceraph/react-native-mcp
2
+
3
+ MCP server for React Native and Expo development. Automatic build error capture, console monitoring, reliable screen interactions, and prebuild detection.
4
+
5
+ Works with any MCP client: Claude Code, Cursor, Codex, Windsurf, and others.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js 18+
10
+ - macOS (for iOS Simulator/device support)
11
+ - [WebDriverAgent](https://github.com/appium/WebDriverAgent) running on `localhost:8100`
12
+ - [`mobile-mcp`](https://github.com/mobile-next/mobile-mcp) for screenshots, swipe, and device management
13
+ - Expo dev client or prebuilt app (Expo Go is not supported)
14
+
15
+ ## Quick Setup
16
+
17
+ Run this from your project root:
18
+
19
+ ```bash
20
+ npx @ceraph/react-native-mcp init
21
+ ```
22
+
23
+ This automatically:
24
+ - Configures MCP servers for all detected clients (Claude Code, Cursor, Codex, VS Code, Windsurf, Antigravity)
25
+ - Installs a Claude Code hook that injects runtime errors into your conversation automatically
26
+ - Adds `.rn-errors.json` to your `.gitignore`
27
+
28
+ ## Manual Setup
29
+
30
+ If you prefer to configure manually, follow the instructions for your client below.
31
+
32
+ ### Claude Code
33
+
34
+ Add to `.mcp.json` in your project root:
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "mobile-mcp": {
40
+ "command": "npx",
41
+ "args": ["-y", "@mobilenext/mobile-mcp@latest"]
42
+ },
43
+ "react-native-mcp": {
44
+ "command": "npx",
45
+ "args": ["-y", "@ceraph/react-native-mcp@latest"]
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### Cursor
52
+
53
+ Add to `.cursor/mcp.json` in your project root:
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "mobile-mcp": {
59
+ "command": "npx",
60
+ "args": ["-y", "@mobilenext/mobile-mcp@latest"]
61
+ },
62
+ "react-native-mcp": {
63
+ "command": "npx",
64
+ "args": ["-y", "@ceraph/react-native-mcp@latest"]
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### Codex
71
+
72
+ Add to `.codex/config.toml` in your project root:
73
+
74
+ ```toml
75
+ [mcp_servers.mobile-mcp]
76
+ command = "npx"
77
+ args = ["-y", "@mobilenext/mobile-mcp@latest"]
78
+
79
+ [mcp_servers.react-native-mcp]
80
+ command = "npx"
81
+ args = ["-y", "@ceraph/react-native-mcp@latest"]
82
+ ```
83
+
84
+ ### VS Code / Copilot
85
+
86
+ Add to `.vscode/mcp.json` in your project root:
87
+
88
+ ```json
89
+ {
90
+ "mcpServers": {
91
+ "mobile-mcp": {
92
+ "command": "npx",
93
+ "args": ["-y", "@mobilenext/mobile-mcp@latest"]
94
+ },
95
+ "react-native-mcp": {
96
+ "command": "npx",
97
+ "args": ["-y", "@ceraph/react-native-mcp@latest"]
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ### Windsurf
104
+
105
+ Add to `~/.codeium/windsurf/mcp_config.json` (or Settings → Advanced Settings → Cascade):
106
+
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "mobile-mcp": {
111
+ "command": "npx",
112
+ "args": ["-y", "@mobilenext/mobile-mcp@latest"]
113
+ },
114
+ "react-native-mcp": {
115
+ "command": "npx",
116
+ "args": ["-y", "@ceraph/react-native-mcp@latest"]
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### Antigravity
123
+
124
+ Add to `~/.gemini/antigravity/mcp_config.json` (or Manage MCP Servers → View raw config):
125
+
126
+ ```json
127
+ {
128
+ "mcpServers": {
129
+ "mobile-mcp": {
130
+ "command": "npx",
131
+ "args": ["-y", "@mobilenext/mobile-mcp@latest"]
132
+ },
133
+ "react-native-mcp": {
134
+ "command": "npx",
135
+ "args": ["-y", "@ceraph/react-native-mcp@latest"]
136
+ }
137
+ }
138
+ }
139
+ ```
140
+
141
+ ### Cline / Roo Code (VS Code extensions)
142
+
143
+ Open VS Code Settings → Extensions → Cline (or Roo Code) → MCP Servers, then add:
144
+
145
+ ```json
146
+ {
147
+ "mobile-mcp": {
148
+ "command": "npx",
149
+ "args": ["-y", "@mobilenext/mobile-mcp@latest"]
150
+ },
151
+ "react-native-mcp": {
152
+ "command": "npx",
153
+ "args": ["-y", "@ceraph/react-native-mcp@latest"]
154
+ }
155
+ }
156
+ ```
157
+
158
+ ### JetBrains IDEs
159
+
160
+ Settings → Tools → MCP Servers → Add, then enter:
161
+
162
+ - **Name:** `react-native-mcp`
163
+ - **Command:** `npx`
164
+ - **Args:** `-y @ceraph/react-native-mcp@latest`
165
+
166
+ Repeat for `mobile-mcp` with args `-y @mobilenext/mobile-mcp@latest`.
167
+
168
+ ## Tools
169
+
170
+ ### Build & Runtime
171
+
172
+ | Tool | Description |
173
+ |---|---|
174
+ | `rn_build_ios` | Build the app with `expo run:ios`. Captures Xcode output and returns structured errors (file, line, message, type). Optionally runs `prebuild --clean` first. |
175
+ | `rn_start` | Start Metro dev server. Monitors console output for runtime errors, JS exceptions, and red screens. |
176
+ | `rn_get_errors` | Return all captured build and runtime errors without re-running anything. |
177
+ | `rn_get_console` | Return recent Metro console output, filtered by log level. |
178
+ | `rn_check_prebuild` | Detect if `prebuild --clean` is needed by diffing `package.json`, `app.json`, and `Podfile.lock` against the last successful build. |
179
+ | `rn_stop` | Kill all managed React Native processes. |
180
+
181
+ ### Screen Interaction
182
+
183
+ | Tool | Description |
184
+ |---|---|
185
+ | `screen_tap` | Tap at coordinates with automatic pixel ratio correction. Screenshot coordinates are divided by the device pixel ratio (2x/3x) so taps land where you expect. |
186
+ | `screen_find_and_tap` | Find an element by text, accessibility label, or type, then tap its center. Most reliable interaction method — no coordinate guessing. |
187
+
188
+ ## Why both MCPs?
189
+
190
+ `mobile-mcp` handles low-level device interaction (screenshots, swipe, app lifecycle, recording) and is actively maintained for iOS/Android compatibility.
191
+
192
+ `@ceraph/react-native-mcp` adds the development workflow layer on top: structured error capture, console monitoring, pixel-ratio-corrected taps, and prebuild detection. Both talk to WebDriverAgent independently — they don't depend on each other, they complement each other.
193
+
194
+ ## License
195
+
196
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ceraph/react-native-mcp CLI entry point.
4
+ *
5
+ * Usage:
6
+ * npx @ceraph/react-native-mcp init — set up MCP config and error hook
7
+ * npx @ceraph/react-native-mcp — start the MCP server (stdio transport)
8
+ */
9
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ceraph/react-native-mcp CLI entry point.
4
+ *
5
+ * Usage:
6
+ * npx @ceraph/react-native-mcp init — set up MCP config and error hook
7
+ * npx @ceraph/react-native-mcp — start the MCP server (stdio transport)
8
+ */
9
+ const command = process.argv[2];
10
+ if (command === "init") {
11
+ await import("./init.js");
12
+ }
13
+ else {
14
+ await import("./index.js");
15
+ }
16
+ export {};
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Parses Xcode build errors, Metro bundler errors, and runtime errors
3
+ * from raw process output.
4
+ */
5
+ export interface BuildError {
6
+ file: string;
7
+ line: number;
8
+ column: number;
9
+ message: string;
10
+ severity: "error" | "warning" | "note";
11
+ type: "build" | "link" | "pod";
12
+ }
13
+ export interface RuntimeError {
14
+ message: string;
15
+ stack: string;
16
+ timestamp: string;
17
+ }
18
+ export interface Warning {
19
+ message: string;
20
+ source: string;
21
+ }
22
+ export interface AllErrors {
23
+ buildErrors: BuildError[];
24
+ runtimeErrors: RuntimeError[];
25
+ warnings: Warning[];
26
+ }
27
+ /**
28
+ * Parse all build output lines and extract structured errors.
29
+ */
30
+ export declare function parseBuildOutput(lines: string[]): {
31
+ errors: BuildError[];
32
+ warnings: Warning[];
33
+ buildFailed: boolean;
34
+ };
35
+ /**
36
+ * Stateful parser for Metro output. Multi-line errors (message + stack
37
+ * trace) arrive across multiple `onData` chunks, so the in-progress
38
+ * error must persist across calls. Use one instance per Metro session
39
+ * and call `parse(lines)` as chunks arrive; call `flush()` at the end
40
+ * (e.g. on process exit) to emit any error still being assembled.
41
+ */
42
+ export declare class MetroErrorParser {
43
+ private currentStack;
44
+ private currentErrorMessage;
45
+ private inStackTrace;
46
+ parse(lines: string[]): {
47
+ runtimeErrors: RuntimeError[];
48
+ warnings: Warning[];
49
+ };
50
+ /**
51
+ * Force-emit any error currently being assembled. Call on stream end.
52
+ */
53
+ flush(): {
54
+ runtimeErrors: RuntimeError[];
55
+ warnings: Warning[];
56
+ };
57
+ }
58
+ /**
59
+ * Parse a complete Metro buffer in one shot. Suitable for batch use
60
+ * (e.g. `getErrors()` re-parsing the rolling buffer); for streaming use
61
+ * a `MetroErrorParser` instance instead so multi-line state survives
62
+ * across chunks.
63
+ */
64
+ export declare function parseMetroOutput(lines: string[]): {
65
+ runtimeErrors: RuntimeError[];
66
+ warnings: Warning[];
67
+ };
68
+ /**
69
+ * Classify a log line by level.
70
+ */
71
+ export declare function classifyLogLevel(line: string): "error" | "warn" | "log";
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Parses Xcode build errors, Metro bundler errors, and runtime errors
3
+ * from raw process output.
4
+ */
5
+ // Xcode-style: /path/to/File.swift:12:5: error: something went wrong
6
+ const XCODE_ERROR_RE = /^(.+?):(\d+):(\d+):\s*(error|warning|note):\s*(.+)$/;
7
+ // Xcode build failure markers
8
+ const BUILD_FAILED_MARKERS = [
9
+ "Build Failed",
10
+ "** BUILD FAILED **",
11
+ "xcodebuild: error:",
12
+ "❌",
13
+ ];
14
+ // Linker errors
15
+ const LINKER_ERROR_RE = /^(ld|clang):\s*(error|warning):\s*(.+)$/;
16
+ const UNDEFINED_SYMBOL_RE = /^Undefined symbols? for architecture .+:\s*"(.+)"/;
17
+ // CocoaPods errors
18
+ const POD_ERROR_RE = /^\[!\]\s*(.+)$/;
19
+ // Metro bundler errors
20
+ const METRO_ERROR_RE = /^(Error|TypeError|ReferenceError|SyntaxError|RangeError):\s*(.+)$/;
21
+ const METRO_MODULE_RE = /Unable to resolve module ['"](.+?)['"]/;
22
+ const METRO_ENCOUNTERED_RE = /Metro has encountered an error/;
23
+ // Runtime errors from console
24
+ const CONSOLE_ERROR_RE = /^(ERROR|console\.error)\s*(.+)$/i;
25
+ const INVARIANT_RE = /Invariant Violation:\s*(.+)/;
26
+ const UNHANDLED_JS_RE = /Unhandled JS Exception:\s*(.+)/;
27
+ const RED_SCREEN_RE = /ExceptionsManager\.js/;
28
+ /**
29
+ * Parse a single line for Xcode build errors/warnings.
30
+ */
31
+ function parseXcodeLine(line) {
32
+ const match = line.match(XCODE_ERROR_RE);
33
+ if (match) {
34
+ return {
35
+ file: match[1],
36
+ line: parseInt(match[2], 10),
37
+ column: parseInt(match[3], 10),
38
+ message: match[5],
39
+ severity: match[4],
40
+ type: "build",
41
+ };
42
+ }
43
+ return null;
44
+ }
45
+ /**
46
+ * Parse a single line for linker errors.
47
+ */
48
+ function parseLinkerLine(line) {
49
+ const linkerMatch = line.match(LINKER_ERROR_RE);
50
+ if (linkerMatch) {
51
+ return {
52
+ file: "",
53
+ line: 0,
54
+ column: 0,
55
+ message: linkerMatch[3],
56
+ severity: linkerMatch[2],
57
+ type: "link",
58
+ };
59
+ }
60
+ const undefinedMatch = line.match(UNDEFINED_SYMBOL_RE);
61
+ if (undefinedMatch) {
62
+ return {
63
+ file: "",
64
+ line: 0,
65
+ column: 0,
66
+ message: `Undefined symbol: ${undefinedMatch[1]}`,
67
+ severity: "error",
68
+ type: "link",
69
+ };
70
+ }
71
+ return null;
72
+ }
73
+ /**
74
+ * Parse a single line for CocoaPods notices.
75
+ *
76
+ * CocoaPods uses the `[!]` prefix for both errors and warnings. Default
77
+ * to `error` and only downgrade when the message matches a known-safe
78
+ * pattern (target overrides, deprecation hints, CDN/repo update notices,
79
+ * advisory "should" / "recommends" copy). This is closed under unknown
80
+ * failure modes — a new pod error vocabulary stays classified as an error
81
+ * rather than silently becoming a warning that lets a broken build
82
+ * resolve as success.
83
+ */
84
+ const POD_SAFE_WARNING_RE = /\b(cdn|trunk|repo update|overrides the|setting defined in|deprecated|recommends|should)\b/i;
85
+ function parsePodLine(line) {
86
+ const match = line.match(POD_ERROR_RE);
87
+ if (!match)
88
+ return null;
89
+ const message = match[1];
90
+ const isSafeWarning = POD_SAFE_WARNING_RE.test(message);
91
+ return {
92
+ file: "Podfile",
93
+ line: 0,
94
+ column: 0,
95
+ message,
96
+ severity: isSafeWarning ? "warning" : "error",
97
+ type: "pod",
98
+ };
99
+ }
100
+ /**
101
+ * Parse all build output lines and extract structured errors.
102
+ */
103
+ export function parseBuildOutput(lines) {
104
+ const errors = [];
105
+ const warnings = [];
106
+ let buildFailed = false;
107
+ // Track multi-line error context: sometimes Xcode emits the error
108
+ // on one line and the source context on the next few lines.
109
+ // We append context to the last error's message.
110
+ let lastError = null;
111
+ let contextLines = 0;
112
+ const MAX_CONTEXT = 3;
113
+ for (const line of lines) {
114
+ const trimmed = line.trim();
115
+ if (!trimmed) {
116
+ lastError = null;
117
+ contextLines = 0;
118
+ continue;
119
+ }
120
+ // Check for build failure markers
121
+ if (BUILD_FAILED_MARKERS.some((m) => trimmed.includes(m))) {
122
+ buildFailed = true;
123
+ }
124
+ // Try Xcode error format
125
+ const xcodeError = parseXcodeLine(trimmed);
126
+ if (xcodeError) {
127
+ if (xcodeError.severity === "error") {
128
+ errors.push(xcodeError);
129
+ lastError = xcodeError;
130
+ contextLines = 0;
131
+ }
132
+ else {
133
+ warnings.push({
134
+ message: xcodeError.message,
135
+ source: `${xcodeError.file}:${xcodeError.line}:${xcodeError.column}`,
136
+ });
137
+ // Don't set lastError for warnings/notes
138
+ }
139
+ continue;
140
+ }
141
+ // Try linker error format
142
+ const linkerError = parseLinkerLine(trimmed);
143
+ if (linkerError) {
144
+ if (linkerError.severity === "warning") {
145
+ warnings.push({ message: linkerError.message, source: "linker" });
146
+ }
147
+ else {
148
+ errors.push(linkerError);
149
+ }
150
+ lastError = linkerError;
151
+ contextLines = 0;
152
+ continue;
153
+ }
154
+ // Try CocoaPods notice format (warning or error depending on message)
155
+ const podNotice = parsePodLine(trimmed);
156
+ if (podNotice) {
157
+ if (podNotice.severity === "warning") {
158
+ warnings.push({ message: podNotice.message, source: "CocoaPods" });
159
+ }
160
+ else {
161
+ errors.push(podNotice);
162
+ lastError = podNotice;
163
+ contextLines = 0;
164
+ }
165
+ continue;
166
+ }
167
+ // Append context lines to last error for multi-line messages
168
+ if (lastError && contextLines < MAX_CONTEXT) {
169
+ lastError.message += `\n${trimmed}`;
170
+ contextLines++;
171
+ }
172
+ }
173
+ return { errors, warnings, buildFailed };
174
+ }
175
+ /**
176
+ * Stateful parser for Metro output. Multi-line errors (message + stack
177
+ * trace) arrive across multiple `onData` chunks, so the in-progress
178
+ * error must persist across calls. Use one instance per Metro session
179
+ * and call `parse(lines)` as chunks arrive; call `flush()` at the end
180
+ * (e.g. on process exit) to emit any error still being assembled.
181
+ */
182
+ export class MetroErrorParser {
183
+ currentStack = [];
184
+ currentErrorMessage = "";
185
+ inStackTrace = false;
186
+ parse(lines) {
187
+ const runtimeErrors = [];
188
+ const warnings = [];
189
+ const flushInto = (sink) => {
190
+ if (this.currentErrorMessage) {
191
+ sink.push({
192
+ message: this.currentErrorMessage,
193
+ stack: this.currentStack.join("\n"),
194
+ timestamp: new Date().toISOString(),
195
+ });
196
+ this.currentErrorMessage = "";
197
+ this.currentStack = [];
198
+ this.inStackTrace = false;
199
+ }
200
+ };
201
+ for (const line of lines) {
202
+ const trimmed = line.trim();
203
+ // Metro module resolution errors — single-line, emit immediately.
204
+ const moduleMatch = trimmed.match(METRO_MODULE_RE);
205
+ if (moduleMatch) {
206
+ flushInto(runtimeErrors);
207
+ runtimeErrors.push({
208
+ message: `Unable to resolve module '${moduleMatch[1]}'`,
209
+ stack: trimmed,
210
+ timestamp: new Date().toISOString(),
211
+ });
212
+ continue;
213
+ }
214
+ // Metro encountered an error
215
+ if (METRO_ENCOUNTERED_RE.test(trimmed)) {
216
+ flushInto(runtimeErrors);
217
+ this.currentErrorMessage = "Metro has encountered an error";
218
+ this.inStackTrace = true;
219
+ continue;
220
+ }
221
+ // Invariant Violation
222
+ const invariantMatch = trimmed.match(INVARIANT_RE);
223
+ if (invariantMatch) {
224
+ flushInto(runtimeErrors);
225
+ this.currentErrorMessage = `Invariant Violation: ${invariantMatch[1]}`;
226
+ this.inStackTrace = true;
227
+ continue;
228
+ }
229
+ // Unhandled JS Exception
230
+ const unhandledMatch = trimmed.match(UNHANDLED_JS_RE);
231
+ if (unhandledMatch) {
232
+ flushInto(runtimeErrors);
233
+ this.currentErrorMessage = `Unhandled JS Exception: ${unhandledMatch[1]}`;
234
+ this.inStackTrace = true;
235
+ continue;
236
+ }
237
+ // JS error types
238
+ const metroMatch = trimmed.match(METRO_ERROR_RE);
239
+ if (metroMatch) {
240
+ flushInto(runtimeErrors);
241
+ this.currentErrorMessage = `${metroMatch[1]}: ${metroMatch[2]}`;
242
+ this.inStackTrace = true;
243
+ continue;
244
+ }
245
+ // Console errors
246
+ const consoleMatch = trimmed.match(CONSOLE_ERROR_RE);
247
+ if (consoleMatch) {
248
+ flushInto(runtimeErrors);
249
+ this.currentErrorMessage = consoleMatch[2];
250
+ this.inStackTrace = true;
251
+ continue;
252
+ }
253
+ // Red screen detection
254
+ if (RED_SCREEN_RE.test(trimmed)) {
255
+ if (!this.currentErrorMessage) {
256
+ this.currentErrorMessage = "Red screen error detected";
257
+ }
258
+ this.inStackTrace = true;
259
+ }
260
+ // Stack trace lines (indented, or starting with "at ")
261
+ if (this.inStackTrace) {
262
+ if (trimmed.startsWith("at ") ||
263
+ trimmed.startsWith("in ") ||
264
+ /^\s+at\s/.test(line) ||
265
+ /^\d+\s*\|/.test(trimmed) // source code context lines
266
+ ) {
267
+ this.currentStack.push(trimmed);
268
+ continue;
269
+ }
270
+ else if (trimmed === "") {
271
+ flushInto(runtimeErrors);
272
+ continue;
273
+ }
274
+ else {
275
+ // Continuation of the error message. Skip the warning check —
276
+ // a `warn ...` line interleaved with a stack trace would
277
+ // otherwise be double-counted as both stack and warning.
278
+ this.currentStack.push(trimmed);
279
+ continue;
280
+ }
281
+ }
282
+ // Warnings from Metro
283
+ if (/^warn\s/i.test(trimmed) || /\(WARN\)/.test(trimmed)) {
284
+ warnings.push({
285
+ message: trimmed.replace(/^warn\s*/i, "").replace(/\(WARN\)\s*/, ""),
286
+ source: "metro",
287
+ });
288
+ }
289
+ }
290
+ // Do NOT flush at end of chunk — the in-progress error may continue
291
+ // in the next chunk. Caller must invoke `flush()` when the stream ends.
292
+ return { runtimeErrors, warnings };
293
+ }
294
+ /**
295
+ * Force-emit any error currently being assembled. Call on stream end.
296
+ */
297
+ flush() {
298
+ const runtimeErrors = [];
299
+ if (this.currentErrorMessage) {
300
+ runtimeErrors.push({
301
+ message: this.currentErrorMessage,
302
+ stack: this.currentStack.join("\n"),
303
+ timestamp: new Date().toISOString(),
304
+ });
305
+ this.currentErrorMessage = "";
306
+ this.currentStack = [];
307
+ this.inStackTrace = false;
308
+ }
309
+ return { runtimeErrors, warnings: [] };
310
+ }
311
+ }
312
+ /**
313
+ * Parse a complete Metro buffer in one shot. Suitable for batch use
314
+ * (e.g. `getErrors()` re-parsing the rolling buffer); for streaming use
315
+ * a `MetroErrorParser` instance instead so multi-line state survives
316
+ * across chunks.
317
+ */
318
+ export function parseMetroOutput(lines) {
319
+ const parser = new MetroErrorParser();
320
+ const streamed = parser.parse(lines);
321
+ const flushed = parser.flush();
322
+ return {
323
+ runtimeErrors: [...streamed.runtimeErrors, ...flushed.runtimeErrors],
324
+ warnings: [...streamed.warnings, ...flushed.warnings],
325
+ };
326
+ }
327
+ /**
328
+ * Classify a log line by level.
329
+ */
330
+ export function classifyLogLevel(line) {
331
+ const trimmed = line.trim().toLowerCase();
332
+ if (trimmed.startsWith("error") ||
333
+ trimmed.includes("console.error") ||
334
+ trimmed.startsWith("❌") ||
335
+ /^\s*error\s*:/i.test(line)) {
336
+ return "error";
337
+ }
338
+ if (trimmed.startsWith("warn") ||
339
+ trimmed.includes("console.warn") ||
340
+ trimmed.includes("(warn)") ||
341
+ /^\s*warning\s*:/i.test(line)) {
342
+ return "warn";
343
+ }
344
+ return "log";
345
+ }