2327442b89
if isEdit === true & Morph API Key is in .env: - Morph Fast Apply is activated.
181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
// 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 };
|
|
}
|
|
}
|
|
|
|
|