// 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 blocks from LLM output export function parseMorphEdits(text: string): MorphEditBlock[] { const edits: MorphEditBlock[] = []; const editRegex = /([\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(/([\s\S]*?)<\/instructions>/); const updateMatch = inner.match(/([\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 { // 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 { // 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 { 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: `${params.instructions || ''}\n${initialCode}\n${params.updateSnippet}` } ] }); 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 }; } }