Spaces:
Build error
Build error
| // Using direct fetch to Morph's OpenAI-compatible API to avoid SDK type issues | |
| export interface MorphEditBlock { | |
| targetFile: string; | |
| instructions: string; | |
| update: string; | |
| } | |
| export interface MorphApplyResult { | |
| success: boolean; | |
| normalizedPath?: string; | |
| mergedCode?: string; | |
| error?: string; | |
| } | |
| // Normalize project-relative paths to sandbox layout | |
| export function normalizeProjectPath(inputPath: string): { normalizedPath: string; fullPath: string } { | |
| let normalizedPath = inputPath.trim(); | |
| if (normalizedPath.startsWith('/')) normalizedPath = normalizedPath.slice(1); | |
| const configFiles = new Set([ | |
| 'tailwind.config.js', | |
| 'vite.config.js', | |
| 'package.json', | |
| 'package-lock.json', | |
| 'tsconfig.json', | |
| 'postcss.config.js' | |
| ]); | |
| const fileName = normalizedPath.split('/').pop() || ''; | |
| if (!normalizedPath.startsWith('src/') && | |
| !normalizedPath.startsWith('public/') && | |
| normalizedPath !== 'index.html' && | |
| !configFiles.has(fileName)) { | |
| normalizedPath = 'src/' + normalizedPath; | |
| } | |
| const fullPath = `/home/user/app/${normalizedPath}`; | |
| return { normalizedPath, fullPath }; | |
| } | |
| async function morphChatCompletionsCreate(payload: any) { | |
| if (!process.env.MORPH_API_KEY) throw new Error('MORPH_API_KEY is not set'); | |
| const res = await fetch('https://api.morphllm.com/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${process.env.MORPH_API_KEY}` | |
| }, | |
| body: JSON.stringify(payload) | |
| }); | |
| if (!res.ok) { | |
| const text = await res.text(); | |
| throw new Error(`Morph API error ${res.status}: ${text}`); | |
| } | |
| return res.json(); | |
| } | |
| // Parse <edit> blocks from LLM output | |
| export function parseMorphEdits(text: string): MorphEditBlock[] { | |
| const edits: MorphEditBlock[] = []; | |
| const editRegex = /<edit\s+target_file="([^"]+)">([\s\S]*?)<\/edit>/g; | |
| let match: RegExpExecArray | null; | |
| while ((match = editRegex.exec(text)) !== null) { | |
| const targetFile = match[1].trim(); | |
| const inner = match[2]; | |
| const instrMatch = inner.match(/<instructions>([\s\S]*?)<\/instructions>/); | |
| const updateMatch = inner.match(/<update>([\s\S]*?)<\/update>/); | |
| const instructions = instrMatch ? instrMatch[1].trim() : ''; | |
| const update = updateMatch ? updateMatch[1].trim() : ''; | |
| if (targetFile && update) { | |
| edits.push({ targetFile, instructions, update }); | |
| } | |
| } | |
| return edits; | |
| } | |
| // Read a file from sandbox: prefers cache, then sandbox.files, then commands.run("cat ...") | |
| async function readFileFromSandbox(sandbox: any, normalizedPath: string, fullPath: string): Promise<string> { | |
| // Try backend cache first | |
| if ((global as any).sandboxState?.fileCache?.files?.[normalizedPath]?.content) { | |
| return (global as any).sandboxState.fileCache.files[normalizedPath].content as string; | |
| } | |
| // Try E2B files API | |
| if (sandbox?.files?.read) { | |
| return await sandbox.files.read(fullPath); | |
| } | |
| // Try provider runCommand (preferred for provider pattern) | |
| if (typeof sandbox?.runCommand === 'function') { | |
| try { | |
| const res = await sandbox.runCommand(`cat ${normalizedPath}`); | |
| if (res && typeof res.stdout === 'string') { | |
| return res.stdout as string; | |
| } | |
| } catch {} | |
| // fallback to absolute path | |
| try { | |
| const resAbs = await sandbox.runCommand(`cat ${fullPath}`); | |
| if (resAbs && typeof resAbs.stdout === 'string') { | |
| return resAbs.stdout as string; | |
| } | |
| } catch {} | |
| } | |
| // Try shell cat via commands.run | |
| if (sandbox?.commands?.run) { | |
| const result = await sandbox.commands.run(`cat ${fullPath}`, { cwd: '/home/user/app', timeout: 30 }); | |
| if (result?.exitCode === 0 && typeof result?.stdout === 'string') { | |
| return result.stdout as string; | |
| } | |
| } | |
| throw new Error(`Unable to read file: ${normalizedPath}`); | |
| } | |
| // Write a file to sandbox and update cache | |
| async function writeFileToSandbox(sandbox: any, normalizedPath: string, fullPath: string, content: string): Promise<void> { | |
| // Provider pattern (writeFile) | |
| if (typeof sandbox?.writeFile === 'function') { | |
| await sandbox.writeFile(normalizedPath, content); | |
| return; | |
| } | |
| // Provider pattern (runCommand redirect) | |
| if (typeof sandbox?.runCommand === 'function') { | |
| // Ensure directory exists | |
| const dir = normalizedPath.includes('/') ? normalizedPath.substring(0, normalizedPath.lastIndexOf('/')) : ''; | |
| if (dir) { | |
| try { await sandbox.runCommand(`mkdir -p ${dir}`); } catch {} | |
| } | |
| // Write via heredoc with proper escaping | |
| const heredoc = `bash -lc 'cat > ${normalizedPath} <<\"EOF\"\n${content.replace(/\\/g, '\\\\').replace(/\n/g, '\n').replace(/\$/g, '\$')}\nEOF'`; | |
| const result = await sandbox.runCommand(heredoc); | |
| if (result?.stdout || result?.stderr) { | |
| // no-op | |
| } | |
| return; | |
| } | |
| // Prefer E2B files API | |
| if (sandbox?.files?.write) { | |
| await sandbox.files.write(fullPath, content); | |
| } else if (sandbox?.runCode) { | |
| // Use Python to write safely | |
| const escaped = content | |
| .replace(/\\/g, '\\\\') | |
| .replace(/"""/g, '\"\"\"'); | |
| await sandbox.runCode(` | |
| import os | |
| os.makedirs(os.path.dirname("${fullPath}"), exist_ok=True) | |
| with open("${fullPath}", 'w') as f: | |
| f.write("""${escaped}""") | |
| print("WROTE:${fullPath}") | |
| `); | |
| } else if (sandbox?.commands?.run) { | |
| // Shell redirection (fallback) | |
| // Note: beware of special chars; this is a last-resort path | |
| const result = await sandbox.commands.run(`bash -lc 'mkdir -p "$(dirname "${fullPath}")" && cat > "${fullPath}" << \EOF\n${content}\nEOF'`, { cwd: '/home/user/app', timeout: 60 }); | |
| if (result?.exitCode !== 0) { | |
| throw new Error(`Failed to write file via shell: ${normalizedPath}`); | |
| } | |
| } else { | |
| throw new Error('No available method to write files to sandbox'); | |
| } | |
| // Update backend cache if available | |
| if ((global as any).sandboxState?.fileCache) { | |
| (global as any).sandboxState.fileCache.files[normalizedPath] = { | |
| content, | |
| lastModified: Date.now() | |
| }; | |
| } | |
| if ((global as any).existingFiles) { | |
| (global as any).existingFiles.add(normalizedPath); | |
| } | |
| } | |
| export async function applyMorphEditToFile(params: { | |
| sandbox: any; | |
| targetPath: string; | |
| instructions: string; | |
| updateSnippet: string; | |
| }): Promise<MorphApplyResult> { | |
| try { | |
| if (!process.env.MORPH_API_KEY) { | |
| return { success: false, error: 'MORPH_API_KEY not set' }; | |
| } | |
| const { normalizedPath, fullPath } = normalizeProjectPath(params.targetPath); | |
| // Read original code (existence validation happens here) | |
| const initialCode = await readFileFromSandbox(params.sandbox, normalizedPath, fullPath); | |
| const resp = await morphChatCompletionsCreate({ | |
| model: 'morph-v3-large', | |
| messages: [ | |
| { | |
| role: 'user', | |
| content: `<instruction>${params.instructions || ''}</instruction>\n<code>${initialCode}</code>\n<update>${params.updateSnippet}</update>` | |
| } | |
| ] | |
| }); | |
| const mergedCode = (resp as any)?.choices?.[0]?.message?.content || ''; | |
| if (!mergedCode) { | |
| return { success: false, error: 'Morph returned empty content', normalizedPath }; | |
| } | |
| await writeFileToSandbox(params.sandbox, normalizedPath, fullPath, mergedCode); | |
| return { success: true, normalizedPath, mergedCode }; | |
| } catch (error) { | |
| return { success: false, error: (error as Error).message }; | |
| } | |
| } | |