UNPKG

scai

Version:

> **A local-first AI CLI for understanding, querying, and iterating on large codebases.** > **100% local • No token costs • No cloud • No prompt injection • Private by design**

177 lines (167 loc) 7.75 kB
import { generate } from "../../lib/generate.js"; import { logInputOutput } from "../../utils/promptLogHelper.js"; import { extractTaggedContent } from "../../utils/parseTaggedContent.js"; function previewContent(content, lines = 5) { const all = content.split("\n"); return { totalLines: all.length, head: all.slice(0, lines), tail: all.slice(-lines), }; } export const codeTransformModule = { name: "codeTransform", description: "Transforms a single file according to an ordered plan of steps, in a single pass.", groups: ["transform"], run: async (input) => { var _a, _b; const query = typeof input.query === "string" ? input.query : String(input.query ?? ""); const context = input.context; if (!context) { logInputOutput("[codeTransformModule] No context provided", "input", {}); return { query, data: { files: [], errors: ["No context provided"] } }; } const workingFiles = context.workingFiles ?? []; const currentStep = context.currentStep; logInputOutput("[codeTransformModule] full generated plan steps", "input", context.analysis?.planSuggestion?.plan?.steps?.map(s => s.id)); if (!currentStep) { logInputOutput("[codeTransformModule] No current step set", "input", {}); return { query, data: { files: [], errors: ["No current step set in context for code transformation"], }, }; } const targetFile = currentStep.targetFile; const file = workingFiles.find(f => f.path === targetFile); if (!file || typeof file.code !== "string") { logInputOutput(`[codeTransformModule] Target file not found or missing code: ${targetFile}`, "input", {}); return { query, data: { files: [], errors: [`Target file not found or missing code: ${targetFile}`], }, }; } // ------------------------------------------------------------------ // Execution state // ------------------------------------------------------------------ context.execution || (context.execution = {}); (_a = context.execution).codeTransformArtifacts || (_a.codeTransformArtifacts = { files: [] }); let currentContent = file.code; const previousArtifact = context.execution.codeTransformArtifacts.files.find(f => f.filePath === targetFile); if (previousArtifact?.content) { currentContent = previousArtifact.content; logInputOutput("[codeTransformModule] previous artifact", "input", { source: "previousArtifact", preview: previewContent(currentContent), }); } else { logInputOutput("[codeTransformModule] original file content", "input", { source: "originalFile", preview: previewContent(currentContent), }); } // ------------------------------------------------------------------ // Ordered steps for this file (single-pass) // ------------------------------------------------------------------ const allSteps = context.analysis?.planSuggestion?.plan?.steps ?? []; const fileSteps = allSteps.filter(s => s.targetFile === targetFile); logInputOutput("[codeTransformModule] ordered steps for file", "input", fileSteps.map(s => s.id)); if (!fileSteps.length) { logInputOutput("[codeTransformModule] No plan steps found for target file", "input", {}); return { query, data: { files: [], errors: [`No plan steps found for target file ${targetFile}`], }, }; } // ------------------------------------------------------------------ // Outline-based steps instead of ordered numbered steps // ------------------------------------------------------------------ const stepsOutline = fileSteps .map((s) => `- Action: ${s.action}\n Description: ${typeof s.description === "string" ? s.description : "No description provided"}\n Metadata: ${s.metadata ? JSON.stringify(s.metadata, null, 2) : "none"}`) .join("\n\n"); // ------------------------------------------------------------------ // Prompt // ------------------------------------------------------------------ const prompt = ` You are a precise code transformation assistant. You are given: - An authoritative file - An outline of requested changes Use the outline to update the file as appropriate. Focus on applying the requested changes. ==================== TRANSFORMATION OUTLINE ==================== ${stepsOutline} ==================== FILE (AUTHORITATIVE INPUT) ==================== FILE: ${file.path} -------------------------------------------------- ${currentContent} ==================== RULES ==================== - Apply all requested changes where appropriate. - You MUST return the FULL rewritten file content inside <FILE_CONTENT> tags. - You may provide optional notes or explanations inside <NOTES> tags. - Do NOT return JSON. - Do NOT return diffs or partial snippets. - If the outline results in no net change, return the original file content. Expected Output Format: <FILE_CONTENT> (The complete updated code goes here) </FILE_CONTENT> <NOTES> (Optional notes) </NOTES> `.trim(); logInputOutput("[codeTransformModule] final prompt to LLM", "output", prompt); const outputs = []; const perFileErrors = []; try { const llmResponse = await generate({ content: prompt, query }); const rawText = typeof llmResponse.data === "string" ? llmResponse.data : ""; // 🔑 Use the helper method for extraction const { content: extractedContent, notes } = extractTaggedContent(rawText, "FILE_CONTENT"); let finalContent = currentContent; if (extractedContent && extractedContent.length > 10) { finalContent = extractedContent; } else if (rawText) { perFileErrors.push("Could not find valid <FILE_CONTENT> tags or content was too short in LLM response."); } else { perFileErrors.push("LLM response data is empty or invalid."); } const out = { filePath: file.path, content: finalContent, notes: notes ?? undefined }; logInputOutput("[codeTransformModule] extracted final content preview", "output", previewContent(out.content)); outputs.push(out); // Update context artifacts context.execution.codeTransformArtifacts.files = context.execution.codeTransformArtifacts.files.filter(f => f.filePath !== file.path); context.execution.codeTransformArtifacts.files.push(out); context.plan || (context.plan = {}); (_b = context.plan).touchedFiles || (_b.touchedFiles = []); if (!context.plan.touchedFiles.includes(file.path)) { context.plan.touchedFiles.push(file.path); } } catch (err) { perFileErrors.push(`Transformation failed: ${err.message}`); } return { query, data: { files: outputs, errors: perFileErrors.length > 0 ? perFileErrors : undefined, }, }; }, };