Spaces:
Build error
Build error
File size: 7,416 Bytes
75fefa7 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
// 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 };
}
}
|