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.
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +1 -1
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +11 -0
- package/dist/dev-environment.js.map +1 -1
- package/dist/utils/log-filename.d.ts.map +1 -1
- package/dist/utils/log-filename.js +8 -3
- package/dist/utils/log-filename.js.map +1 -1
- package/mcp-server/app/mcp/route.ts +81 -7
- package/mcp-server/app/mcp/tools.ts +33 -3
- package/mcp-server/next-env.d.ts +1 -1
- package/mcp-server/package.json +0 -12
- package/package.json +6 -7
- package/mcp-server/.next/build/chunks/[root-of-the-server]__25374c4f._.js +0 -500
- package/mcp-server/.next/build/chunks/[root-of-the-server]__25374c4f._.js.map +0 -11
- package/mcp-server/.next/build/chunks/[root-of-the-server]__6e020478._.js +0 -441
- package/mcp-server/.next/build/chunks/[root-of-the-server]__6e020478._.js.map +0 -7
- package/mcp-server/.next/build/chunks/[root-of-the-server]__c438ef56._.js +0 -205
- package/mcp-server/.next/build/chunks/[root-of-the-server]__c438ef56._.js.map +0 -8
- package/mcp-server/.next/build/chunks/[root-of-the-server]__c7ae8543._.js +0 -500
- package/mcp-server/.next/build/chunks/[root-of-the-server]__c7ae8543._.js.map +0 -11
- package/mcp-server/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_80bff36f._.js +0 -13
- package/mcp-server/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_80bff36f._.js.map +0 -5
- package/mcp-server/.next/build/chunks/[turbopack-node]_transforms_webpack-loaders_ts_c84aa21a._.js +0 -12
- package/mcp-server/.next/build/chunks/[turbopack-node]_transforms_webpack-loaders_ts_c84aa21a._.js.map +0 -5
- package/mcp-server/.next/build/chunks/[turbopack]_runtime.js +0 -770
- package/mcp-server/.next/build/chunks/[turbopack]_runtime.js.map +0 -10
- package/mcp-server/.next/build/chunks/node_modules__pnpm_806d01c0._.js +0 -6758
- package/mcp-server/.next/build/chunks/node_modules__pnpm_806d01c0._.js.map +0 -47
- package/mcp-server/.next/build/package.json +0 -1
- package/mcp-server/.next/build/postcss.js +0 -6
- package/mcp-server/.next/build/postcss.js.map +0 -5
- package/mcp-server/.next/build/webpack-loaders.js +0 -6
- package/mcp-server/.next/build/webpack-loaders.js.map +0 -5
- package/mcp-server/.next/package.json +0 -1
- package/mcp-server/app/api/auth/authorize/route.ts +0 -52
- package/mcp-server/app/api/auth/callback/route.ts +0 -128
- package/mcp-server/app/api/auth/signout/route.ts +0 -34
- package/mcp-server/app/api/auth/token/route.ts +0 -16
- package/mcp-server/app/api/cloud/check-pr/route.ts +0 -53
- package/mcp-server/app/api/cloud/check-pr/steps.ts +0 -458
- package/mcp-server/app/api/cloud/check-pr/workflow.ts +0 -109
- package/mcp-server/app/api/cloud/fix-workflow/health/route.ts +0 -10
- package/mcp-server/app/api/cloud/fix-workflow/route.ts +0 -50
- package/mcp-server/app/api/cloud/fix-workflow/steps.ts +0 -2091
- package/mcp-server/app/api/cloud/fix-workflow/workflow.ts +0 -296
- package/mcp-server/app/api/cloud/start-fix/route.ts +0 -192
- package/mcp-server/app/api/integration/webhook/route.ts +0 -290
- package/mcp-server/app/api/projects/[projectId]/bypass-token/route.ts +0 -48
- package/mcp-server/app/api/projects/branches/route.ts +0 -115
- package/mcp-server/app/api/projects/check-protection/route.ts +0 -33
- package/mcp-server/app/api/projects/route.ts +0 -97
- package/mcp-server/app/api/workflows/route.ts +0 -105
- package/mcp-server/app/auth/error/page.tsx +0 -47
- package/mcp-server/app/signin/page.tsx +0 -37
- package/mcp-server/app/workflows/[id]/report/agent-analysis.tsx +0 -7
- package/mcp-server/app/workflows/[id]/report/page.tsx +0 -199
- package/mcp-server/app/workflows/new/new-workflow-client.tsx +0 -32
- package/mcp-server/app/workflows/new/page.tsx +0 -13
- package/mcp-server/app/workflows/new-workflow-modal.tsx +0 -973
- package/mcp-server/app/workflows/page.tsx +0 -16
- 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
|
-
}
|