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