added fast apply
if isEdit === true & Morph API Key is in .env: - Morph Fast Apply is activated.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
// 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 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> {
|
||||
// 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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user