dev3000 0.0.22 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +6 -4
- package/dist/cli.js.map +1 -1
- package/dist/dev-environment.d.ts +2 -1
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +97 -75
- package/dist/dev-environment.js.map +1 -1
- package/mcp-server/app/api/replay/route.ts +351 -0
- package/mcp-server/app/logs/LogsClient.tsx +308 -39
- package/mcp-server/app/replay/ReplayClient.tsx +274 -0
- package/mcp-server/app/replay/page.tsx +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
|
|
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;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface NavigationEvent {
|
|
17
|
+
timestamp: string;
|
|
18
|
+
url: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ScreenshotEvent {
|
|
22
|
+
timestamp: string;
|
|
23
|
+
url: string;
|
|
24
|
+
event: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ReplayData {
|
|
28
|
+
interactions: InteractionEvent[];
|
|
29
|
+
navigations: NavigationEvent[];
|
|
30
|
+
screenshots: ScreenshotEvent[];
|
|
31
|
+
startTime: string;
|
|
32
|
+
endTime: string;
|
|
33
|
+
duration: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
61
|
+
// Parse interaction events (both old and new formats)
|
|
62
|
+
const interactionMatch = line.match(/\[INTERACTION\] (.+)/);
|
|
63
|
+
if (interactionMatch) {
|
|
64
|
+
const data = interactionMatch[1];
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Try parsing as JSON (new format)
|
|
68
|
+
const interactionData = JSON.parse(data);
|
|
69
|
+
|
|
70
|
+
if (interactionData.type === 'CLICK' || interactionData.type === 'TAP') {
|
|
71
|
+
interactions.push({
|
|
72
|
+
timestamp,
|
|
73
|
+
type: interactionData.type as 'CLICK' | 'TAP',
|
|
74
|
+
x: interactionData.coordinates?.x || 0,
|
|
75
|
+
y: interactionData.coordinates?.y || 0,
|
|
76
|
+
target: interactionData.target || 'unknown'
|
|
77
|
+
});
|
|
78
|
+
} else if (interactionData.type === 'SCROLL') {
|
|
79
|
+
interactions.push({
|
|
80
|
+
timestamp,
|
|
81
|
+
type: 'SCROLL',
|
|
82
|
+
direction: interactionData.direction || 'DOWN',
|
|
83
|
+
distance: interactionData.distance || 0,
|
|
84
|
+
x: interactionData.to?.x || 0,
|
|
85
|
+
y: interactionData.to?.y || 0
|
|
86
|
+
});
|
|
87
|
+
} else if (interactionData.type === 'KEY') {
|
|
88
|
+
interactions.push({
|
|
89
|
+
timestamp,
|
|
90
|
+
type: 'KEY',
|
|
91
|
+
key: interactionData.key || 'unknown',
|
|
92
|
+
target: interactionData.target || 'unknown'
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
} catch (jsonError) {
|
|
96
|
+
// Fallback to old format parsing
|
|
97
|
+
const oldFormatMatch = data.match(/(CLICK|TAP|SCROLL|KEY) (.+)/);
|
|
98
|
+
if (oldFormatMatch) {
|
|
99
|
+
const [, type, details] = oldFormatMatch;
|
|
100
|
+
|
|
101
|
+
if (type === 'CLICK' || type === 'TAP') {
|
|
102
|
+
const coordMatch = details.match(/at \((\d+), (\d+)\) on (.+)/);
|
|
103
|
+
if (coordMatch) {
|
|
104
|
+
interactions.push({
|
|
105
|
+
timestamp,
|
|
106
|
+
type: type as 'CLICK' | 'TAP',
|
|
107
|
+
x: parseInt(coordMatch[1]),
|
|
108
|
+
y: parseInt(coordMatch[2]),
|
|
109
|
+
target: coordMatch[3]
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
} else if (type === 'SCROLL') {
|
|
113
|
+
const scrollMatch = details.match(/(\w+) (\d+)px to \((\d+), (\d+)\)/);
|
|
114
|
+
if (scrollMatch) {
|
|
115
|
+
interactions.push({
|
|
116
|
+
timestamp,
|
|
117
|
+
type: 'SCROLL',
|
|
118
|
+
direction: scrollMatch[1],
|
|
119
|
+
distance: parseInt(scrollMatch[2]),
|
|
120
|
+
x: parseInt(scrollMatch[3]),
|
|
121
|
+
y: parseInt(scrollMatch[4])
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
} else if (type === 'KEY') {
|
|
125
|
+
const keyMatch = details.match(/(.+) in (.+)/);
|
|
126
|
+
if (keyMatch) {
|
|
127
|
+
interactions.push({
|
|
128
|
+
timestamp,
|
|
129
|
+
type: 'KEY',
|
|
130
|
+
key: keyMatch[1],
|
|
131
|
+
target: keyMatch[2]
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Parse navigation events
|
|
140
|
+
const navigationMatch = line.match(/\[NAVIGATION\] (.+)/);
|
|
141
|
+
if (navigationMatch) {
|
|
142
|
+
navigations.push({
|
|
143
|
+
timestamp,
|
|
144
|
+
url: navigationMatch[1]
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse screenshot events
|
|
149
|
+
const screenshotMatch = line.match(/\[SCREENSHOT\] (.+)/);
|
|
150
|
+
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
|
+
|
|
155
|
+
screenshots.push({
|
|
156
|
+
timestamp,
|
|
157
|
+
url: screenshotMatch[1],
|
|
158
|
+
event: eventType
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const startTimeMs = new Date(actualStartTime).getTime();
|
|
164
|
+
const endTimeMs = new Date(actualEndTime).getTime();
|
|
165
|
+
const duration = endTimeMs - startTimeMs;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
interactions,
|
|
169
|
+
navigations,
|
|
170
|
+
screenshots,
|
|
171
|
+
startTime: actualStartTime,
|
|
172
|
+
endTime: actualEndTime,
|
|
173
|
+
duration
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function GET(request: NextRequest) {
|
|
178
|
+
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
|
+
|
|
184
|
+
// Get log file path from environment
|
|
185
|
+
const logFilePath = process.env.LOG_FILE_PATH || '/tmp/dev3000.log';
|
|
186
|
+
|
|
187
|
+
if (!existsSync(logFilePath)) {
|
|
188
|
+
return NextResponse.json({ error: 'Log file not found' }, { status: 404 });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const logContent = readFileSync(logFilePath, 'utf8');
|
|
192
|
+
|
|
193
|
+
if (action === 'parse') {
|
|
194
|
+
// Parse the log file and return replay data
|
|
195
|
+
const replayData = parseLogFile(logContent, startTime || undefined, endTime || undefined);
|
|
196
|
+
return NextResponse.json(replayData);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error('Replay API error:', error);
|
|
202
|
+
return NextResponse.json(
|
|
203
|
+
{ error: 'Failed to process replay request' },
|
|
204
|
+
{ status: 500 }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function POST(request: NextRequest) {
|
|
210
|
+
try {
|
|
211
|
+
const body = await request.json();
|
|
212
|
+
const { action, replayData, speed = 1 } = body;
|
|
213
|
+
|
|
214
|
+
if (action === 'execute') {
|
|
215
|
+
// Generate CDP commands for replay
|
|
216
|
+
const cdpCommands = generateCDPCommands(replayData, speed);
|
|
217
|
+
|
|
218
|
+
// Try to execute the commands via CDP
|
|
219
|
+
try {
|
|
220
|
+
const result = await executeCDPCommands(cdpCommands);
|
|
221
|
+
return NextResponse.json({
|
|
222
|
+
success: true,
|
|
223
|
+
message: 'Replay executed successfully',
|
|
224
|
+
result: result,
|
|
225
|
+
totalCommands: cdpCommands.length
|
|
226
|
+
});
|
|
227
|
+
} catch (error) {
|
|
228
|
+
// Fallback: return commands for manual execution
|
|
229
|
+
return NextResponse.json({
|
|
230
|
+
success: false,
|
|
231
|
+
message: 'CDP execution failed, returning commands for manual execution',
|
|
232
|
+
commands: cdpCommands,
|
|
233
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error('Replay execution error:', error);
|
|
241
|
+
return NextResponse.json(
|
|
242
|
+
{ error: 'Failed to execute replay' },
|
|
243
|
+
{ status: 500 }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface CDPCommand {
|
|
249
|
+
method: string;
|
|
250
|
+
params: any;
|
|
251
|
+
delay: number;
|
|
252
|
+
description: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function generateCDPCommands(replayData: ReplayData, speed: number): CDPCommand[] {
|
|
256
|
+
const events = [
|
|
257
|
+
...replayData.interactions.map(i => ({ ...i, eventType: 'interaction' })),
|
|
258
|
+
...replayData.navigations.map(n => ({ ...n, eventType: 'navigation' }))
|
|
259
|
+
].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
260
|
+
|
|
261
|
+
const commands: CDPCommand[] = [];
|
|
262
|
+
const startTime = new Date(replayData.startTime).getTime();
|
|
263
|
+
|
|
264
|
+
for (const event of events) {
|
|
265
|
+
const eventTime = new Date(event.timestamp).getTime();
|
|
266
|
+
const delay = Math.max(0, (eventTime - startTime) / speed);
|
|
267
|
+
|
|
268
|
+
if (event.eventType === 'navigation') {
|
|
269
|
+
commands.push({
|
|
270
|
+
method: 'Page.navigate',
|
|
271
|
+
params: { url: event.url },
|
|
272
|
+
delay: delay,
|
|
273
|
+
description: `Navigate to ${event.url}`
|
|
274
|
+
});
|
|
275
|
+
} else if (event.eventType === 'interaction') {
|
|
276
|
+
if (event.type === 'CLICK' && event.x !== undefined && event.y !== undefined) {
|
|
277
|
+
// Mouse down
|
|
278
|
+
commands.push({
|
|
279
|
+
method: 'Input.dispatchMouseEvent',
|
|
280
|
+
params: {
|
|
281
|
+
type: 'mousePressed',
|
|
282
|
+
x: event.x,
|
|
283
|
+
y: event.y,
|
|
284
|
+
button: 'left',
|
|
285
|
+
clickCount: 1
|
|
286
|
+
},
|
|
287
|
+
delay: delay,
|
|
288
|
+
description: `Click at (${event.x}, ${event.y}) on ${event.target}`
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Mouse up (after small delay)
|
|
292
|
+
commands.push({
|
|
293
|
+
method: 'Input.dispatchMouseEvent',
|
|
294
|
+
params: {
|
|
295
|
+
type: 'mouseReleased',
|
|
296
|
+
x: event.x,
|
|
297
|
+
y: event.y,
|
|
298
|
+
button: 'left',
|
|
299
|
+
clickCount: 1
|
|
300
|
+
},
|
|
301
|
+
delay: 50, // 50ms between down and up
|
|
302
|
+
description: `Release click at (${event.x}, ${event.y})`
|
|
303
|
+
});
|
|
304
|
+
} else if (event.type === 'SCROLL' && event.x !== undefined && event.y !== undefined) {
|
|
305
|
+
commands.push({
|
|
306
|
+
method: 'Runtime.evaluate',
|
|
307
|
+
params: {
|
|
308
|
+
expression: `window.scrollTo({left: ${event.x}, top: ${event.y}, behavior: 'smooth'})`
|
|
309
|
+
},
|
|
310
|
+
delay: delay,
|
|
311
|
+
description: `Scroll to (${event.x}, ${event.y})`
|
|
312
|
+
});
|
|
313
|
+
} else if (event.type === 'KEY' && event.key) {
|
|
314
|
+
// Key down
|
|
315
|
+
commands.push({
|
|
316
|
+
method: 'Input.dispatchKeyEvent',
|
|
317
|
+
params: {
|
|
318
|
+
type: 'keyDown',
|
|
319
|
+
key: event.key,
|
|
320
|
+
text: event.key.length === 1 ? event.key : undefined
|
|
321
|
+
},
|
|
322
|
+
delay: delay,
|
|
323
|
+
description: `Key down: ${event.key}`
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Key up
|
|
327
|
+
commands.push({
|
|
328
|
+
method: 'Input.dispatchKeyEvent',
|
|
329
|
+
params: {
|
|
330
|
+
type: 'keyUp',
|
|
331
|
+
key: event.key
|
|
332
|
+
},
|
|
333
|
+
delay: 50,
|
|
334
|
+
description: `Key up: ${event.key}`
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return commands;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function executeCDPCommands(commands: CDPCommand[]): Promise<any> {
|
|
344
|
+
// For now, we'll try to connect to the CDP session
|
|
345
|
+
// This would require the browser to be launched with --remote-debugging-port
|
|
346
|
+
// or we'd need to get the CDP endpoint from the Playwright instance
|
|
347
|
+
|
|
348
|
+
// Since we can't easily access the existing Playwright browser from here,
|
|
349
|
+
// let's return the commands for the client to execute
|
|
350
|
+
throw new Error('Direct CDP execution not yet implemented - browser connection needed');
|
|
351
|
+
}
|