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 };
  }
}