dev3000 0.0.121 → 0.0.124

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 (62) hide show
  1. package/dist/cli.js +19 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/dev-environment.d.ts.map +1 -1
  4. package/dist/dev-environment.js +11 -0
  5. package/dist/dev-environment.js.map +1 -1
  6. package/dist/utils/log-filename.d.ts.map +1 -1
  7. package/dist/utils/log-filename.js +8 -3
  8. package/dist/utils/log-filename.js.map +1 -1
  9. package/mcp-server/app/mcp/route.ts +81 -7
  10. package/mcp-server/app/mcp/tools.ts +33 -3
  11. package/mcp-server/next-env.d.ts +1 -1
  12. package/mcp-server/package.json +0 -12
  13. package/package.json +6 -7
  14. package/mcp-server/.next/build/chunks/[root-of-the-server]__25374c4f._.js +0 -500
  15. package/mcp-server/.next/build/chunks/[root-of-the-server]__25374c4f._.js.map +0 -11
  16. package/mcp-server/.next/build/chunks/[root-of-the-server]__6e020478._.js +0 -441
  17. package/mcp-server/.next/build/chunks/[root-of-the-server]__6e020478._.js.map +0 -7
  18. package/mcp-server/.next/build/chunks/[root-of-the-server]__c438ef56._.js +0 -205
  19. package/mcp-server/.next/build/chunks/[root-of-the-server]__c438ef56._.js.map +0 -8
  20. package/mcp-server/.next/build/chunks/[root-of-the-server]__c7ae8543._.js +0 -500
  21. package/mcp-server/.next/build/chunks/[root-of-the-server]__c7ae8543._.js.map +0 -11
  22. package/mcp-server/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_80bff36f._.js +0 -13
  23. package/mcp-server/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_80bff36f._.js.map +0 -5
  24. package/mcp-server/.next/build/chunks/[turbopack-node]_transforms_webpack-loaders_ts_c84aa21a._.js +0 -12
  25. package/mcp-server/.next/build/chunks/[turbopack-node]_transforms_webpack-loaders_ts_c84aa21a._.js.map +0 -5
  26. package/mcp-server/.next/build/chunks/[turbopack]_runtime.js +0 -770
  27. package/mcp-server/.next/build/chunks/[turbopack]_runtime.js.map +0 -10
  28. package/mcp-server/.next/build/chunks/node_modules__pnpm_806d01c0._.js +0 -6758
  29. package/mcp-server/.next/build/chunks/node_modules__pnpm_806d01c0._.js.map +0 -47
  30. package/mcp-server/.next/build/package.json +0 -1
  31. package/mcp-server/.next/build/postcss.js +0 -6
  32. package/mcp-server/.next/build/postcss.js.map +0 -5
  33. package/mcp-server/.next/build/webpack-loaders.js +0 -6
  34. package/mcp-server/.next/build/webpack-loaders.js.map +0 -5
  35. package/mcp-server/.next/package.json +0 -1
  36. package/mcp-server/app/api/auth/authorize/route.ts +0 -52
  37. package/mcp-server/app/api/auth/callback/route.ts +0 -128
  38. package/mcp-server/app/api/auth/signout/route.ts +0 -34
  39. package/mcp-server/app/api/auth/token/route.ts +0 -16
  40. package/mcp-server/app/api/cloud/check-pr/route.ts +0 -53
  41. package/mcp-server/app/api/cloud/check-pr/steps.ts +0 -458
  42. package/mcp-server/app/api/cloud/check-pr/workflow.ts +0 -109
  43. package/mcp-server/app/api/cloud/fix-workflow/health/route.ts +0 -10
  44. package/mcp-server/app/api/cloud/fix-workflow/route.ts +0 -50
  45. package/mcp-server/app/api/cloud/fix-workflow/steps.ts +0 -2091
  46. package/mcp-server/app/api/cloud/fix-workflow/workflow.ts +0 -296
  47. package/mcp-server/app/api/cloud/start-fix/route.ts +0 -192
  48. package/mcp-server/app/api/integration/webhook/route.ts +0 -290
  49. package/mcp-server/app/api/projects/[projectId]/bypass-token/route.ts +0 -48
  50. package/mcp-server/app/api/projects/branches/route.ts +0 -115
  51. package/mcp-server/app/api/projects/check-protection/route.ts +0 -33
  52. package/mcp-server/app/api/projects/route.ts +0 -97
  53. package/mcp-server/app/api/workflows/route.ts +0 -105
  54. package/mcp-server/app/auth/error/page.tsx +0 -47
  55. package/mcp-server/app/signin/page.tsx +0 -37
  56. package/mcp-server/app/workflows/[id]/report/agent-analysis.tsx +0 -7
  57. package/mcp-server/app/workflows/[id]/report/page.tsx +0 -199
  58. package/mcp-server/app/workflows/new/new-workflow-client.tsx +0 -32
  59. package/mcp-server/app/workflows/new/page.tsx +0 -13
  60. package/mcp-server/app/workflows/new-workflow-modal.tsx +0 -973
  61. package/mcp-server/app/workflows/page.tsx +0 -16
  62. package/mcp-server/app/workflows/workflows-client.tsx +0 -290
@@ -1,2091 +0,0 @@
1
- /**
2
- * Step functions for fix-workflow
3
- * Separated into their own module to avoid workflow bundler issues
4
- */
5
-
6
- import { put } from "@vercel/blob"
7
- import type { Sandbox } from "@vercel/sandbox"
8
- import { createGateway, generateText, stepCountIs, tool } from "ai"
9
- import { z } from "zod"
10
- import { createD3kSandbox as createD3kSandboxUtil } from "@/lib/cloud/d3k-sandbox"
11
- import type { WorkflowReport } from "@/types"
12
-
13
- /**
14
- * D3K Sandbox Tools
15
- * These tools allow the AI agent to interact with the sandbox environment
16
- * where d3k is running, giving it access to code, search, and MCP capabilities
17
- */
18
-
19
- /**
20
- * Create tools that execute against the sandbox
21
- * These are d3k-specific - they know about the sandbox structure and d3k MCP
22
- */
23
- function createD3kSandboxTools(sandbox: Sandbox, mcpUrl: string) {
24
- const SANDBOX_CWD = "/vercel/sandbox"
25
-
26
- return {
27
- /**
28
- * Read a file from the sandbox
29
- */
30
- readFile: tool({
31
- description:
32
- "Read a file from the codebase. Use this to understand code before proposing fixes. Path should be relative to project root (e.g., 'src/components/Header.tsx').",
33
- inputSchema: z.object({
34
- path: z.string().describe("File path relative to project root"),
35
- maxLines: z.number().optional().describe("Maximum lines to read (default: 500)")
36
- }),
37
- execute: async ({ path, maxLines = 500 }: { path: string; maxLines?: number }) => {
38
- const fullPath = `${SANDBOX_CWD}/${path}`
39
- const result = await runSandboxCommand(sandbox, "sh", [
40
- "-c",
41
- `head -n ${maxLines} "${fullPath}" 2>&1 || echo "ERROR: File not found or unreadable"`
42
- ])
43
- if (result.stdout.startsWith("ERROR:")) {
44
- return `Failed to read ${path}: ${result.stdout}`
45
- }
46
- return `Contents of ${path}:\n\`\`\`\n${result.stdout}\n\`\`\``
47
- }
48
- }),
49
-
50
- /**
51
- * Search for files by glob pattern
52
- */
53
- globSearch: tool({
54
- description:
55
- "Find files matching a glob pattern. Use this to discover relevant files. Examples: '**/*.tsx', 'src/components/*.ts', '**/Header*'",
56
- inputSchema: z.object({
57
- pattern: z.string().describe("Glob pattern to match files"),
58
- maxResults: z.number().optional().describe("Maximum results (default: 20)")
59
- }),
60
- execute: async ({ pattern, maxResults = 20 }: { pattern: string; maxResults?: number }) => {
61
- const result = await runSandboxCommand(sandbox, "sh", [
62
- "-c",
63
- `cd ${SANDBOX_CWD} && find . -type f -name "${pattern}" 2>/dev/null | head -n ${maxResults} | sed 's|^\\./||'`
64
- ])
65
- if (!result.stdout.trim()) {
66
- return `No files found matching pattern: ${pattern}`
67
- }
68
- const files = result.stdout.trim().split("\n")
69
- return `Found ${files.length} file(s) matching "${pattern}":\n${files.map((f) => `- ${f}`).join("\n")}`
70
- }
71
- }),
72
-
73
- /**
74
- * Search file contents with grep
75
- */
76
- grepSearch: tool({
77
- description:
78
- "Search for text/patterns in files. Use this to find where specific code, classes, or functions are defined or used.",
79
- inputSchema: z.object({
80
- pattern: z.string().describe("Search pattern (regex supported)"),
81
- fileGlob: z.string().optional().describe("File pattern to search in (e.g., '*.tsx')"),
82
- maxResults: z.number().optional().describe("Maximum results (default: 20)")
83
- }),
84
- execute: async ({
85
- pattern,
86
- fileGlob,
87
- maxResults = 20
88
- }: {
89
- pattern: string
90
- fileGlob?: string
91
- maxResults?: number
92
- }) => {
93
- const includeArg = fileGlob ? `--include="${fileGlob}"` : ""
94
- const result = await runSandboxCommand(sandbox, "sh", [
95
- "-c",
96
- `cd ${SANDBOX_CWD} && grep -rn ${includeArg} "${pattern}" . 2>/dev/null | head -n ${maxResults}`
97
- ])
98
- if (!result.stdout.trim()) {
99
- return `No matches found for pattern: ${pattern}`
100
- }
101
- return `Search results for "${pattern}":\n${result.stdout}`
102
- }
103
- }),
104
-
105
- /**
106
- * List directory contents
107
- */
108
- listDirectory: tool({
109
- description: "List files and directories at a path. Use this to explore the project structure.",
110
- inputSchema: z.object({
111
- path: z.string().optional().describe("Directory path relative to project root (default: root)")
112
- }),
113
- execute: async ({ path = "" }: { path?: string }) => {
114
- const fullPath = path ? `${SANDBOX_CWD}/${path}` : SANDBOX_CWD
115
- const result = await runSandboxCommand(sandbox, "sh", ["-c", `ls -la "${fullPath}" 2>&1`])
116
- return `Contents of ${path || "/"}:\n${result.stdout}`
117
- }
118
- }),
119
-
120
- /**
121
- * Call d3k MCP tool - find_component_source
122
- * This is d3k-specific: maps DOM elements to React component source
123
- */
124
- findComponentSource: tool({
125
- description:
126
- "Find the source file for a React component by its DOM selector. Use this when you know which element caused a layout shift and need to find the source file to fix it. d3k-specific tool.",
127
- inputSchema: z.object({
128
- selector: z.string().describe("CSS selector for the DOM element (e.g., 'nav', '.header', '#main')")
129
- }),
130
- execute: async ({ selector }: { selector: string }) => {
131
- try {
132
- const mcpResponse = await fetch(`${mcpUrl}/mcp`, {
133
- method: "POST",
134
- headers: { "Content-Type": "application/json" },
135
- body: JSON.stringify({
136
- jsonrpc: "2.0",
137
- id: 1,
138
- method: "tools/call",
139
- params: {
140
- name: "find_component_source",
141
- arguments: { selector }
142
- }
143
- })
144
- })
145
-
146
- if (!mcpResponse.ok) {
147
- return `Failed to call find_component_source: HTTP ${mcpResponse.status}`
148
- }
149
-
150
- const text = await mcpResponse.text()
151
- // Parse SSE response
152
- const lines = text.split("\n")
153
- for (const line of lines) {
154
- if (line.startsWith("data: ")) {
155
- try {
156
- const json = JSON.parse(line.substring(6))
157
- if (json.result?.content) {
158
- for (const content of json.result.content) {
159
- if (content.type === "text") {
160
- return content.text
161
- }
162
- }
163
- }
164
- } catch {
165
- // Continue to next line on parse failure
166
- }
167
- }
168
- }
169
- return `No result from find_component_source for selector: ${selector}`
170
- } catch (error) {
171
- return `Error calling find_component_source: ${error instanceof Error ? error.message : String(error)}`
172
- }
173
- }
174
- }),
175
-
176
- /**
177
- * Write/edit a file in the sandbox
178
- */
179
- writeFile: tool({
180
- description:
181
- "Write content to a file. Use this to apply fixes. For small edits, prefer editFile. For creating new files or complete rewrites, use this.",
182
- inputSchema: z.object({
183
- path: z.string().describe("File path relative to project root"),
184
- content: z.string().describe("Complete file content to write")
185
- }),
186
- execute: async ({ path, content }: { path: string; content: string }) => {
187
- const fullPath = `${SANDBOX_CWD}/${path}`
188
- // Escape content for shell
189
- const escapedContent = content.replace(/'/g, "'\\''")
190
- const result = await runSandboxCommand(sandbox, "sh", [
191
- "-c",
192
- `cat > "${fullPath}" << 'FILEEOF'\n${escapedContent}\nFILEEOF`
193
- ])
194
- if (result.exitCode !== 0) {
195
- return `Failed to write ${path}: ${result.stderr}`
196
- }
197
- return `Successfully wrote ${content.length} characters to ${path}`
198
- }
199
- }),
200
-
201
- /**
202
- * Get git diff of changes made so far
203
- */
204
- getGitDiff: tool({
205
- description:
206
- "Get the git diff of all changes made in the sandbox. Use this to review your fixes before finalizing.",
207
- inputSchema: z.object({}),
208
- execute: async () => {
209
- const result = await runSandboxCommand(sandbox, "sh", [
210
- "-c",
211
- `cd ${SANDBOX_CWD} && git diff --no-color 2>/dev/null || echo "No changes or not a git repo"`
212
- ])
213
- if (!result.stdout.trim() || result.stdout.includes("No changes")) {
214
- return "No changes have been made yet."
215
- }
216
- return `Current changes:\n\`\`\`diff\n${result.stdout}\n\`\`\``
217
- }
218
- })
219
- }
220
- }
221
-
222
- /**
223
- * Save or update a workflow report to blob storage
224
- * This is called incrementally as data becomes available throughout the workflow
225
- */
226
- export async function saveReportToBlob(
227
- report: Partial<WorkflowReport> & { id: string; projectName: string; timestamp: string }
228
- ): Promise<string> {
229
- // Use consistent filename based on report ID so updates overwrite the same file
230
- const filename = `report-${report.id}.json`
231
-
232
- const blob = await put(filename, JSON.stringify(report, null, 2), {
233
- access: "public",
234
- contentType: "application/json",
235
- addRandomSuffix: false, // Important: ensures we can update the same file
236
- allowOverwrite: true // Required: allows updating the same file on subsequent saves
237
- })
238
-
239
- console.log(`[Report] Saved report ${report.id} to: ${blob.url}`)
240
- return blob.url
241
- }
242
-
243
- /**
244
- * Helper function to properly consume sandbox command output
245
- * The Vercel Sandbox SDK returns a result object with an async logs() iterator
246
- */
247
- async function runSandboxCommand(
248
- sandbox: Sandbox,
249
- cmd: string,
250
- args: string[]
251
- ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
252
- const result = await sandbox.runCommand({ cmd, args })
253
- let stdout = ""
254
- let stderr = ""
255
- for await (const log of result.logs()) {
256
- if (log.stream === "stdout") {
257
- stdout += log.data
258
- } else {
259
- stderr += log.data
260
- }
261
- }
262
- await result.wait()
263
- return { exitCode: result.exitCode, stdout, stderr }
264
- }
265
-
266
- /**
267
- * Capture a screenshot using puppeteer-core inside the sandbox
268
- * Returns the base64 encoded PNG image data and logs the page title
269
- */
270
- async function captureScreenshotInSandbox(
271
- sandbox: Sandbox,
272
- appUrl: string,
273
- chromiumPath: string,
274
- label: string,
275
- sandboxCwd = "/vercel/sandbox"
276
- ): Promise<string | null> {
277
- console.log(`[Screenshot] Capturing ${label} screenshot of ${appUrl}...`)
278
-
279
- // Create a Node.js script to capture screenshot with puppeteer-core
280
- // The script is placed in the sandbox cwd so it can find puppeteer-core from node_modules
281
- // Output format: JSON with { title, screenshot } on first line, then base64 data
282
- const screenshotScript = `
283
- const puppeteer = require('puppeteer-core');
284
-
285
- (async () => {
286
- let browser;
287
- try {
288
- browser = await puppeteer.launch({
289
- executablePath: '${chromiumPath}',
290
- headless: true,
291
- args: [
292
- '--no-sandbox',
293
- '--disable-setuid-sandbox',
294
- '--disable-dev-shm-usage',
295
- '--disable-gpu',
296
- '--single-process'
297
- ]
298
- });
299
-
300
- const page = await browser.newPage();
301
- await page.setViewport({ width: 1280, height: 720 });
302
-
303
- // Navigate with a reasonable timeout
304
- await page.goto('${appUrl}', {
305
- waitUntil: 'networkidle2',
306
- timeout: 30000
307
- });
308
-
309
- // Get the page title for verification
310
- const title = await page.title();
311
- console.error('PAGE_TITLE:' + title);
312
-
313
- // Wait a bit for any animations/layout shifts
314
- await new Promise(r => setTimeout(r, 2000));
315
-
316
- // Take screenshot as base64
317
- const screenshot = await page.screenshot({
318
- encoding: 'base64',
319
- fullPage: false
320
- });
321
-
322
- console.log(screenshot);
323
-
324
- await browser.close();
325
- } catch (error) {
326
- console.error('Screenshot error:', error.message);
327
- if (browser) await browser.close();
328
- process.exit(1);
329
- }
330
- })();
331
- `
332
-
333
- try {
334
- // Write the script to the sandbox cwd so it can find puppeteer-core from node_modules
335
- const scriptPath = `${sandboxCwd}/_screenshot.js`
336
- const writeResult = await runSandboxCommand(sandbox, "sh", [
337
- "-c",
338
- `cat > ${scriptPath} << 'SCRIPT_EOF'
339
- ${screenshotScript}
340
- SCRIPT_EOF`
341
- ])
342
-
343
- if (writeResult.exitCode !== 0) {
344
- console.log(`[Screenshot] Failed to write script: ${writeResult.stderr}`)
345
- return null
346
- }
347
-
348
- // Run the script from the sandbox directory so node can find puppeteer-core in node_modules
349
- const screenshotResult = await sandbox.runCommand({
350
- cmd: "node",
351
- args: [scriptPath],
352
- cwd: sandboxCwd
353
- })
354
-
355
- let stdout = ""
356
- let stderr = ""
357
- for await (const log of screenshotResult.logs()) {
358
- if (log.stream === "stdout") {
359
- stdout += log.data
360
- } else {
361
- stderr += log.data
362
- }
363
- }
364
- await screenshotResult.wait()
365
-
366
- // Extract page title from stderr (format: PAGE_TITLE:xxx)
367
- const titleMatch = stderr.match(/PAGE_TITLE:(.*)/)
368
- if (titleMatch) {
369
- console.log(`[Screenshot] Page title: "${titleMatch[1]}"`)
370
- }
371
-
372
- if (screenshotResult.exitCode !== 0) {
373
- console.log(`[Screenshot] Failed to capture: ${stderr}`)
374
- return null
375
- }
376
-
377
- // The stdout should be the base64 image
378
- const base64Data = stdout.trim()
379
- if (base64Data && base64Data.length > 100) {
380
- console.log(`[Screenshot] Captured ${label} screenshot (${base64Data.length} bytes base64)`)
381
- return base64Data
382
- }
383
-
384
- console.log(`[Screenshot] No valid screenshot data returned`)
385
- return null
386
- } catch (error) {
387
- console.log(`[Screenshot] Error: ${error instanceof Error ? error.message : String(error)}`)
388
- return null
389
- }
390
- }
391
-
392
- /**
393
- * Upload a base64 screenshot to Vercel Blob
394
- */
395
- async function uploadScreenshot(base64Data: string, label: string, projectName: string): Promise<string | null> {
396
- try {
397
- const imageBuffer = Buffer.from(base64Data, "base64")
398
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
399
- const filename = `screenshot-${label}-${projectName}-${timestamp}.png`
400
-
401
- const blob = await put(filename, imageBuffer, {
402
- access: "public",
403
- contentType: "image/png"
404
- })
405
-
406
- console.log(`[Screenshot] Uploaded ${label} screenshot: ${blob.url}`)
407
- return blob.url
408
- } catch (error) {
409
- console.log(`[Screenshot] Upload failed: ${error instanceof Error ? error.message : String(error)}`)
410
- return null
411
- }
412
- }
413
-
414
- /**
415
- * Fetch d3k's CLS jank screenshots from the sandbox MCP server and upload to Vercel Blob
416
- * Returns URLs to the uploaded screenshots and metadata
417
- */
418
- async function fetchAndUploadD3kArtifacts(
419
- sandbox: Sandbox,
420
- _mcpUrl: string,
421
- projectName: string
422
- ): Promise<{
423
- clsScreenshots: Array<{ label: string; blobUrl: string; timestamp: number }>
424
- screencastSessionId: string | null
425
- fullLogs: string | null
426
- metadata: Record<string, unknown> | null
427
- }> {
428
- const result: {
429
- clsScreenshots: Array<{ label: string; blobUrl: string; timestamp: number }>
430
- screencastSessionId: string | null
431
- fullLogs: string | null
432
- metadata: Record<string, unknown> | null
433
- } = {
434
- clsScreenshots: [],
435
- screencastSessionId: null,
436
- fullLogs: null,
437
- metadata: null
438
- }
439
-
440
- console.log(`[D3k Artifacts] Fetching screenshots and logs from sandbox MCP server...`)
441
-
442
- try {
443
- // 1. Fetch full d3k logs from the sandbox
444
- console.log(`[D3k Artifacts] Fetching d3k logs...`)
445
- const logsResult = await runSandboxCommand(sandbox, "sh", [
446
- "-c",
447
- 'for log in /home/vercel-sandbox/.d3k/logs/*.log; do [ -f "$log" ] && cat "$log" || true; done 2>/dev/null || echo "No log files found"'
448
- ])
449
- if (logsResult.exitCode === 0 && logsResult.stdout) {
450
- result.fullLogs = logsResult.stdout
451
- console.log(`[D3k Artifacts] Captured ${result.fullLogs.length} chars of d3k logs`)
452
- }
453
-
454
- // 2. Parse logs to find screenshot URLs and session ID
455
- if (result.fullLogs) {
456
- // Extract screenshot filenames from logs like:
457
- // [CDP] Before: http://localhost:3684/api/screenshots/2025-12-06T21-39-24Z-jank-388ms.png
458
- const screenshotRegex = /http:\/\/localhost:\d+\/api\/screenshots\/([^\s]+\.png)/g
459
- const screenshotMatches = [...result.fullLogs.matchAll(screenshotRegex)]
460
- const uniqueFilenames = [...new Set(screenshotMatches.map((m) => m[1]))]
461
- console.log(`[D3k Artifacts] Found ${uniqueFilenames.length} screenshot filenames in logs`)
462
-
463
- // Extract session ID from screencast URL like:
464
- // [SCREENCAST] View frame analysis: http://localhost:3684/video/2025-12-06T21-39-24Z
465
- const sessionMatch = result.fullLogs.match(/\/video\/([^\s]+)/)
466
- if (sessionMatch) {
467
- result.screencastSessionId = sessionMatch[1]
468
- console.log(`[D3k Artifacts] Screencast session ID: ${result.screencastSessionId}`)
469
- }
470
-
471
- // 3. Fetch and upload each screenshot to Vercel Blob
472
- for (const filename of uniqueFilenames) {
473
- try {
474
- // Fetch screenshot from sandbox MCP server
475
- console.log(`[D3k Artifacts] Fetching screenshot: ${filename}`)
476
-
477
- // Use curl from inside sandbox to get the screenshot as base64
478
- const fetchResult = await runSandboxCommand(sandbox, "sh", [
479
- "-c",
480
- `curl -s http://localhost:3684/api/screenshots/${filename} | base64`
481
- ])
482
-
483
- if (fetchResult.exitCode === 0 && fetchResult.stdout.trim().length > 100) {
484
- const base64Data = fetchResult.stdout.trim()
485
-
486
- // Upload to Vercel Blob
487
- const imageBuffer = Buffer.from(base64Data, "base64")
488
- const blobFilename = `d3k-cls-${projectName}-${filename}`
489
- const blob = await put(blobFilename, imageBuffer, {
490
- access: "public",
491
- contentType: "image/png"
492
- })
493
-
494
- // Extract timestamp from filename like "2025-12-06T21-39-24Z-jank-388ms.png"
495
- const timestampMatch = filename.match(/-(\d+)ms\.png$/)
496
- const timestamp = timestampMatch ? parseInt(timestampMatch[1], 10) : 0
497
-
498
- result.clsScreenshots.push({
499
- label: filename,
500
- blobUrl: blob.url,
501
- timestamp
502
- })
503
-
504
- console.log(`[D3k Artifacts] Uploaded ${filename} -> ${blob.url}`)
505
- } else {
506
- console.log(`[D3k Artifacts] Failed to fetch ${filename}: empty or error`)
507
- }
508
- } catch (error) {
509
- console.log(
510
- `[D3k Artifacts] Error fetching screenshot ${filename}: ${error instanceof Error ? error.message : String(error)}`
511
- )
512
- }
513
- }
514
-
515
- // 4. Fetch metadata JSON if available
516
- if (result.screencastSessionId) {
517
- try {
518
- const metadataResult = await runSandboxCommand(sandbox, "sh", [
519
- "-c",
520
- `curl -s http://localhost:3684/api/screenshots/${result.screencastSessionId}-metadata.json`
521
- ])
522
- if (metadataResult.exitCode === 0 && metadataResult.stdout.trim().startsWith("{")) {
523
- result.metadata = JSON.parse(metadataResult.stdout.trim())
524
- console.log(
525
- `[D3k Artifacts] Captured metadata: CLS score ${(result.metadata as { totalCLS?: number })?.totalCLS}`
526
- )
527
- }
528
- } catch {
529
- console.log(`[D3k Artifacts] Could not fetch metadata`)
530
- }
531
- }
532
- }
533
-
534
- console.log(
535
- `[D3k Artifacts] Summary: ${result.clsScreenshots.length} screenshots, ${result.fullLogs ? "logs captured" : "no logs"}, metadata: ${result.metadata ? "yes" : "no"}`
536
- )
537
- } catch (error) {
538
- console.log(`[D3k Artifacts] Error: ${error instanceof Error ? error.message : String(error)}`)
539
- }
540
-
541
- return result
542
- }
543
-
544
- /**
545
- * Step 0: Create d3k sandbox with MCP tools pre-configured
546
- * Also captures a "before" screenshot of the app and saves initial report to blob
547
- */
548
- export async function createD3kSandbox(
549
- repoUrl: string,
550
- branch: string,
551
- projectName: string,
552
- vercelToken?: string,
553
- vercelOidcToken?: string,
554
- runId?: string
555
- ) {
556
- "use step"
557
-
558
- console.log(`[Step 0] Creating d3k sandbox for ${projectName}...`)
559
- console.log(`[Step 0] Repository: ${repoUrl}`)
560
- console.log(`[Step 0] Branch: ${branch}`)
561
-
562
- // Log available token types
563
- console.log(`[Step 0] VERCEL_OIDC_TOKEN from env: ${!!process.env.VERCEL_OIDC_TOKEN}`)
564
- console.log(`[Step 0] VERCEL_OIDC_TOKEN passed as param: ${!!vercelOidcToken}`)
565
- console.log(`[Step 0] VERCEL_TOKEN available: ${!!process.env.VERCEL_TOKEN}`)
566
- console.log(`[Step 0] User access token provided: ${!!vercelToken}`)
567
-
568
- // Set VERCEL_OIDC_TOKEN if passed from workflow context
569
- // This is necessary because workflow steps don't automatically inherit environment variables
570
- if (vercelOidcToken && !process.env.VERCEL_OIDC_TOKEN) {
571
- process.env.VERCEL_OIDC_TOKEN = vercelOidcToken
572
- console.log(`[Step 0] Set VERCEL_OIDC_TOKEN from workflow context`)
573
- }
574
-
575
- const sandboxResult = await createD3kSandboxUtil({
576
- repoUrl,
577
- branch,
578
- projectDir: "",
579
- packageManager: "pnpm",
580
- debug: true
581
- })
582
-
583
- console.log(`[Step 0] Sandbox created successfully`)
584
- console.log(`[Step 0] Dev URL: ${sandboxResult.devUrl}`)
585
- console.log(`[Step 0] MCP URL: ${sandboxResult.mcpUrl}`)
586
-
587
- // Get the chromium path for screenshots
588
- console.log(`[Step 0] Getting Chromium path for screenshots...`)
589
- let chromiumPath = "/tmp/chromium"
590
- try {
591
- const chromiumResult = await runSandboxCommand(sandboxResult.sandbox, "node", [
592
- "-e",
593
- "require('@sparticuz/chromium').executablePath().then(p => console.log(p))"
594
- ])
595
- if (chromiumResult.exitCode === 0 && chromiumResult.stdout.trim()) {
596
- chromiumPath = chromiumResult.stdout.trim()
597
- console.log(`[Step 0] Chromium path: ${chromiumPath}`)
598
- }
599
- } catch {
600
- console.log(`[Step 0] Could not get chromium path, using default: ${chromiumPath}`)
601
- }
602
-
603
- // CRITICAL DIAGNOSTIC: Test Chrome with EXACT d3k command
604
- // d3k uses: --user-data-dir, no --remote-debugging-address, loading page, etc.
605
- console.log(`[Step 0] ===== CHROMIUM CDP TEST (d3k exact command) =====`)
606
- try {
607
- const chromeTestScript = `
608
- exec 2>&1
609
- echo "=== Chromium CDP Test (d3k exact command) ==="
610
- echo "Chromium path: ${chromiumPath}"
611
- echo ""
612
-
613
- # Create user-data-dir like d3k does
614
- USER_DATA_DIR="/tmp/d3k-test-profile"
615
- mkdir -p "$USER_DATA_DIR"
616
- echo "1. Created user-data-dir: $USER_DATA_DIR"
617
-
618
- # Create loading page like d3k does
619
- LOADING_DIR="/tmp/dev3000-loading"
620
- mkdir -p "$LOADING_DIR"
621
- cat > "$LOADING_DIR/loading.html" << 'LOADINGHTML'
622
- <!DOCTYPE html>
623
- <html>
624
- <head><title>Loading...</title></head>
625
- <body><h1>Loading dev3000...</h1></body>
626
- </html>
627
- LOADINGHTML
628
- echo "2. Created loading page: $LOADING_DIR/loading.html"
629
- echo ""
630
-
631
- # Use EXACT d3k command (from cdp-monitor.ts)
632
- # Note: d3k does NOT use --remote-debugging-address
633
- echo "3. Starting Chrome with d3k's exact args..."
634
- echo " Command: ${chromiumPath} --remote-debugging-port=9222 --user-data-dir=$USER_DATA_DIR --no-first-run --no-default-browser-check --disable-component-extensions-with-background-pages --disable-background-networking --disable-sync --metrics-recording-only --disable-default-apps --disable-session-crashed-bubble --disable-restore-session-state --headless=new --no-sandbox --disable-setuid-sandbox --disable-gpu --disable-dev-shm-usage file://$LOADING_DIR/loading.html"
635
-
636
- timeout 15 "${chromiumPath}" \\
637
- --remote-debugging-port=9222 \\
638
- --user-data-dir="$USER_DATA_DIR" \\
639
- --no-first-run \\
640
- --no-default-browser-check \\
641
- --disable-component-extensions-with-background-pages \\
642
- --disable-background-networking \\
643
- --disable-sync \\
644
- --metrics-recording-only \\
645
- --disable-default-apps \\
646
- --disable-session-crashed-bubble \\
647
- --disable-restore-session-state \\
648
- --headless=new \\
649
- --no-sandbox \\
650
- --disable-setuid-sandbox \\
651
- --disable-gpu \\
652
- --disable-dev-shm-usage \\
653
- "file://$LOADING_DIR/loading.html" &
654
- PID=$!
655
- echo " Chrome PID: $PID"
656
- sleep 3
657
- echo ""
658
-
659
- echo "4. Checking if Chrome is still running..."
660
- if ps -p $PID > /dev/null 2>&1; then
661
- echo " Chrome is RUNNING after 3s"
662
- echo ""
663
- echo "5. Trying CDP (note: d3k doesn't use --remote-debugging-address)..."
664
- echo " Trying 127.0.0.1..."
665
- curl -s --max-time 5 http://127.0.0.1:9222/json/version 2>&1 || echo " 127.0.0.1 failed"
666
- echo ""
667
- echo " Trying localhost..."
668
- curl -s --max-time 5 http://localhost:9222/json/version 2>&1 || echo " localhost failed"
669
- echo ""
670
- echo "6. Checking what's listening on 9222..."
671
- ss -tlnp 2>/dev/null | grep 9222 || netstat -tlnp 2>/dev/null | grep 9222 || echo " Could not check listening ports"
672
- echo ""
673
- echo "7. Killing test Chrome..."
674
- kill $PID 2>/dev/null
675
- else
676
- echo " Chrome DIED within 3s"
677
- wait $PID 2>/dev/null
678
- EXIT_CODE=$?
679
- echo " Exit code: $EXIT_CODE"
680
- echo ""
681
- echo " Checking for crash logs..."
682
- ls -la "$USER_DATA_DIR" 2>&1 | head -10 || echo " No user-data-dir"
683
- fi
684
- echo ""
685
- echo "=== End d3k exact command test ==="
686
- `
687
- const chromeTest = await runSandboxCommand(sandboxResult.sandbox, "bash", ["-c", chromeTestScript])
688
- console.log(`[Step 0] d3k Chrome test (exit ${chromeTest.exitCode}):\n${chromeTest.stdout || "(no output)"}`)
689
- if (chromeTest.stderr) console.log(`[Step 0] d3k Chrome test stderr: ${chromeTest.stderr}`)
690
- } catch (error) {
691
- console.log(`[Step 0] d3k Chrome test error: ${error instanceof Error ? error.message : String(error)}`)
692
- }
693
- console.log(`[Step 0] ===== END d3k EXACT COMMAND TEST =====`)
694
-
695
- // Capture "BEFORE" screenshot - this shows the app before any fixes
696
- console.log(`[Step 0] Capturing BEFORE screenshot...`)
697
- let beforeScreenshotUrl: string | null = null
698
- try {
699
- const beforeBase64 = await captureScreenshotInSandbox(
700
- sandboxResult.sandbox,
701
- "http://localhost:3000",
702
- chromiumPath,
703
- "before"
704
- )
705
- if (beforeBase64) {
706
- beforeScreenshotUrl = await uploadScreenshot(beforeBase64, "before", projectName)
707
- }
708
- } catch (error) {
709
- console.log(`[Step 0] Before screenshot failed: ${error instanceof Error ? error.message : String(error)}`)
710
- }
711
-
712
- // Now capture CLS and errors using MCP from INSIDE the sandbox
713
- console.log(`[Step 0] Capturing CLS metrics from inside sandbox...`)
714
-
715
- let clsData: unknown = null
716
- let mcpError: string | null = null
717
-
718
- try {
719
- // Call fix_my_app MCP tool via curl from inside the sandbox
720
- // This avoids network isolation issues - we're calling localhost:3684 from within the sandbox
721
- const mcpCommand = `curl -s -X POST http://localhost:3684/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"fix_my_app","arguments":{"mode":"snapshot","focusArea":"performance","returnRawData":true}}}'`
722
-
723
- console.log(`[Step 0] Executing MCP command inside sandbox...`)
724
- console.log(`[Step 0] MCP command: ${mcpCommand.substring(0, 200)}...`)
725
-
726
- let stdout = ""
727
- let stderr = ""
728
- let exitCode = -1
729
-
730
- try {
731
- const result = await runSandboxCommand(sandboxResult.sandbox, "bash", ["-c", mcpCommand])
732
- stdout = result.stdout
733
- stderr = result.stderr
734
- exitCode = result.exitCode
735
- console.log(`[Step 0] MCP command exit code: ${exitCode}`)
736
- console.log(`[Step 0] MCP stdout length: ${stdout.length} bytes`)
737
- if (stderr) {
738
- console.log(`[Step 0] MCP stderr: ${stderr.substring(0, 500)}`)
739
- }
740
- } catch (runCommandError) {
741
- const errorMsg = runCommandError instanceof Error ? runCommandError.message : String(runCommandError)
742
- console.log(`[Step 0] sandbox.runCommand threw: ${errorMsg}`)
743
- mcpError = `sandbox.runCommand failed: ${errorMsg}`
744
- }
745
-
746
- if (exitCode === 0 && stdout) {
747
- try {
748
- const mcpResponse = JSON.parse(stdout)
749
- if (mcpResponse.result?.content) {
750
- // Extract the actual data from MCP response
751
- const contentArray = mcpResponse.result.content
752
- for (const item of contentArray) {
753
- if (item.type === "text" && item.text) {
754
- // Try to parse the text as JSON if it contains structured data
755
- try {
756
- clsData = JSON.parse(item.text)
757
- console.log(`[Step 0] Successfully parsed CLS data as JSON`)
758
- } catch {
759
- // If not JSON, treat as plain text
760
- clsData = { rawOutput: item.text }
761
- console.log(`[Step 0] CLS data stored as rawOutput (not JSON)`)
762
- }
763
- break
764
- }
765
- }
766
- }
767
-
768
- if (clsData) {
769
- console.log(`[Step 0] CLS data captured:`, JSON.stringify(clsData).substring(0, 500))
770
- } else {
771
- console.log(`[Step 0] No CLS data extracted from MCP response`)
772
- console.log(`[Step 0] Response structure: ${JSON.stringify(mcpResponse).substring(0, 500)}`)
773
- }
774
- } catch (parseError) {
775
- mcpError = `Failed to parse MCP response: ${parseError instanceof Error ? parseError.message : String(parseError)}`
776
- console.log(`[Step 0] ${mcpError}`)
777
- console.log(`[Step 0] Raw stdout: ${stdout.substring(0, 1000)}`)
778
- // Use raw stdout as fallback CLS data so Step 1 doesn't hang
779
- clsData = { rawMcpOutput: stdout.substring(0, 10000), parseError: mcpError }
780
- console.log(`[Step 0] Using raw stdout as fallback CLS data`)
781
- }
782
- } else if (exitCode !== 0 && !mcpError) {
783
- mcpError = `MCP command failed with exit code ${exitCode}`
784
- console.log(`[Step 0] ${mcpError}`)
785
- if (stderr) {
786
- console.log(`[Step 0] stderr: ${stderr}`)
787
- }
788
- }
789
- } catch (error) {
790
- mcpError = `MCP execution error: ${error instanceof Error ? error.message : String(error)}`
791
- console.log(`[Step 0] ${mcpError}`)
792
- }
793
-
794
- // IMPORTANT: Ensure clsData is ALWAYS set to something truthy so Step 1 doesn't hang on timeouts
795
- // Even if MCP failed, we should have sandbox logs that Step 1 can use
796
- if (!clsData) {
797
- console.log(`[Step 0] WARNING: No CLS data captured, creating placeholder to prevent Step 1 timeout`)
798
- clsData = {
799
- warning: "MCP fix_my_app did not return data",
800
- mcpError: mcpError || "Unknown error",
801
- sandboxDevUrl: sandboxResult.devUrl,
802
- sandboxMcpUrl: sandboxResult.mcpUrl
803
- }
804
- }
805
- console.log(`[Step 0] Final clsData truthy check: ${!!clsData}`)
806
-
807
- // Dump all sandbox logs before returning for debugging
808
- console.log(`[Step 0] === Dumping sandbox logs before returning ===`)
809
- try {
810
- const logsResult = await runSandboxCommand(sandboxResult.sandbox, "sh", [
811
- "-c",
812
- 'for log in /home/vercel-sandbox/.d3k/logs/*.log; do [ -f "$log" ] && echo "=== $log ===" && tail -100 "$log" || true; done 2>/dev/null || echo "No log files found"'
813
- ])
814
- console.log(logsResult.stdout)
815
- } catch (logsError) {
816
- console.log(`[Step 0] Failed to dump logs: ${logsError instanceof Error ? logsError.message : String(logsError)}`)
817
- }
818
- console.log(`[Step 0] === End sandbox log dump ===`)
819
-
820
- // Capture git diff from sandbox - this shows any changes made by d3k
821
- console.log(`[Step 0] Capturing git diff from sandbox...`)
822
- let gitDiff: string | null = null
823
- try {
824
- const diffResult = await runSandboxCommand(sandboxResult.sandbox, "sh", [
825
- "-c",
826
- "cd /vercel/sandbox && git diff --no-color 2>/dev/null || echo 'No git diff available'"
827
- ])
828
- if (diffResult.exitCode === 0 && diffResult.stdout.trim() && diffResult.stdout.trim() !== "No git diff available") {
829
- gitDiff = diffResult.stdout.trim()
830
- console.log(`[Step 0] Git diff captured (${gitDiff.length} chars)`)
831
- console.log(`[Step 0] Git diff preview:\n${gitDiff.substring(0, 500)}...`)
832
- } else {
833
- console.log(`[Step 0] No git changes detected in sandbox`)
834
- }
835
- } catch (diffError) {
836
- console.log(
837
- `[Step 0] Failed to capture git diff: ${diffError instanceof Error ? diffError.message : String(diffError)}`
838
- )
839
- }
840
-
841
- // Fetch d3k artifacts (CLS screenshots, full logs, metadata) BEFORE sandbox terminates
842
- console.log(`[Step 0] Fetching d3k artifacts from sandbox...`)
843
- const d3kArtifacts = await fetchAndUploadD3kArtifacts(sandboxResult.sandbox, sandboxResult.mcpUrl, projectName)
844
-
845
- // Save initial report to blob storage immediately
846
- // This ensures we capture CLS data, screenshots, and logs even if later steps fail
847
- const reportId = runId || `report-${Date.now()}`
848
- const timestamp = new Date().toISOString()
849
-
850
- // Extract CLS data from d3kArtifacts metadata for the initial report
851
- let clsScore: number | undefined
852
- let clsGrade: "good" | "needs-improvement" | "poor" | undefined
853
- let layoutShifts:
854
- | Array<{
855
- score: number
856
- timestamp: number
857
- elements: string[]
858
- }>
859
- | undefined
860
-
861
- if (d3kArtifacts?.metadata) {
862
- const meta = d3kArtifacts.metadata as {
863
- totalCLS?: number
864
- clsGrade?: string
865
- layoutShifts?: Array<{
866
- score: number
867
- timestamp: number
868
- sources?: Array<{ node?: string }>
869
- }>
870
- }
871
- clsScore = meta.totalCLS
872
- if (meta.clsGrade === "good" || meta.clsGrade === "needs-improvement" || meta.clsGrade === "poor") {
873
- clsGrade = meta.clsGrade
874
- }
875
- if (meta.layoutShifts) {
876
- layoutShifts = meta.layoutShifts.map((shift) => ({
877
- score: shift.score,
878
- timestamp: shift.timestamp,
879
- elements: shift.sources?.map((s) => s.node || "unknown").filter(Boolean) || []
880
- }))
881
- }
882
- }
883
-
884
- // Build and save initial report with all data captured so far
885
- const initialReport: Partial<WorkflowReport> & { id: string; projectName: string; timestamp: string } = {
886
- id: reportId,
887
- projectName,
888
- timestamp,
889
- sandboxDevUrl: sandboxResult.devUrl,
890
- sandboxMcpUrl: sandboxResult.mcpUrl,
891
- clsScore,
892
- clsGrade,
893
- layoutShifts,
894
- beforeScreenshotUrl: beforeScreenshotUrl || undefined,
895
- clsScreenshots: d3kArtifacts?.clsScreenshots?.map((s) => ({
896
- timestamp: s.timestamp,
897
- blobUrl: s.blobUrl,
898
- label: s.label
899
- })),
900
- d3kLogs: d3kArtifacts?.fullLogs || undefined,
901
- // Placeholder for agent analysis - will be filled below
902
- agentAnalysis: "Analysis in progress..."
903
- }
904
-
905
- console.log(`[Step 0] Saving initial report to blob storage...`)
906
- console.log(`[Step 0] Report ID: ${reportId}`)
907
- console.log(`[Step 0] CLS Score: ${clsScore ?? "not captured"}`)
908
- console.log(`[Step 0] CLS Screenshots: ${initialReport.clsScreenshots?.length ?? 0}`)
909
- if (initialReport.d3kLogs) {
910
- console.log(`[Step 0] d3k logs: ${initialReport.d3kLogs.length} chars`)
911
- }
912
-
913
- const reportBlobUrl = await saveReportToBlob(initialReport)
914
- console.log(`[Step 0] Initial report saved: ${reportBlobUrl}`)
915
-
916
- // Run AI agent analysis while we still have sandbox access
917
- // This allows the agent to read/write files and use d3k MCP tools
918
- console.log(`[Step 0] Running AI agent with sandbox tools...`)
919
- let agentAnalysis: string | null = null
920
- try {
921
- const logAnalysis = clsData ? JSON.stringify(clsData, null, 2) : "No CLS data captured"
922
- agentAnalysis = await runAgentWithSandboxTools(
923
- sandboxResult.sandbox,
924
- sandboxResult.mcpUrl,
925
- sandboxResult.devUrl,
926
- logAnalysis
927
- )
928
- console.log(`[Step 0] Agent analysis completed (${agentAnalysis.length} chars)`)
929
-
930
- // Update report with agent analysis
931
- initialReport.agentAnalysis = agentAnalysis
932
- initialReport.agentAnalysisModel = "anthropic/claude-sonnet-4-20250514"
933
- await saveReportToBlob(initialReport)
934
- console.log(`[Step 0] Report updated with agent analysis`)
935
-
936
- // Capture git diff after agent made changes
937
- const diffResult = await runSandboxCommand(sandboxResult.sandbox, "sh", [
938
- "-c",
939
- "cd /vercel/sandbox && git diff --no-color 2>/dev/null || echo 'No git diff available'"
940
- ])
941
- if (diffResult.exitCode === 0 && diffResult.stdout.trim() && diffResult.stdout.trim() !== "No git diff available") {
942
- gitDiff = diffResult.stdout.trim()
943
- console.log(`[Step 0] Agent made changes - git diff captured (${gitDiff.length} chars)`)
944
- }
945
- } catch (agentError) {
946
- console.log(
947
- `[Step 0] Agent analysis failed: ${agentError instanceof Error ? agentError.message : String(agentError)}`
948
- )
949
- agentAnalysis = `Agent analysis failed: ${agentError instanceof Error ? agentError.message : String(agentError)}`
950
- }
951
-
952
- // Note: We cannot return the cleanup function or sandbox object as they're not serializable
953
- // Sandbox cleanup will happen automatically when the sandbox times out
954
- return {
955
- mcpUrl: sandboxResult.mcpUrl,
956
- devUrl: sandboxResult.devUrl,
957
- bypassToken: sandboxResult.bypassToken,
958
- clsData,
959
- mcpError,
960
- beforeScreenshotUrl,
961
- chromiumPath,
962
- gitDiff,
963
- d3kArtifacts,
964
- reportId,
965
- reportBlobUrl,
966
- agentAnalysis
967
- }
968
- }
969
-
970
- /**
971
- * Run AI agent with sandbox tools
972
- * This is called from within Step 0 while we have sandbox access
973
- */
974
- async function runAgentWithSandboxTools(
975
- sandbox: Sandbox,
976
- mcpUrl: string,
977
- devUrl: string,
978
- logAnalysis: string
979
- ): Promise<string> {
980
- console.log("[Agent] Starting AI agent with d3k sandbox tools...")
981
-
982
- // Create AI Gateway instance
983
- const gateway = createGateway({
984
- apiKey: process.env.AI_GATEWAY_API_KEY,
985
- baseURL: "https://ai-gateway.vercel.sh/v1/ai"
986
- })
987
-
988
- const model = gateway("anthropic/claude-sonnet-4-20250514")
989
- const tools = createD3kSandboxTools(sandbox, mcpUrl)
990
-
991
- const systemPrompt = `You are a CLS (Cumulative Layout Shift) specialist engineer working with d3k, a development debugging tool. Your ONLY focus is fixing layout shift issues.
992
-
993
- ## TOOLS AVAILABLE
994
- You have access to tools to explore and modify the codebase:
995
- - **readFile**: Read source files to understand the code
996
- - **globSearch**: Find files by pattern (e.g., '*.tsx', '**/Header*')
997
- - **grepSearch**: Search for code patterns
998
- - **listDirectory**: Explore project structure
999
- - **findComponentSource**: d3k-specific tool to map DOM elements to React source files
1000
- - **writeFile**: Write fixes to files
1001
- - **getGitDiff**: Review changes you've made
1002
-
1003
- ## WORKFLOW
1004
- 1. First, understand the CLS issue from the diagnostic data
1005
- 2. Use findComponentSource or grepSearch to locate the source files
1006
- 3. Read the relevant files to understand the code
1007
- 4. Write fixes using writeFile
1008
- 5. Use getGitDiff to verify your changes
1009
- 6. Provide a summary of what you fixed
1010
-
1011
- ## CLS KNOWLEDGE
1012
-
1013
- CLS (Cumulative Layout Shift) measures visual stability. A good CLS score is 0.1 or less.
1014
-
1015
- ### What causes CLS:
1016
- 1. **Images without dimensions** - <img> tags missing width/height cause layout shifts when images load
1017
- 2. **Dynamic content insertion** - Content that appears after initial render
1018
- 3. **Web fonts causing FOIT/FOUT** - Text that shifts when custom fonts load
1019
- 4. **Async loaded components** - React components rendering after data fetches
1020
- 5. **Animations that trigger layout** - CSS animations affecting dimensions
1021
-
1022
- ### How to fix CLS:
1023
- 1. **Add width/height to images**: Always specify explicit dimensions or use aspect-ratio
1024
- 2. **Reserve space**: Use min-height, skeleton loaders, or CSS aspect-ratio
1025
- 3. **Use font-display**: Prevent font-related shifts with 'optional' or 'swap'
1026
- 4. **Suspense with sized fallbacks**: Wrap async components with properly sized placeholders
1027
- 5. **Use transform animations**: Prefer transform/opacity over dimension changes
1028
-
1029
- ## OUTPUT FORMAT
1030
-
1031
- After investigating and fixing, provide:
1032
-
1033
- ## Summary
1034
- [Brief description of what was found and fixed]
1035
-
1036
- ## CLS Score
1037
- [The measured score from diagnostics]
1038
-
1039
- ## Root Cause
1040
- [What element(s) caused the shift and why]
1041
-
1042
- ## Fix Applied
1043
- [What changes were made]
1044
-
1045
- ## Git Diff
1046
- \`\`\`diff
1047
- [Actual diff from getGitDiff]
1048
- \`\`\`
1049
-
1050
- ## RULES
1051
- 1. ONLY fix CLS/layout shift issues
1052
- 2. If CLS score is < 0.05, report "✅ NO CLS ISSUES - Score: [score]"
1053
- 3. Always read files before modifying them
1054
- 4. Make minimal, targeted fixes`
1055
-
1056
- const userPrompt = `The dev server is running at: ${devUrl}
1057
-
1058
- Here's the diagnostic data captured from the running application:
1059
- ${logAnalysis}
1060
-
1061
- Please investigate and fix any CLS issues.`
1062
-
1063
- const { text, steps } = await generateText({
1064
- model,
1065
- system: systemPrompt,
1066
- prompt: userPrompt,
1067
- tools,
1068
- stopWhen: stepCountIs(20) // Allow up to 20 tool call steps
1069
- })
1070
-
1071
- console.log(`[Agent] Completed in ${steps.length} step(s)`)
1072
- console.log(`[Agent] Final text length: ${text.length} chars`)
1073
- console.log(`[Agent] Text preview: ${text.substring(0, 200)}...`)
1074
-
1075
- // Log tool usage summary
1076
- const toolCalls = steps.flatMap((s) => s.toolCalls || [])
1077
- if (toolCalls.length > 0) {
1078
- const toolSummary = toolCalls.reduce(
1079
- (acc, tc) => {
1080
- acc[tc.toolName] = (acc[tc.toolName] || 0) + 1
1081
- return acc
1082
- },
1083
- {} as Record<string, number>
1084
- )
1085
- console.log(`[Agent] Tool usage: ${JSON.stringify(toolSummary)}`)
1086
- }
1087
-
1088
- // If text is very short, log warning
1089
- if (text.length < 100) {
1090
- console.log(`[Agent] WARNING: Agent returned very short text, may indicate tool-only response`)
1091
- }
1092
-
1093
- return text
1094
- }
1095
-
1096
- /**
1097
- * Step 1: Use browser automation to capture real errors
1098
- * Uses d3k MCP server in sandbox (if available) or AI Gateway for browser automation
1099
- */
1100
- export async function fetchRealLogs(
1101
- mcpUrlOrDevUrl: string,
1102
- bypassToken?: string,
1103
- sandboxDevUrl?: string,
1104
- clsData?: unknown,
1105
- mcpError?: string | null,
1106
- beforeScreenshotUrlFromStep0?: string | null
1107
- ) {
1108
- "use step"
1109
-
1110
- // Debug: Log what we received from Step 0
1111
- console.log(`[Step 1] Received clsData: ${clsData ? "truthy" : "falsy"}, type: ${typeof clsData}`)
1112
- if (clsData) {
1113
- console.log(`[Step 1] clsData preview: ${JSON.stringify(clsData).substring(0, 200)}`)
1114
- }
1115
-
1116
- // If we already have CLS data from Step 0, use it along with the screenshot
1117
- // This early return prevents the long MCP timeout delays
1118
- if (clsData) {
1119
- console.log("[Step 1] ✅ Using CLS data captured in Step 0 (skipping MCP calls)")
1120
- if (beforeScreenshotUrlFromStep0) {
1121
- console.log(`[Step 1] Before screenshot from Step 0: ${beforeScreenshotUrlFromStep0}`)
1122
- }
1123
- return { logAnalysis: JSON.stringify(clsData, null, 2), beforeScreenshotUrl: beforeScreenshotUrlFromStep0 || null }
1124
- }
1125
-
1126
- console.log("[Step 1] ⚠️ No CLS data from Step 0, will try MCP calls (may timeout)")
1127
-
1128
- // If there was an MCP error in Step 0, log it
1129
- if (mcpError) {
1130
- console.log(`[Step 1] Note: MCP error from Step 0: ${mcpError}`)
1131
- }
1132
-
1133
- // Determine if we're using sandbox MCP or direct dev URL
1134
- const isSandbox = !!sandboxDevUrl
1135
- const devUrl = sandboxDevUrl || mcpUrlOrDevUrl
1136
- const mcpUrl = isSandbox ? mcpUrlOrDevUrl : null
1137
-
1138
- console.log(`[Step 1] Fetching logs from: ${devUrl}`)
1139
- console.log(`[Step 1] Using sandbox: ${isSandbox ? "yes" : "no"}`)
1140
- if (mcpUrl) {
1141
- console.log(`[Step 1] MCP URL: ${mcpUrl}`)
1142
- }
1143
- console.log(`[Step 1] Bypass token: ${bypassToken ? "provided" : "not provided"}`)
1144
-
1145
- try {
1146
- // Construct URL with bypass token if provided
1147
- const urlWithBypass = bypassToken ? `${devUrl}?x-vercel-protection-bypass=${bypassToken}` : devUrl
1148
-
1149
- console.log(`[Step 1] Final URL: ${urlWithBypass.replace(bypassToken || "", "***")}`)
1150
-
1151
- if (isSandbox && mcpUrl) {
1152
- // Use d3k MCP server in sandbox - capture CLS metrics and errors
1153
- console.log("[Step 1] Using d3k MCP server to capture CLS metrics and errors...")
1154
-
1155
- // First, validate MCP server access and list available tools
1156
- // Use a 30-second timeout to avoid hanging the entire workflow
1157
- console.log("[Step 1] Validating d3k MCP server access...")
1158
- const validationController = new AbortController()
1159
- const validationTimeout = setTimeout(() => validationController.abort(), 30000)
1160
- try {
1161
- const toolsResponse = await fetch(`${mcpUrl}/mcp`, {
1162
- method: "POST",
1163
- headers: {
1164
- "Content-Type": "application/json",
1165
- Accept: "application/json, text/event-stream"
1166
- },
1167
- body: JSON.stringify({
1168
- jsonrpc: "2.0",
1169
- id: 0,
1170
- method: "tools/list"
1171
- }),
1172
- signal: validationController.signal
1173
- })
1174
- clearTimeout(validationTimeout)
1175
-
1176
- if (toolsResponse.ok) {
1177
- const toolsText = await toolsResponse.text()
1178
- try {
1179
- // Parse SSE response format: "event: message\ndata: {...}\n\n"
1180
- let toolsData = null
1181
- const lines = toolsText.split("\n")
1182
- for (const line of lines) {
1183
- if (line.startsWith("data: ")) {
1184
- try {
1185
- toolsData = JSON.parse(line.substring(6))
1186
- break
1187
- } catch {
1188
- // Continue to next line
1189
- }
1190
- }
1191
- }
1192
- // Fallback: try parsing the whole response as JSON (non-SSE format)
1193
- if (!toolsData) {
1194
- toolsData = JSON.parse(toolsText)
1195
- }
1196
-
1197
- const toolNames = toolsData.result?.tools?.map((t: { name: string }) => t.name) || []
1198
- console.log(`[Step 1] ✅ d3k MCP server accessible`)
1199
- console.log(`[Step 1] Available tools (${toolNames.length}): ${toolNames.join(", ")}`)
1200
-
1201
- // Check for expected chrome-devtools and nextjs-dev tools
1202
- const hasChrome = toolNames.some((name: string) => name.includes("chrome-devtools"))
1203
- const hasNextjs = toolNames.some((name: string) => name.includes("nextjs"))
1204
- const hasFixMyApp = toolNames.includes("fix_my_app")
1205
-
1206
- console.log(`[Step 1] Chrome DevTools MCP: ${hasChrome ? "✅" : "❌"}`)
1207
- console.log(`[Step 1] Next.js DevTools MCP: ${hasNextjs ? "✅" : "❌"}`)
1208
- console.log(`[Step 1] fix_my_app tool: ${hasFixMyApp ? "✅" : "❌"}`)
1209
- } catch {
1210
- console.log(`[Step 1] MCP server responded but couldn't parse tools list: ${toolsText.substring(0, 200)}`)
1211
- }
1212
- } else {
1213
- console.log(`[Step 1] ⚠️ MCP server not accessible: ${toolsResponse.status}`)
1214
- }
1215
- } catch (error) {
1216
- clearTimeout(validationTimeout)
1217
- const errorMsg = error instanceof Error ? error.message : String(error)
1218
- const isTimeout = error instanceof Error && error.name === "AbortError"
1219
- console.log(`[Step 1] ⚠️ Failed to validate MCP server: ${isTimeout ? "Timed out after 30s" : errorMsg}`)
1220
- }
1221
-
1222
- // Navigate to the app to generate logs (with 30s timeout)
1223
- console.log("[Step 1] Navigating browser to app URL...")
1224
- const navController = new AbortController()
1225
- const navTimeout = setTimeout(() => navController.abort(), 30000)
1226
- try {
1227
- const navResponse = await fetch(`${mcpUrl}/mcp`, {
1228
- method: "POST",
1229
- headers: {
1230
- "Content-Type": "application/json",
1231
- Accept: "application/json, text/event-stream"
1232
- },
1233
- body: JSON.stringify({
1234
- jsonrpc: "2.0",
1235
- id: 0,
1236
- method: "tools/call",
1237
- params: {
1238
- name: "execute_browser_action",
1239
- arguments: {
1240
- action: "navigate",
1241
- params: { url: urlWithBypass }
1242
- }
1243
- }
1244
- }),
1245
- signal: navController.signal
1246
- })
1247
- clearTimeout(navTimeout)
1248
-
1249
- if (navResponse.ok) {
1250
- console.log("[Step 1] Browser navigation completed")
1251
- } else {
1252
- console.log(`[Step 1] Browser navigation failed: ${navResponse.status}`)
1253
- }
1254
- } catch (navError) {
1255
- clearTimeout(navTimeout)
1256
- const isTimeout = navError instanceof Error && navError.name === "AbortError"
1257
- console.log(
1258
- `[Step 1] Browser navigation error: ${isTimeout ? "Timed out after 30s" : navError instanceof Error ? navError.message : String(navError)}`
1259
- )
1260
- }
1261
-
1262
- // Wait for page to fully load
1263
- console.log("[Step 1] Waiting 5s for page load...")
1264
- await new Promise((resolve) => setTimeout(resolve, 5000))
1265
-
1266
- // Capture "before" screenshot to prove the page loaded and for later comparison
1267
- let beforeScreenshotUrl: string | null = null
1268
- console.log("[Step 1] Capturing 'before' screenshot...")
1269
- const screenshotController = new AbortController()
1270
- const screenshotTimeout = setTimeout(() => screenshotController.abort(), 30000)
1271
- try {
1272
- const screenshotResponse = await fetch(`${mcpUrl}/mcp`, {
1273
- method: "POST",
1274
- headers: {
1275
- "Content-Type": "application/json",
1276
- Accept: "application/json, text/event-stream"
1277
- },
1278
- body: JSON.stringify({
1279
- jsonrpc: "2.0",
1280
- id: 0,
1281
- method: "tools/call",
1282
- params: {
1283
- name: "chrome-devtools_take_snapshot",
1284
- arguments: {}
1285
- }
1286
- }),
1287
- signal: screenshotController.signal
1288
- })
1289
- clearTimeout(screenshotTimeout)
1290
-
1291
- if (screenshotResponse.ok) {
1292
- const screenshotText = await screenshotResponse.text()
1293
- // Parse SSE response to get screenshot data
1294
- const lines = screenshotText.split("\n")
1295
- for (const line of lines) {
1296
- if (line.startsWith("data: ")) {
1297
- try {
1298
- const json = JSON.parse(line.substring(6))
1299
- if (json.result?.content) {
1300
- for (const content of json.result.content) {
1301
- if (content.type === "image" && content.data) {
1302
- // Upload base64 image to Vercel Blob
1303
- const imageBuffer = Buffer.from(content.data, "base64")
1304
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
1305
- const filename = `screenshot-before-${timestamp}.png`
1306
- const blob = await put(filename, imageBuffer, {
1307
- access: "public",
1308
- contentType: "image/png"
1309
- })
1310
- beforeScreenshotUrl = blob.url
1311
- console.log(`[Step 1] ✅ Before screenshot uploaded: ${beforeScreenshotUrl}`)
1312
- }
1313
- }
1314
- }
1315
- } catch {
1316
- // Continue parsing other lines
1317
- }
1318
- }
1319
- }
1320
- if (!beforeScreenshotUrl) {
1321
- console.log(`[Step 1] Screenshot response received but no image data found`)
1322
- console.log(`[Step 1] Response preview: ${screenshotText.substring(0, 500)}`)
1323
- }
1324
- } else {
1325
- console.log(`[Step 1] Screenshot request failed: ${screenshotResponse.status}`)
1326
- }
1327
- } catch (error) {
1328
- clearTimeout(screenshotTimeout)
1329
- const isTimeout = error instanceof Error && error.name === "AbortError"
1330
- console.log(
1331
- `[Step 1] Screenshot capture error: ${isTimeout ? "Timed out after 30s" : error instanceof Error ? error.message : String(error)}`
1332
- )
1333
- }
1334
-
1335
- // Check d3k logs to see if it's capturing data (with 15s timeout)
1336
- console.log("[Step 1] Fetching d3k logs from sandbox to verify it's working...")
1337
- const logsController = new AbortController()
1338
- const logsTimeout = setTimeout(() => logsController.abort(), 15000)
1339
- try {
1340
- const logsResponse = await fetch(`${mcpUrl}/api/logs`, { signal: logsController.signal })
1341
- clearTimeout(logsTimeout)
1342
- if (logsResponse.ok) {
1343
- const logsText = await logsResponse.text()
1344
- console.log(`[Step 1] d3k logs (last 1000 chars):\n${logsText.slice(-1000)}`)
1345
- } else {
1346
- console.log(`[Step 1] Could not fetch d3k logs: ${logsResponse.status}`)
1347
- }
1348
- } catch (error) {
1349
- clearTimeout(logsTimeout)
1350
- const isTimeout = error instanceof Error && error.name === "AbortError"
1351
- console.log(
1352
- `[Step 1] Failed to fetch d3k logs: ${isTimeout ? "Timed out after 15s" : error instanceof Error ? error.message : String(error)}`
1353
- )
1354
- }
1355
-
1356
- // Call fix_my_app with focusArea='performance' to capture CLS and jank
1357
- console.log("[Step 1] Calling fix_my_app with focusArea='performance'...")
1358
-
1359
- // Set a 3-minute timeout for the MCP call
1360
- const controller = new AbortController()
1361
- const timeoutId = setTimeout(() => controller.abort(), 3 * 60 * 1000)
1362
-
1363
- try {
1364
- const mcpResponse = await fetch(`${mcpUrl}/mcp`, {
1365
- method: "POST",
1366
- headers: {
1367
- "Content-Type": "application/json",
1368
- Accept: "application/json, text/event-stream"
1369
- },
1370
- body: JSON.stringify({
1371
- jsonrpc: "2.0",
1372
- id: 1,
1373
- method: "tools/call",
1374
- params: {
1375
- name: "fix_my_app",
1376
- arguments: {
1377
- mode: "snapshot",
1378
- focusArea: "performance",
1379
- timeRangeMinutes: 5,
1380
- returnRawData: false
1381
- }
1382
- }
1383
- }),
1384
- signal: controller.signal
1385
- })
1386
-
1387
- clearTimeout(timeoutId)
1388
-
1389
- if (!mcpResponse.ok) {
1390
- throw new Error(`MCP request failed: ${mcpResponse.status}`)
1391
- }
1392
-
1393
- // Parse SSE response
1394
- const text = await mcpResponse.text()
1395
- console.log(`[Step 1] fix_my_app response length: ${text.length} bytes`)
1396
- console.log(`[Step 1] fix_my_app response preview (first 500 chars):\n${text.substring(0, 500)}`)
1397
-
1398
- const lines = text.split("\n")
1399
- console.log(`[Step 1] Response split into ${lines.length} lines`)
1400
-
1401
- let logAnalysis = ""
1402
- let linesProcessed = 0
1403
- let contentBlocks = 0
1404
-
1405
- for (const line of lines) {
1406
- if (line.startsWith("data: ")) {
1407
- linesProcessed++
1408
- try {
1409
- const json = JSON.parse(line.substring(6))
1410
- console.log(`[Step 1] Parsed JSON line ${linesProcessed}:`, JSON.stringify(json).substring(0, 200))
1411
-
1412
- if (json.result?.content) {
1413
- for (const content of json.result.content) {
1414
- if (content.type === "text") {
1415
- contentBlocks++
1416
- logAnalysis += content.text
1417
- console.log(`[Step 1] Added text content block ${contentBlocks}, length: ${content.text.length}`)
1418
- }
1419
- }
1420
- } else if (json.error) {
1421
- console.log(`[Step 1] ERROR in response: ${JSON.stringify(json.error)}`)
1422
- }
1423
- } catch (error) {
1424
- console.log(
1425
- `[Step 1] Failed to parse JSON line ${linesProcessed}: ${error instanceof Error ? error.message : String(error)}`
1426
- )
1427
- console.log(`[Step 1] Problem line: ${line.substring(0, 200)}`)
1428
- }
1429
- }
1430
- }
1431
-
1432
- console.log(`[Step 1] Processed ${linesProcessed} data lines, ${contentBlocks} content blocks`)
1433
- console.log(`[Step 1] Got ${logAnalysis.length} chars from fix_my_app (performance analysis)`)
1434
-
1435
- if (logAnalysis.length === 0) {
1436
- console.log(`[Step 1] WARNING: fix_my_app returned NO data. Full response:\n${text}`)
1437
- }
1438
-
1439
- return {
1440
- logAnalysis: `d3k Performance Analysis for ${devUrl}\n\n${logAnalysis}`,
1441
- beforeScreenshotUrl
1442
- }
1443
- } catch (error) {
1444
- clearTimeout(timeoutId)
1445
-
1446
- if (error instanceof Error && error.name === "AbortError") {
1447
- console.log("[Step 1] fix_my_app timed out after 3 minutes, using fallback method")
1448
- // Fall through to fallback method below
1449
- } else {
1450
- console.log(`[Step 1] fix_my_app error: ${error instanceof Error ? error.message : String(error)}`)
1451
- // Fall through to fallback method below
1452
- }
1453
- }
1454
- }
1455
-
1456
- // Fallback: Use AI Gateway with browser automation prompting
1457
- console.log("[Step 1] Using AI Gateway with browser automation...")
1458
- const gateway = createGateway({
1459
- apiKey: process.env.AI_GATEWAY_API_KEY,
1460
- baseURL: "https://ai-gateway.vercel.sh/v1/ai"
1461
- })
1462
-
1463
- const model = gateway("anthropic/claude-sonnet-4-20250514")
1464
-
1465
- const prompt = `You are a web application debugger with access to browser automation tools via Playwright MCP.
1466
-
1467
- Your task is to visit this URL and capture any errors, warnings, or issues:
1468
- ${urlWithBypass}
1469
-
1470
- Steps to follow:
1471
- 1. Use browser_eval with action="start" to start the browser
1472
- 2. Use browser_eval with action="navigate" and params={url: "${urlWithBypass}"} to navigate to the page
1473
- 3. Wait a few seconds for the page to fully load and JavaScript to execute
1474
- 4. Use browser_eval with action="console_messages" to get all browser console output (errors, warnings, logs)
1475
- 5. Use browser_eval with action="screenshot" to capture a screenshot
1476
- 6. Use browser_eval with action="close" to close the browser
1477
-
1478
- Analyze the console messages and provide a detailed report including:
1479
- - All console errors (with full stack traces if available)
1480
- - All console warnings
1481
- - HTTP status codes or network errors
1482
- - Any visual issues you can identify from the screenshot
1483
- - Screenshot URL if captured
1484
-
1485
- Format your response as a clear, structured report that helps identify what's broken in the application.`
1486
-
1487
- const { text } = await generateText({
1488
- model,
1489
- prompt,
1490
- toolChoice: "auto",
1491
- // @ts-expect-error - AI SDK types for maxTokens are incomplete
1492
- maxTokens: 4000
1493
- })
1494
-
1495
- console.log(`[Step 1] Browser automation response (first 500 chars): ${text.substring(0, 500)}...`)
1496
- return { logAnalysis: `Browser Automation Analysis for ${devUrl}\n\n${text}`, beforeScreenshotUrl: null }
1497
- } catch (error) {
1498
- console.error("[Step 1] Error with browser automation:", error)
1499
-
1500
- // Fallback to simple fetch if browser automation fails
1501
- console.log("[Step 1] Falling back to simple HTTP fetch...")
1502
- try {
1503
- const urlWithBypass = bypassToken ? `${devUrl}?x-vercel-protection-bypass=${bypassToken}` : devUrl
1504
- const headers: HeadersInit = {
1505
- "User-Agent": "dev3000-cloud-fix/1.0",
1506
- Accept: "text/html,application/json,*/*"
1507
- }
1508
- if (bypassToken) {
1509
- headers["x-vercel-protection-bypass"] = bypassToken
1510
- }
1511
-
1512
- const response = await fetch(urlWithBypass, { method: "GET", headers })
1513
- const body = await response.text()
1514
-
1515
- // Extract and log page title from HTML
1516
- const titleMatch = body.match(/<title[^>]*>([^<]*)<\/title>/i)
1517
- const pageTitle = titleMatch ? titleMatch[1].trim() : "(no title found)"
1518
- console.log(`[Step 1] HTTP fallback - Page title: "${pageTitle}"`)
1519
-
1520
- let logAnalysis = `Dev Server URL: ${devUrl}\n`
1521
- logAnalysis += `Page Title: ${pageTitle}\n`
1522
- logAnalysis += `HTTP Status: ${response.status} ${response.statusText}\n\n`
1523
- logAnalysis += `Note: Browser automation failed, using fallback HTTP fetch.\n\n`
1524
-
1525
- if (!response.ok) {
1526
- logAnalysis += `ERROR: HTTP ${response.status} ${response.statusText}\n\n`
1527
- }
1528
-
1529
- if (body.includes("ReferenceError") || body.includes("Error") || body.includes("error")) {
1530
- logAnalysis += `Response body contains error information:\n${body.substring(0, 5000)}\n\n`
1531
- } else if (!response.ok) {
1532
- logAnalysis += `Response body:\n${body.substring(0, 2000)}\n\n`
1533
- } else {
1534
- logAnalysis += "No errors detected in response.\n"
1535
- }
1536
-
1537
- return { logAnalysis, beforeScreenshotUrl: null }
1538
- } catch (fallbackError) {
1539
- const errorMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError)
1540
- return {
1541
- logAnalysis: `Failed to fetch logs from ${devUrl}\n\nError: ${errorMessage}\n\nThis may indicate the dev server is not accessible or has crashed.`,
1542
- beforeScreenshotUrl: null
1543
- }
1544
- }
1545
- }
1546
- }
1547
-
1548
- /**
1549
- * Step 2: Invoke AI agent to analyze logs and propose fixes
1550
- * Uses AI SDK with AI Gateway + d3k sandbox tools for code access
1551
- *
1552
- * When sandbox is provided, the agent can:
1553
- * - Read files to understand the codebase
1554
- * - Search for relevant code with glob/grep
1555
- * - Find component sources via d3k MCP
1556
- * - Write fixes directly to the sandbox
1557
- * - Get git diff of changes
1558
- */
1559
- export async function analyzeLogsWithAgent(logAnalysis: string, devUrl: string, sandbox?: Sandbox, mcpUrl?: string) {
1560
- "use step"
1561
-
1562
- console.log("[Step 2] Invoking AI agent to analyze logs...")
1563
- console.log(`[Step 2] Sandbox available: ${!!sandbox}`)
1564
- console.log(`[Step 2] MCP URL: ${mcpUrl || "not provided"}`)
1565
-
1566
- // Create AI Gateway instance
1567
- const gateway = createGateway({
1568
- apiKey: process.env.AI_GATEWAY_API_KEY,
1569
- baseURL: "https://ai-gateway.vercel.sh/v1/ai"
1570
- })
1571
-
1572
- // Use Claude Sonnet 4 via AI Gateway
1573
- const model = gateway("anthropic/claude-sonnet-4-20250514")
1574
-
1575
- // Create d3k sandbox tools if sandbox is available
1576
- const tools = sandbox && mcpUrl ? createD3kSandboxTools(sandbox, mcpUrl) : undefined
1577
-
1578
- const systemPrompt = `You are a CLS (Cumulative Layout Shift) specialist engineer working with d3k, a development debugging tool. Your ONLY focus is fixing layout shift issues.
1579
-
1580
- ${
1581
- tools
1582
- ? `## TOOLS AVAILABLE
1583
- You have access to tools to explore and modify the codebase:
1584
- - **readFile**: Read source files to understand the code
1585
- - **globSearch**: Find files by pattern (e.g., '*.tsx', '**/Header*')
1586
- - **grepSearch**: Search for code patterns
1587
- - **listDirectory**: Explore project structure
1588
- - **findComponentSource**: d3k-specific tool to map DOM elements to React source files
1589
- - **writeFile**: Write fixes to files
1590
- - **getGitDiff**: Review changes you've made
1591
-
1592
- ## WORKFLOW
1593
- 1. First, understand the CLS issue from the diagnostic data
1594
- 2. Use findComponentSource or grepSearch to locate the source files
1595
- 3. Read the relevant files to understand the code
1596
- 4. Write fixes using writeFile
1597
- 5. Use getGitDiff to verify your changes
1598
- 6. Provide a summary of what you fixed`
1599
- : `## LIMITED MODE
1600
- No sandbox access - you can only analyze the diagnostic data and propose fixes.
1601
- You cannot read or modify the actual source code.`
1602
- }
1603
-
1604
- ## CLS KNOWLEDGE
1605
-
1606
- CLS (Cumulative Layout Shift) measures visual stability. A good CLS score is 0.1 or less.
1607
-
1608
- ### What causes CLS:
1609
- 1. **Images without dimensions** - <img> tags missing width/height cause layout shifts when images load
1610
- 2. **Dynamic content insertion** - Content that appears after initial render
1611
- 3. **Web fonts causing FOIT/FOUT** - Text that shifts when custom fonts load
1612
- 4. **Async loaded components** - React components rendering after data fetches
1613
- 5. **Animations that trigger layout** - CSS animations affecting dimensions
1614
-
1615
- ### How to fix CLS:
1616
- 1. **Add width/height to images**: Always specify explicit dimensions or use aspect-ratio
1617
- 2. **Reserve space**: Use min-height, skeleton loaders, or CSS aspect-ratio
1618
- 3. **Use font-display**: Prevent font-related shifts with 'optional' or 'swap'
1619
- 4. **Suspense with sized fallbacks**: Wrap async components with properly sized placeholders
1620
- 5. **Use transform animations**: Prefer transform/opacity over dimension changes
1621
-
1622
- ## OUTPUT FORMAT
1623
-
1624
- After investigating and fixing (if tools available), provide:
1625
-
1626
- ## Summary
1627
- [Brief description of what was found and fixed]
1628
-
1629
- ## CLS Score
1630
- [The measured score from diagnostics]
1631
-
1632
- ## Root Cause
1633
- [What element(s) caused the shift and why]
1634
-
1635
- ## Fix Applied
1636
- [What changes were made, or proposed changes if no sandbox access]
1637
-
1638
- ## Git Diff
1639
- \`\`\`diff
1640
- [Actual diff from getGitDiff, or proposed diff if no sandbox]
1641
- \`\`\`
1642
-
1643
- ## RULES
1644
- 1. ONLY fix CLS/layout shift issues
1645
- 2. If CLS score is < 0.05, report "✅ NO CLS ISSUES - Score: [score]"
1646
- 3. Always read files before modifying them
1647
- 4. Make minimal, targeted fixes`
1648
-
1649
- const userPrompt = `The dev server is running at: ${devUrl}
1650
-
1651
- Here's the diagnostic data captured from the running application:
1652
- ${logAnalysis}
1653
-
1654
- Please investigate and fix any CLS issues.`
1655
-
1656
- if (tools) {
1657
- // Agentic mode with tools
1658
- console.log("[Step 2] Running in agentic mode with d3k sandbox tools...")
1659
-
1660
- const { text, steps } = await generateText({
1661
- model,
1662
- system: systemPrompt,
1663
- prompt: userPrompt,
1664
- tools,
1665
- stopWhen: stepCountIs(20) // Allow up to 20 tool call steps
1666
- })
1667
-
1668
- console.log(`[Step 2] Agent completed in ${steps.length} step(s)`)
1669
- console.log(`[Step 2] AI agent response (first 500 chars): ${text.substring(0, 500)}...`)
1670
-
1671
- // Log tool usage summary
1672
- const toolCalls = steps.flatMap((s) => s.toolCalls || [])
1673
- if (toolCalls.length > 0) {
1674
- const toolSummary = toolCalls.reduce(
1675
- (acc, tc) => {
1676
- acc[tc.toolName] = (acc[tc.toolName] || 0) + 1
1677
- return acc
1678
- },
1679
- {} as Record<string, number>
1680
- )
1681
- console.log(`[Step 2] Tool usage: ${JSON.stringify(toolSummary)}`)
1682
- }
1683
-
1684
- return text
1685
- } else {
1686
- // Non-agentic fallback (no sandbox)
1687
- console.log("[Step 2] Running in limited mode (no sandbox access)...")
1688
-
1689
- const { text } = await generateText({
1690
- model,
1691
- system: systemPrompt,
1692
- prompt: userPrompt
1693
- })
1694
-
1695
- console.log(`[Step 2] AI agent response (first 500 chars): ${text.substring(0, 500)}...`)
1696
-
1697
- return text
1698
- }
1699
- }
1700
-
1701
- /**
1702
- * Step 3: Update the report with AI agent analysis
1703
- * The initial report was saved in Step 0 with CLS data, screenshots, and logs
1704
- * This step updates it with the agent's fix proposal
1705
- */
1706
- export async function uploadToBlob(
1707
- fixProposal: string,
1708
- projectName: string,
1709
- _logAnalysis: string,
1710
- sandboxDevUrl: string,
1711
- beforeScreenshotUrl?: string | null,
1712
- _gitDiff?: string | null,
1713
- d3kArtifacts?: {
1714
- clsScreenshots: Array<{ label: string; blobUrl: string; timestamp: number }>
1715
- screencastSessionId: string | null
1716
- fullLogs: string | null
1717
- metadata: Record<string, unknown> | null
1718
- },
1719
- runId?: string,
1720
- sandboxMcpUrl?: string,
1721
- agentAnalysisModel?: string
1722
- ) {
1723
- "use step"
1724
-
1725
- const reportId = runId || `report-${Date.now()}`
1726
- console.log(`[Step 3] Updating report ${reportId} with agent analysis...`)
1727
-
1728
- // Extract CLS data from d3kArtifacts metadata
1729
- let clsScore: number | undefined
1730
- let clsGrade: "good" | "needs-improvement" | "poor" | undefined
1731
- let layoutShifts:
1732
- | Array<{
1733
- score: number
1734
- timestamp: number
1735
- elements: string[]
1736
- }>
1737
- | undefined
1738
-
1739
- if (d3kArtifacts?.metadata) {
1740
- const meta = d3kArtifacts.metadata as {
1741
- totalCLS?: number
1742
- clsGrade?: string
1743
- layoutShifts?: Array<{
1744
- score: number
1745
- timestamp: number
1746
- sources?: Array<{ node?: string }>
1747
- }>
1748
- }
1749
- clsScore = meta.totalCLS
1750
- if (meta.clsGrade === "good" || meta.clsGrade === "needs-improvement" || meta.clsGrade === "poor") {
1751
- clsGrade = meta.clsGrade
1752
- }
1753
- if (meta.layoutShifts) {
1754
- layoutShifts = meta.layoutShifts.map((shift) => ({
1755
- score: shift.score,
1756
- timestamp: shift.timestamp,
1757
- elements: shift.sources?.map((s) => s.node || "unknown").filter(Boolean) || []
1758
- }))
1759
- }
1760
- }
1761
-
1762
- // Build the complete report with agent analysis
1763
- // This overwrites the initial report saved in Step 0, adding the agent analysis
1764
- const report: Partial<WorkflowReport> & { id: string; projectName: string; timestamp: string } = {
1765
- id: reportId,
1766
- projectName,
1767
- timestamp: new Date().toISOString(),
1768
- sandboxDevUrl,
1769
- sandboxMcpUrl: sandboxMcpUrl || undefined,
1770
- clsScore,
1771
- clsGrade,
1772
- layoutShifts,
1773
- beforeScreenshotUrl: beforeScreenshotUrl || undefined,
1774
- clsScreenshots: d3kArtifacts?.clsScreenshots?.map((s) => ({
1775
- timestamp: s.timestamp,
1776
- blobUrl: s.blobUrl,
1777
- label: s.label
1778
- })),
1779
- agentAnalysis: fixProposal,
1780
- agentAnalysisModel: agentAnalysisModel || undefined,
1781
- d3kLogs: d3kArtifacts?.fullLogs || undefined
1782
- }
1783
-
1784
- // Log what we're saving
1785
- console.log(`[Step 3] Agent analysis length: ${fixProposal.length} chars`)
1786
- console.log(`[Step 3] Agent model: ${report.agentAnalysisModel ?? "not specified"}`)
1787
-
1788
- // Save updated report (overwrites the initial report from Step 0)
1789
- const blobUrl = await saveReportToBlob(report)
1790
- console.log(`[Step 3] Report updated: ${blobUrl}`)
1791
-
1792
- return {
1793
- success: true,
1794
- projectName,
1795
- fixProposal,
1796
- blobUrl,
1797
- beforeScreenshotUrl: beforeScreenshotUrl || null,
1798
- message: "Fix analysis completed and report updated"
1799
- }
1800
- }
1801
-
1802
- /**
1803
- * Step 4: Create GitHub PR with the fix
1804
- * Uses GitHub API to create a branch, commit the patch, and open a PR
1805
- */
1806
- export async function createGitHubPR(
1807
- fixProposal: string,
1808
- blobUrl: string,
1809
- repoOwner: string,
1810
- repoName: string,
1811
- baseBranch: string,
1812
- projectName: string
1813
- ) {
1814
- "use step"
1815
-
1816
- console.log(`[Step 4] Creating GitHub PR for ${repoOwner}/${repoName}...`)
1817
-
1818
- const githubToken = process.env.GITHUB_TOKEN
1819
- if (!githubToken) {
1820
- console.error("[Step 4] GITHUB_TOKEN not found in environment")
1821
- return {
1822
- success: false,
1823
- error: "GitHub token not configured"
1824
- }
1825
- }
1826
-
1827
- try {
1828
- // Extract the git patch from the fix proposal
1829
- const patchMatch = fixProposal.match(/```diff\n([\s\S]*?)\n```/)
1830
- if (!patchMatch) {
1831
- console.error("[Step 4] No git patch found in fix proposal")
1832
- return {
1833
- success: false,
1834
- error: "No git patch found in fix proposal"
1835
- }
1836
- }
1837
-
1838
- const patch = patchMatch[1]
1839
- console.log(`[Step 4] Extracted patch (${patch.length} chars)`)
1840
-
1841
- // Parse the patch to extract file changes
1842
- const fileChanges = parsePatchToFileChanges(patch)
1843
- if (fileChanges.length === 0) {
1844
- console.error("[Step 4] Failed to parse any file changes from patch")
1845
- return {
1846
- success: false,
1847
- error: "Failed to parse file changes from patch"
1848
- }
1849
- }
1850
-
1851
- console.log(`[Step 4] Parsed ${fileChanges.length} file change(s)`)
1852
-
1853
- // Create a unique branch name
1854
- const branchName = `dev3000-fix-${projectName}-${Date.now()}`
1855
- console.log(`[Step 4] Branch name: ${branchName}`)
1856
-
1857
- // Get the base branch SHA
1858
- const baseRef = await fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/git/ref/heads/${baseBranch}`, {
1859
- headers: {
1860
- Authorization: `Bearer ${githubToken}`,
1861
- Accept: "application/vnd.github.v3+json"
1862
- }
1863
- })
1864
-
1865
- if (!baseRef.ok) {
1866
- const error = await baseRef.text()
1867
- console.error(`[Step 4] Failed to get base branch: ${error}`)
1868
- return {
1869
- success: false,
1870
- error: `Failed to get base branch: ${baseRef.status}`
1871
- }
1872
- }
1873
-
1874
- const baseData = await baseRef.json()
1875
- const baseSha = baseData.object.sha
1876
- console.log(`[Step 4] Base SHA: ${baseSha}`)
1877
-
1878
- // Create new branch
1879
- const createBranch = await fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/git/refs`, {
1880
- method: "POST",
1881
- headers: {
1882
- Authorization: `Bearer ${githubToken}`,
1883
- Accept: "application/vnd.github.v3+json",
1884
- "Content-Type": "application/json"
1885
- },
1886
- body: JSON.stringify({
1887
- ref: `refs/heads/${branchName}`,
1888
- sha: baseSha
1889
- })
1890
- })
1891
-
1892
- if (!createBranch.ok) {
1893
- const error = await createBranch.text()
1894
- console.error(`[Step 4] Failed to create branch: ${error}`)
1895
- return {
1896
- success: false,
1897
- error: `Failed to create branch: ${createBranch.status}`
1898
- }
1899
- }
1900
-
1901
- console.log(`[Step 4] Created branch: ${branchName}`)
1902
-
1903
- // For each file, fetch current content, apply changes, and commit
1904
- for (const fileChange of fileChanges) {
1905
- console.log(`[Step 4] Processing file: ${fileChange.path}`)
1906
-
1907
- // Get current file content
1908
- const fileResp = await fetch(
1909
- `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${fileChange.path}?ref=${branchName}`,
1910
- {
1911
- headers: {
1912
- Authorization: `Bearer ${githubToken}`,
1913
- Accept: "application/vnd.github.v3+json"
1914
- }
1915
- }
1916
- )
1917
-
1918
- let currentContent = ""
1919
- let currentSha = ""
1920
-
1921
- if (fileResp.ok) {
1922
- const fileData = await fileResp.json()
1923
- currentSha = fileData.sha
1924
- currentContent = Buffer.from(fileData.content, "base64").toString("utf-8")
1925
- } else {
1926
- console.log(`[Step 4] File doesn't exist, will create new file`)
1927
- }
1928
-
1929
- // Apply the patch changes to the content
1930
- const newContent = applyPatchChanges(currentContent, fileChange.changes)
1931
-
1932
- // Update file
1933
- const updateFile = await fetch(
1934
- `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${fileChange.path}`,
1935
- {
1936
- method: "PUT",
1937
- headers: {
1938
- Authorization: `Bearer ${githubToken}`,
1939
- Accept: "application/vnd.github.v3+json",
1940
- "Content-Type": "application/json"
1941
- },
1942
- body: JSON.stringify({
1943
- message: `Fix: Apply dev3000 fix for ${projectName}`,
1944
- content: Buffer.from(newContent).toString("base64"),
1945
- branch: branchName,
1946
- ...(currentSha && { sha: currentSha })
1947
- })
1948
- }
1949
- )
1950
-
1951
- if (!updateFile.ok) {
1952
- const error = await updateFile.text()
1953
- console.error(`[Step 4] Failed to update file ${fileChange.path}: ${error}`)
1954
- return {
1955
- success: false,
1956
- error: `Failed to update file ${fileChange.path}: ${updateFile.status}`
1957
- }
1958
- }
1959
-
1960
- console.log(`[Step 4] Updated file: ${fileChange.path}`)
1961
- }
1962
-
1963
- // Create PR
1964
- const prBody = `## Automated Fix Proposal
1965
-
1966
- This PR was automatically generated by [dev3000](https://github.com/vercel-labs/dev3000) after analyzing your application.
1967
-
1968
- ### Fix Details
1969
- View the full analysis: [${blobUrl}](${blobUrl})
1970
-
1971
- ${fixProposal}
1972
-
1973
- ---
1974
-
1975
- 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1976
-
1977
- Co-Authored-By: Claude (dev3000) <noreply@anthropic.com>`
1978
-
1979
- const createPR = await fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/pulls`, {
1980
- method: "POST",
1981
- headers: {
1982
- Authorization: `Bearer ${githubToken}`,
1983
- Accept: "application/vnd.github.v3+json",
1984
- "Content-Type": "application/json"
1985
- },
1986
- body: JSON.stringify({
1987
- title: `Fix: ${projectName} - Automated fix from dev3000`,
1988
- head: branchName,
1989
- base: baseBranch,
1990
- body: prBody
1991
- })
1992
- })
1993
-
1994
- if (!createPR.ok) {
1995
- const error = await createPR.text()
1996
- console.error(`[Step 4] Failed to create PR: ${error}`)
1997
- return {
1998
- success: false,
1999
- error: `Failed to create PR: ${createPR.status}`
2000
- }
2001
- }
2002
-
2003
- const prData = await createPR.json()
2004
- console.log(`[Step 4] Created PR: ${prData.html_url}`)
2005
-
2006
- return {
2007
- success: true,
2008
- prUrl: prData.html_url,
2009
- prNumber: prData.number,
2010
- branch: branchName
2011
- }
2012
- } catch (error) {
2013
- console.error("[Step 4] Error creating PR:", error)
2014
- return {
2015
- success: false,
2016
- error: error instanceof Error ? error.message : String(error)
2017
- }
2018
- }
2019
- }
2020
-
2021
- /**
2022
- * Parse a git patch into file changes
2023
- */
2024
- function parsePatchToFileChanges(patch: string) {
2025
- const fileChanges: Array<{ path: string; changes: string }> = []
2026
- const files = patch.split(/diff --git /).filter(Boolean)
2027
-
2028
- for (const file of files) {
2029
- const lines = file.split("\n")
2030
- const pathMatch = lines[0].match(/a\/(.*?) b\//)
2031
- if (!pathMatch) continue
2032
-
2033
- const path = pathMatch[1]
2034
- const changes = lines.slice(1).join("\n")
2035
- fileChanges.push({ path, changes })
2036
- }
2037
-
2038
- return fileChanges
2039
- }
2040
-
2041
- /**
2042
- * Apply patch changes to file content
2043
- * This is a simplified implementation - may need enhancement for complex patches
2044
- */
2045
- function applyPatchChanges(content: string, changes: string): string {
2046
- const lines = content.split("\n")
2047
- const changeLines = changes.split("\n")
2048
-
2049
- let currentLine = 0
2050
- const result: string[] = []
2051
-
2052
- for (const change of changeLines) {
2053
- if (change.startsWith("@@")) {
2054
- // Parse hunk header to get line number
2055
- const match = change.match(/@@ -(\d+)/)
2056
- if (match) {
2057
- currentLine = Number.parseInt(match[1], 10) - 1
2058
- }
2059
- } else if (change.startsWith("-")) {
2060
- // Remove line
2061
- currentLine++
2062
- } else if (change.startsWith("+")) {
2063
- // Add line
2064
- result.push(change.substring(1))
2065
- } else if (change.startsWith(" ")) {
2066
- // Context line
2067
- if (currentLine < lines.length) {
2068
- result.push(lines[currentLine])
2069
- currentLine++
2070
- }
2071
- }
2072
- }
2073
-
2074
- return result.join("\n")
2075
- }
2076
-
2077
- /**
2078
- * Cleanup step: Stop the sandbox
2079
- */
2080
- export async function cleanupSandbox(cleanup: () => Promise<void>) {
2081
- "use step"
2082
-
2083
- console.log("[Cleanup] Stopping sandbox...")
2084
- try {
2085
- await cleanup()
2086
- console.log("[Cleanup] Sandbox stopped successfully")
2087
- } catch (error) {
2088
- console.error("[Cleanup] Error stopping sandbox:", error)
2089
- // Don't throw - cleanup errors shouldn't fail the workflow
2090
- }
2091
- }