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.
- package/README.md +55 -6
- package/dist/cdp-monitor.d.ts.map +1 -1
- package/dist/cdp-monitor.js +54 -48
- package/dist/cdp-monitor.js.map +1 -1
- package/dist/cli.js +39 -33
- package/dist/cli.js.map +1 -1
- package/dist/dev-environment.d.ts +2 -0
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +212 -181
- package/dist/dev-environment.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/mcp-server/app/api/config/route.ts +7 -7
- package/mcp-server/app/api/logs/append/route.ts +59 -51
- package/mcp-server/app/api/logs/head/route.ts +22 -22
- package/mcp-server/app/api/logs/list/route.ts +39 -42
- package/mcp-server/app/api/logs/rotate/route.ts +28 -38
- package/mcp-server/app/api/logs/stream/route.ts +35 -35
- package/mcp-server/app/api/logs/tail/route.ts +22 -22
- package/mcp-server/app/api/mcp/[transport]/route.ts +189 -188
- package/mcp-server/app/api/replay/route.ts +217 -202
- package/mcp-server/app/layout.tsx +9 -8
- package/mcp-server/app/logs/LogsClient.test.ts +123 -99
- package/mcp-server/app/logs/LogsClient.tsx +724 -562
- package/mcp-server/app/logs/page.tsx +71 -72
- package/mcp-server/app/logs/utils.ts +99 -28
- package/mcp-server/app/page.tsx +10 -14
- package/mcp-server/app/replay/ReplayClient.tsx +120 -119
- package/mcp-server/app/replay/page.tsx +3 -3
- package/mcp-server/next.config.ts +2 -0
- package/mcp-server/package.json +5 -2
- package/mcp-server/pnpm-lock.yaml +37 -5
- package/mcp-server/tsconfig.json +4 -17
- package/package.json +16 -13
|
@@ -1,169 +1,193 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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:
|
|
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(
|
|
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 ===
|
|
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
|
|
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 ||
|
|
77
|
-
})
|
|
78
|
-
} else if (interactionData.type ===
|
|
76
|
+
target: interactionData.target || "unknown"
|
|
77
|
+
})
|
|
78
|
+
} else if (interactionData.type === "SCROLL") {
|
|
79
79
|
interactions.push({
|
|
80
80
|
timestamp,
|
|
81
|
-
type:
|
|
82
|
-
direction: interactionData.direction ||
|
|
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 ===
|
|
86
|
+
})
|
|
87
|
+
} else if (interactionData.type === "KEY") {
|
|
88
88
|
interactions.push({
|
|
89
89
|
timestamp,
|
|
90
|
-
type:
|
|
91
|
-
key: interactionData.key ||
|
|
92
|
-
target: interactionData.target ||
|
|
93
|
-
})
|
|
90
|
+
type: "KEY",
|
|
91
|
+
key: interactionData.key || "unknown",
|
|
92
|
+
target: interactionData.target || "unknown"
|
|
93
|
+
})
|
|
94
94
|
}
|
|
95
|
-
} catch (
|
|
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 ===
|
|
102
|
-
|
|
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
|
|
107
|
-
x
|
|
108
|
-
y
|
|
109
|
-
target
|
|
110
|
-
})
|
|
122
|
+
type: type as "CLICK" | "TAP",
|
|
123
|
+
x,
|
|
124
|
+
y,
|
|
125
|
+
target
|
|
126
|
+
})
|
|
111
127
|
}
|
|
112
|
-
} else if (type ===
|
|
113
|
-
|
|
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:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 ===
|
|
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:
|
|
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(
|
|
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(
|
|
181
|
-
const startTime = searchParams.get(
|
|
182
|
-
const endTime = searchParams.get(
|
|
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 ||
|
|
186
|
-
|
|
209
|
+
const logFilePath = process.env.LOG_FILE_PATH || "/tmp/dev3000.log"
|
|
210
|
+
|
|
187
211
|
if (!existsSync(logFilePath)) {
|
|
188
|
-
return NextResponse.json({ error:
|
|
212
|
+
return NextResponse.json({ error: "Log file not found" }, { status: 404 })
|
|
189
213
|
}
|
|
190
|
-
|
|
191
|
-
const logContent = readFileSync(logFilePath,
|
|
192
|
-
|
|
193
|
-
if (action ===
|
|
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:
|
|
222
|
+
|
|
223
|
+
return NextResponse.json({ error: "Invalid action" }, { status: 400 })
|
|
200
224
|
} catch (error) {
|
|
201
|
-
console.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 ===
|
|
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:
|
|
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:
|
|
229
|
-
error: error instanceof Error ? error.message :
|
|
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:
|
|
254
|
+
|
|
255
|
+
return NextResponse.json({ error: "Invalid action" }, { status: 400 })
|
|
235
256
|
} catch (error) {
|
|
236
|
-
console.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 ||
|
|
249
|
-
|
|
265
|
+
const mcpServerUrl = process.env.MCP_SERVER_URL || "http://localhost:3684"
|
|
266
|
+
|
|
250
267
|
const events = [
|
|
251
|
-
...replayData.interactions.map(i => ({ ...i, eventType:
|
|
252
|
-
...replayData.navigations.map(n => ({ ...n, eventType:
|
|
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 ===
|
|
287
|
+
let response
|
|
288
|
+
|
|
289
|
+
if (event.eventType === "navigation") {
|
|
273
290
|
// Navigate to URL
|
|
274
291
|
response = await fetch(`${mcpServerUrl}/mcp`, {
|
|
275
|
-
method:
|
|
276
|
-
headers: {
|
|
292
|
+
method: "POST",
|
|
293
|
+
headers: { "Content-Type": "application/json" },
|
|
277
294
|
body: JSON.stringify({
|
|
278
|
-
jsonrpc:
|
|
295
|
+
jsonrpc: "2.0",
|
|
279
296
|
id: i + 1,
|
|
280
|
-
method:
|
|
297
|
+
method: "tools/call",
|
|
281
298
|
params: {
|
|
282
|
-
name:
|
|
299
|
+
name: "execute_browser_action",
|
|
283
300
|
arguments: {
|
|
284
|
-
action:
|
|
301
|
+
action: "navigate",
|
|
285
302
|
url: event.url
|
|
286
303
|
}
|
|
287
304
|
}
|
|
288
305
|
})
|
|
289
|
-
})
|
|
290
|
-
} else if (event.eventType ===
|
|
291
|
-
if (
|
|
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:
|
|
295
|
-
headers: {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: { "Content-Type": "application/json" },
|
|
296
313
|
body: JSON.stringify({
|
|
297
|
-
jsonrpc:
|
|
314
|
+
jsonrpc: "2.0",
|
|
298
315
|
id: i + 1,
|
|
299
|
-
method:
|
|
316
|
+
method: "tools/call",
|
|
300
317
|
params: {
|
|
301
|
-
name:
|
|
318
|
+
name: "execute_browser_action",
|
|
302
319
|
arguments: {
|
|
303
|
-
action:
|
|
320
|
+
action: "click",
|
|
304
321
|
x: event.x,
|
|
305
322
|
y: event.y
|
|
306
323
|
}
|
|
307
324
|
}
|
|
308
325
|
})
|
|
309
|
-
})
|
|
310
|
-
} else if (
|
|
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:
|
|
314
|
-
headers: {
|
|
330
|
+
method: "POST",
|
|
331
|
+
headers: { "Content-Type": "application/json" },
|
|
315
332
|
body: JSON.stringify({
|
|
316
|
-
jsonrpc:
|
|
333
|
+
jsonrpc: "2.0",
|
|
317
334
|
id: i + 1,
|
|
318
|
-
method:
|
|
335
|
+
method: "tools/call",
|
|
319
336
|
params: {
|
|
320
|
-
name:
|
|
337
|
+
name: "execute_browser_action",
|
|
321
338
|
arguments: {
|
|
322
|
-
action:
|
|
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 (
|
|
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:
|
|
335
|
-
headers: {
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers: { "Content-Type": "application/json" },
|
|
336
353
|
body: JSON.stringify({
|
|
337
|
-
jsonrpc:
|
|
354
|
+
jsonrpc: "2.0",
|
|
338
355
|
id: i + 1,
|
|
339
|
-
method:
|
|
356
|
+
method: "tools/call",
|
|
340
357
|
params: {
|
|
341
|
-
name:
|
|
358
|
+
name: "execute_browser_action",
|
|
342
359
|
arguments: {
|
|
343
|
-
action:
|
|
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 ===
|
|
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 :
|
|
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
|
-
|
|
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
|
+
}
|