dev3000 0.0.49 → 0.0.51

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 (36) hide show
  1. package/README.md +55 -6
  2. package/dist/cdp-monitor.d.ts.map +1 -1
  3. package/dist/cdp-monitor.js +54 -48
  4. package/dist/cdp-monitor.js.map +1 -1
  5. package/dist/cli.js +39 -33
  6. package/dist/cli.js.map +1 -1
  7. package/dist/dev-environment.d.ts +2 -0
  8. package/dist/dev-environment.d.ts.map +1 -1
  9. package/dist/dev-environment.js +212 -181
  10. package/dist/dev-environment.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/mcp-server/app/api/config/route.ts +7 -7
  16. package/mcp-server/app/api/logs/append/route.ts +59 -51
  17. package/mcp-server/app/api/logs/head/route.ts +22 -22
  18. package/mcp-server/app/api/logs/list/route.ts +39 -42
  19. package/mcp-server/app/api/logs/rotate/route.ts +28 -38
  20. package/mcp-server/app/api/logs/stream/route.ts +35 -35
  21. package/mcp-server/app/api/logs/tail/route.ts +22 -22
  22. package/mcp-server/app/api/mcp/[transport]/route.ts +189 -188
  23. package/mcp-server/app/api/replay/route.ts +217 -202
  24. package/mcp-server/app/layout.tsx +9 -8
  25. package/mcp-server/app/logs/LogsClient.test.ts +123 -99
  26. package/mcp-server/app/logs/LogsClient.tsx +724 -562
  27. package/mcp-server/app/logs/page.tsx +71 -72
  28. package/mcp-server/app/logs/utils.ts +99 -28
  29. package/mcp-server/app/page.tsx +10 -14
  30. package/mcp-server/app/replay/ReplayClient.tsx +120 -119
  31. package/mcp-server/app/replay/page.tsx +3 -3
  32. package/mcp-server/next.config.ts +2 -0
  33. package/mcp-server/package.json +5 -2
  34. package/mcp-server/pnpm-lock.yaml +37 -5
  35. package/mcp-server/tsconfig.json +4 -17
  36. package/package.json +16 -13
@@ -1,169 +1,193 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { readFileSync, existsSync } from 'fs';
1
+ import { existsSync, readFileSync } from "fs"
2
+ import { type NextRequest, NextResponse } from "next/server"
3
3
 
4
4
  interface InteractionEvent {
5
- timestamp: string;
6
- type: 'CLICK' | 'TAP' | 'SCROLL' | 'KEY';
7
- x?: number;
8
- y?: number;
9
- target?: string;
10
- direction?: string;
11
- distance?: number;
12
- key?: string;
13
- url?: string;
5
+ timestamp: string
6
+ type: "CLICK" | "TAP" | "SCROLL" | "KEY"
7
+ x?: number
8
+ y?: number
9
+ target?: string
10
+ direction?: string
11
+ distance?: number
12
+ key?: string
13
+ url?: string
14
14
  }
15
15
 
16
16
  interface NavigationEvent {
17
- timestamp: string;
18
- url: string;
17
+ timestamp: string
18
+ url: string
19
19
  }
20
20
 
21
21
  interface ScreenshotEvent {
22
- timestamp: string;
23
- url: string;
24
- event: string;
22
+ timestamp: string
23
+ url: string
24
+ event: string
25
25
  }
26
26
 
27
27
  interface ReplayData {
28
- interactions: InteractionEvent[];
29
- navigations: NavigationEvent[];
30
- screenshots: ScreenshotEvent[];
31
- startTime: string;
32
- endTime: string;
33
- duration: number;
28
+ interactions: InteractionEvent[]
29
+ navigations: NavigationEvent[]
30
+ screenshots: ScreenshotEvent[]
31
+ startTime: string
32
+ endTime: string
33
+ duration: number
34
34
  }
35
35
 
36
36
  function parseLogFile(logContent: string, startTime?: string, endTime?: string): ReplayData {
37
- const lines = logContent.split('\n');
38
- const interactions: InteractionEvent[] = [];
39
- const navigations: NavigationEvent[] = [];
40
- const screenshots: ScreenshotEvent[] = [];
41
-
42
- let actualStartTime = '';
43
- let actualEndTime = '';
44
-
37
+ const lines = logContent.split("\n")
38
+ const interactions: InteractionEvent[] = []
39
+ const navigations: NavigationEvent[] = []
40
+ const screenshots: ScreenshotEvent[] = []
41
+
42
+ let actualStartTime = ""
43
+ let actualEndTime = ""
44
+
45
45
  for (const line of lines) {
46
- if (!line.trim()) continue;
47
-
48
- const timestampMatch = line.match(/^\[([^\]]+)\]/);
49
- if (!timestampMatch) continue;
50
-
51
- const timestamp = timestampMatch[1];
52
- const logTime = new Date(timestamp);
53
-
46
+ if (!line.trim()) continue
47
+
48
+ const timestampMatch = line.match(/^\[([^\]]+)\]/)
49
+ if (!timestampMatch) continue
50
+
51
+ const timestamp = timestampMatch[1]
52
+ const logTime = new Date(timestamp)
53
+
54
54
  // Filter by time range if specified
55
- if (startTime && logTime < new Date(startTime)) continue;
56
- if (endTime && logTime > new Date(endTime)) continue;
57
-
58
- if (!actualStartTime) actualStartTime = timestamp;
59
- actualEndTime = timestamp;
60
-
55
+ if (startTime && logTime < new Date(startTime)) continue
56
+ if (endTime && logTime > new Date(endTime)) continue
57
+
58
+ if (!actualStartTime) actualStartTime = timestamp
59
+ actualEndTime = timestamp
60
+
61
61
  // Parse interaction events (both old and new formats)
62
- const interactionMatch = line.match(/\[INTERACTION\] (.+)/);
62
+ const interactionMatch = line.match(/\[INTERACTION\] (.+)/)
63
63
  if (interactionMatch) {
64
- const data = interactionMatch[1];
65
-
64
+ const data = interactionMatch[1]
65
+
66
66
  try {
67
67
  // Try parsing as JSON (new format)
68
- const interactionData = JSON.parse(data);
69
-
70
- if (interactionData.type === 'CLICK' || interactionData.type === 'TAP') {
68
+ const interactionData = JSON.parse(data)
69
+
70
+ if (interactionData.type === "CLICK" || interactionData.type === "TAP") {
71
71
  interactions.push({
72
72
  timestamp,
73
- type: interactionData.type as 'CLICK' | 'TAP',
73
+ type: interactionData.type as "CLICK" | "TAP",
74
74
  x: interactionData.coordinates?.x || 0,
75
75
  y: interactionData.coordinates?.y || 0,
76
- target: interactionData.target || 'unknown'
77
- });
78
- } else if (interactionData.type === 'SCROLL') {
76
+ target: interactionData.target || "unknown"
77
+ })
78
+ } else if (interactionData.type === "SCROLL") {
79
79
  interactions.push({
80
80
  timestamp,
81
- type: 'SCROLL',
82
- direction: interactionData.direction || 'DOWN',
81
+ type: "SCROLL",
82
+ direction: interactionData.direction || "DOWN",
83
83
  distance: interactionData.distance || 0,
84
84
  x: interactionData.to?.x || 0,
85
85
  y: interactionData.to?.y || 0
86
- });
87
- } else if (interactionData.type === 'KEY') {
86
+ })
87
+ } else if (interactionData.type === "KEY") {
88
88
  interactions.push({
89
89
  timestamp,
90
- type: 'KEY',
91
- key: interactionData.key || 'unknown',
92
- target: interactionData.target || 'unknown'
93
- });
90
+ type: "KEY",
91
+ key: interactionData.key || "unknown",
92
+ target: interactionData.target || "unknown"
93
+ })
94
94
  }
95
- } catch (jsonError) {
95
+ } catch (_jsonError) {
96
96
  // Fallback to old format parsing
97
- const oldFormatMatch = data.match(/(CLICK|TAP|SCROLL|KEY) (.+)/);
97
+ const oldFormatMatch = data.match(/(CLICK|TAP|SCROLL|KEY) (.+)/)
98
98
  if (oldFormatMatch) {
99
- const [, type, details] = oldFormatMatch;
100
-
101
- if (type === 'CLICK' || type === 'TAP') {
102
- const coordMatch = details.match(/at \((\d+), (\d+)\) on (.+)/);
99
+ const [, type, details] = oldFormatMatch
100
+
101
+ if (type === "CLICK" || type === "TAP") {
102
+ // Match both old and new CLICK formats:
103
+ // Old: "at (286, 303) on target"
104
+ // New: "at 286,303 on {"selector":"...","tag":"..."}"
105
+ const coordMatch = details.match(/at (?:\((\d+),\s*(\d+)\)|(\d+),(\d+)) on (.+)/)
103
106
  if (coordMatch) {
107
+ const x = parseInt(coordMatch[1] || coordMatch[3], 10)
108
+ const y = parseInt(coordMatch[2] || coordMatch[4], 10)
109
+ const targetData = coordMatch[5]
110
+
111
+ // Try to parse target as JSON, fallback to string
112
+ let target = targetData
113
+ try {
114
+ const parsedTarget = JSON.parse(targetData)
115
+ target = parsedTarget.selector || parsedTarget.tag || targetData
116
+ } catch {
117
+ // Use as string if not JSON
118
+ }
119
+
104
120
  interactions.push({
105
121
  timestamp,
106
- type: type as 'CLICK' | 'TAP',
107
- x: parseInt(coordMatch[1]),
108
- y: parseInt(coordMatch[2]),
109
- target: coordMatch[3]
110
- });
122
+ type: type as "CLICK" | "TAP",
123
+ x,
124
+ y,
125
+ target
126
+ })
111
127
  }
112
- } else if (type === 'SCROLL') {
113
- const scrollMatch = details.match(/(\w+) (\d+)px to \((\d+), (\d+)\)/);
128
+ } else if (type === "SCROLL") {
129
+ // Match new SCROLL format: "from 0,599.5 to 0,0 in document"
130
+ const scrollMatch = details.match(/from ([\d.]+),([\d.]+) to ([\d.]+),([\d.]+) in (.+)/)
114
131
  if (scrollMatch) {
132
+ const _fromX = parseFloat(scrollMatch[1])
133
+ const fromY = parseFloat(scrollMatch[2])
134
+ const toX = parseFloat(scrollMatch[3])
135
+ const toY = parseFloat(scrollMatch[4])
136
+ const target = scrollMatch[5]
137
+
115
138
  interactions.push({
116
139
  timestamp,
117
- type: 'SCROLL',
118
- direction: scrollMatch[1],
119
- distance: parseInt(scrollMatch[2]),
120
- x: parseInt(scrollMatch[3]),
121
- y: parseInt(scrollMatch[4])
122
- });
140
+ type: "SCROLL",
141
+ x: toX,
142
+ y: toY,
143
+ direction: toY > fromY ? "DOWN" : "UP",
144
+ distance: Math.abs(toY - fromY),
145
+ target
146
+ })
123
147
  }
124
- } else if (type === 'KEY') {
125
- const keyMatch = details.match(/(.+) in (.+)/);
148
+ } else if (type === "KEY") {
149
+ const keyMatch = details.match(/(.+) in (.+)/)
126
150
  if (keyMatch) {
127
151
  interactions.push({
128
152
  timestamp,
129
- type: 'KEY',
153
+ type: "KEY",
130
154
  key: keyMatch[1],
131
155
  target: keyMatch[2]
132
- });
156
+ })
133
157
  }
134
158
  }
135
159
  }
136
160
  }
137
161
  }
138
-
162
+
139
163
  // Parse navigation events
140
- const navigationMatch = line.match(/\[NAVIGATION\] (.+)/);
164
+ const navigationMatch = line.match(/\[NAVIGATION\] (.+)/)
141
165
  if (navigationMatch) {
142
166
  navigations.push({
143
167
  timestamp,
144
168
  url: navigationMatch[1]
145
- });
169
+ })
146
170
  }
147
-
171
+
148
172
  // Parse screenshot events
149
- const screenshotMatch = line.match(/\[SCREENSHOT\] (.+)/);
173
+ const screenshotMatch = line.match(/\[SCREENSHOT\] (.+)/)
150
174
  if (screenshotMatch) {
151
- const urlParts = screenshotMatch[1].split('/');
152
- const filename = urlParts[urlParts.length - 1];
153
- const eventType = filename.split('-').slice(3).join('-').replace('.png', '');
154
-
175
+ const urlParts = screenshotMatch[1].split("/")
176
+ const filename = urlParts[urlParts.length - 1]
177
+ const eventType = filename.split("-").slice(3).join("-").replace(".png", "")
178
+
155
179
  screenshots.push({
156
180
  timestamp,
157
181
  url: screenshotMatch[1],
158
182
  event: eventType
159
- });
183
+ })
160
184
  }
161
185
  }
162
-
163
- const startTimeMs = new Date(actualStartTime).getTime();
164
- const endTimeMs = new Date(actualEndTime).getTime();
165
- const duration = endTimeMs - startTimeMs;
166
-
186
+
187
+ const startTimeMs = new Date(actualStartTime).getTime()
188
+ const endTimeMs = new Date(actualEndTime).getTime()
189
+ const duration = endTimeMs - startTimeMs
190
+
167
191
  return {
168
192
  interactions,
169
193
  navigations,
@@ -171,155 +195,148 @@ function parseLogFile(logContent: string, startTime?: string, endTime?: string):
171
195
  startTime: actualStartTime,
172
196
  endTime: actualEndTime,
173
197
  duration
174
- };
198
+ }
175
199
  }
176
200
 
177
201
  export async function GET(request: NextRequest) {
178
202
  try {
179
- const { searchParams } = new URL(request.url);
180
- const action = searchParams.get('action');
181
- const startTime = searchParams.get('startTime');
182
- const endTime = searchParams.get('endTime');
183
-
203
+ const { searchParams } = new URL(request.url)
204
+ const action = searchParams.get("action")
205
+ const startTime = searchParams.get("startTime")
206
+ const endTime = searchParams.get("endTime")
207
+
184
208
  // Get log file path from environment
185
- const logFilePath = process.env.LOG_FILE_PATH || '/tmp/dev3000.log';
186
-
209
+ const logFilePath = process.env.LOG_FILE_PATH || "/tmp/dev3000.log"
210
+
187
211
  if (!existsSync(logFilePath)) {
188
- return NextResponse.json({ error: 'Log file not found' }, { status: 404 });
212
+ return NextResponse.json({ error: "Log file not found" }, { status: 404 })
189
213
  }
190
-
191
- const logContent = readFileSync(logFilePath, 'utf8');
192
-
193
- if (action === 'parse') {
214
+
215
+ const logContent = readFileSync(logFilePath, "utf8")
216
+
217
+ if (action === "parse") {
194
218
  // Parse the log file and return replay data
195
- const replayData = parseLogFile(logContent, startTime || undefined, endTime || undefined);
196
- return NextResponse.json(replayData);
219
+ const replayData = parseLogFile(logContent, startTime || undefined, endTime || undefined)
220
+ return NextResponse.json(replayData)
197
221
  }
198
-
199
- return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
222
+
223
+ return NextResponse.json({ error: "Invalid action" }, { status: 400 })
200
224
  } catch (error) {
201
- console.error('Replay API error:', error);
202
- return NextResponse.json(
203
- { error: 'Failed to process replay request' },
204
- { status: 500 }
205
- );
225
+ console.error("Replay API error:", error)
226
+ return NextResponse.json({ error: "Failed to process replay request" }, { status: 500 })
206
227
  }
207
228
  }
208
229
 
209
230
  export async function POST(request: NextRequest) {
210
231
  try {
211
- const body = await request.json();
212
- const { action, replayData, speed = 1 } = body;
213
-
214
- if (action === 'execute') {
232
+ const body = await request.json()
233
+ const { action, replayData, speed = 1 } = body
234
+
235
+ if (action === "execute") {
215
236
  // Execute replay via MCP server's execute_browser_action tool
216
237
  try {
217
- const result = await executeBrowserActions(replayData, speed);
238
+ const result = await executeBrowserActions(replayData, speed)
218
239
  return NextResponse.json({
219
240
  success: true,
220
- message: 'Replay executed successfully via MCP server',
241
+ message: "Replay executed successfully via MCP server",
221
242
  result: result,
222
243
  totalEvents: result.totalEvents,
223
244
  executedEvents: result.executed
224
- });
245
+ })
225
246
  } catch (error) {
226
247
  return NextResponse.json({
227
248
  success: false,
228
- message: 'MCP server execution failed',
229
- error: error instanceof Error ? error.message : 'Unknown error'
230
- });
249
+ message: "MCP server execution failed",
250
+ error: error instanceof Error ? error.message : "Unknown error"
251
+ })
231
252
  }
232
253
  }
233
-
234
- return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
254
+
255
+ return NextResponse.json({ error: "Invalid action" }, { status: 400 })
235
256
  } catch (error) {
236
- console.error('Replay execution error:', error);
237
- return NextResponse.json(
238
- { error: 'Failed to execute replay' },
239
- { status: 500 }
240
- );
257
+ console.error("Replay execution error:", error)
258
+ return NextResponse.json({ error: "Failed to execute replay" }, { status: 500 })
241
259
  }
242
260
  }
243
261
 
244
-
245
262
  async function executeBrowserActions(replayData: ReplayData, speed: number): Promise<any> {
246
263
  try {
247
264
  // Get MCP server URL from environment (defaults to local MCP server)
248
- const mcpServerUrl = process.env.MCP_SERVER_URL || 'http://localhost:3684';
249
-
265
+ const mcpServerUrl = process.env.MCP_SERVER_URL || "http://localhost:3684"
266
+
250
267
  const events = [
251
- ...replayData.interactions.map(i => ({ ...i, eventType: 'interaction' })),
252
- ...replayData.navigations.map(n => ({ ...n, eventType: 'navigation' }))
253
- ].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
254
-
255
- const results: any[] = [];
256
- const startTime = new Date(replayData.startTime).getTime();
257
-
268
+ ...replayData.interactions.map((i) => ({ ...i, eventType: "interaction" })),
269
+ ...replayData.navigations.map((n) => ({ ...n, eventType: "navigation" }))
270
+ ].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
271
+
272
+ const results: any[] = []
273
+ const startTime = new Date(replayData.startTime).getTime()
274
+
258
275
  // Execute events sequentially with proper timing
259
276
  for (let i = 0; i < events.length; i++) {
260
- const event = events[i];
261
- const eventTime = new Date(event.timestamp).getTime();
262
- const delay = Math.max(0, (eventTime - startTime) / speed);
263
-
277
+ const event = events[i]
278
+ const eventTime = new Date(event.timestamp).getTime()
279
+ const delay = Math.max(0, (eventTime - startTime) / speed)
280
+
264
281
  // Wait for the calculated delay
265
282
  if (delay > 0) {
266
- await new Promise(resolve => setTimeout(resolve, delay));
283
+ await new Promise((resolve) => setTimeout(resolve, delay))
267
284
  }
268
-
285
+
269
286
  try {
270
- let response;
271
-
272
- if (event.eventType === 'navigation') {
287
+ let response
288
+
289
+ if (event.eventType === "navigation") {
273
290
  // Navigate to URL
274
291
  response = await fetch(`${mcpServerUrl}/mcp`, {
275
- method: 'POST',
276
- headers: { 'Content-Type': 'application/json' },
292
+ method: "POST",
293
+ headers: { "Content-Type": "application/json" },
277
294
  body: JSON.stringify({
278
- jsonrpc: '2.0',
295
+ jsonrpc: "2.0",
279
296
  id: i + 1,
280
- method: 'tools/call',
297
+ method: "tools/call",
281
298
  params: {
282
- name: 'execute_browser_action',
299
+ name: "execute_browser_action",
283
300
  arguments: {
284
- action: 'navigate',
301
+ action: "navigate",
285
302
  url: event.url
286
303
  }
287
304
  }
288
305
  })
289
- });
290
- } else if (event.eventType === 'interaction') {
291
- if ('type' in event && event.type === 'CLICK' && event.x !== undefined && event.y !== undefined) {
306
+ })
307
+ } else if (event.eventType === "interaction") {
308
+ if ("type" in event && event.type === "CLICK" && event.x !== undefined && event.y !== undefined) {
292
309
  // Click action
293
310
  response = await fetch(`${mcpServerUrl}/mcp`, {
294
- method: 'POST',
295
- headers: { 'Content-Type': 'application/json' },
311
+ method: "POST",
312
+ headers: { "Content-Type": "application/json" },
296
313
  body: JSON.stringify({
297
- jsonrpc: '2.0',
314
+ jsonrpc: "2.0",
298
315
  id: i + 1,
299
- method: 'tools/call',
316
+ method: "tools/call",
300
317
  params: {
301
- name: 'execute_browser_action',
318
+ name: "execute_browser_action",
302
319
  arguments: {
303
- action: 'click',
320
+ action: "click",
304
321
  x: event.x,
305
322
  y: event.y
306
323
  }
307
324
  }
308
325
  })
309
- });
310
- } else if ('type' in event && event.type === 'SCROLL' && event.x !== undefined && event.y !== undefined) {
326
+ })
327
+ } else if ("type" in event && event.type === "SCROLL" && event.x !== undefined && event.y !== undefined) {
311
328
  // Scroll action
312
329
  response = await fetch(`${mcpServerUrl}/mcp`, {
313
- method: 'POST',
314
- headers: { 'Content-Type': 'application/json' },
330
+ method: "POST",
331
+ headers: { "Content-Type": "application/json" },
315
332
  body: JSON.stringify({
316
- jsonrpc: '2.0',
333
+ jsonrpc: "2.0",
317
334
  id: i + 1,
318
- method: 'tools/call',
335
+ method: "tools/call",
319
336
  params: {
320
- name: 'execute_browser_action',
337
+ name: "execute_browser_action",
321
338
  arguments: {
322
- action: 'scroll',
339
+ action: "scroll",
323
340
  x: 0,
324
341
  y: 0,
325
342
  deltaX: event.x,
@@ -327,53 +344,51 @@ async function executeBrowserActions(replayData: ReplayData, speed: number): Pro
327
344
  }
328
345
  }
329
346
  })
330
- });
331
- } else if ('type' in event && event.type === 'KEY' && event.key) {
347
+ })
348
+ } else if ("type" in event && event.type === "KEY" && event.key) {
332
349
  // Type action
333
350
  response = await fetch(`${mcpServerUrl}/mcp`, {
334
- method: 'POST',
335
- headers: { 'Content-Type': 'application/json' },
351
+ method: "POST",
352
+ headers: { "Content-Type": "application/json" },
336
353
  body: JSON.stringify({
337
- jsonrpc: '2.0',
354
+ jsonrpc: "2.0",
338
355
  id: i + 1,
339
- method: 'tools/call',
356
+ method: "tools/call",
340
357
  params: {
341
- name: 'execute_browser_action',
358
+ name: "execute_browser_action",
342
359
  arguments: {
343
- action: 'type',
360
+ action: "type",
344
361
  text: event.key
345
362
  }
346
363
  }
347
364
  })
348
- });
365
+ })
349
366
  }
350
367
  }
351
-
368
+
352
369
  if (response) {
353
- const result = await response.json();
370
+ const result = await response.json()
354
371
  results.push({
355
372
  event,
356
373
  result,
357
- description: `${event.eventType}: ${event.eventType === 'navigation' ? event.url : ('type' in event ? event.type : 'unknown')}`
358
- });
374
+ description: `${event.eventType}: ${event.eventType === "navigation" ? event.url : "type" in event ? event.type : "unknown"}`
375
+ })
359
376
  }
360
-
361
377
  } catch (error) {
362
378
  results.push({
363
379
  event,
364
- error: error instanceof Error ? error.message : 'Unknown error',
380
+ error: error instanceof Error ? error.message : "Unknown error",
365
381
  description: `Failed: ${event.eventType}`
366
- });
382
+ })
367
383
  }
368
384
  }
369
-
385
+
370
386
  return {
371
387
  executed: results.length,
372
388
  results,
373
389
  totalEvents: events.length
374
- };
375
-
390
+ }
376
391
  } catch (error) {
377
- throw new Error(`Failed to execute replay via MCP server: ${error}`);
392
+ throw new Error(`Failed to execute replay via MCP server: ${error}`)
378
393
  }
379
- }
394
+ }
@@ -1,12 +1,13 @@
1
- import React from 'react';
2
-
3
- export default function RootLayout({
4
- children,
5
- }: any) {
1
+ export default function RootLayout({ children }: any) {
6
2
  return (
7
3
  <html lang="en" className="h-full">
8
4
  <head>
9
5
  <title>🎯 dev3000</title>
6
+ <link rel="icon" href="/favicon.ico" sizes="32x32" />
7
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
8
+ <link rel="icon" href="/favicon-16.svg" type="image/svg+xml" sizes="16x16" />
9
+ <link rel="apple-touch-icon" href="/favicon-180.png" />
10
+ <meta name="theme-color" content="#1f2937" />
10
11
  <script src="https://cdn.tailwindcss.com"></script>
11
12
  <script
12
13
  dangerouslySetInnerHTML={{
@@ -14,11 +15,11 @@ export default function RootLayout({
14
15
  tailwind.config = {
15
16
  darkMode: 'class',
16
17
  }
17
- `,
18
+ `
18
19
  }}
19
20
  />
20
21
  </head>
21
22
  <body className="h-full">{children}</body>
22
23
  </html>
23
- );
24
- }
24
+ )
25
+ }