scai
Version:
> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ❤️.
209 lines (200 loc) • 7.82 kB
JavaScript
import { generate } from "../../lib/generate.js";
import { cleanupModule } from "./cleanupModule.js";
import { logInputOutput } from "../../utils/promptLogHelper.js";
import { splitCodeIntoChunks, countTokens } from "../../utils/splitCodeIntoChunk.js";
const SINGLE_SHOT_TOKEN_LIMIT = 1800;
const CHUNK_TOKEN_LIMIT = 1500;
/**
* Remove leading/trailing markdown code fences.
*/
function stripCodeFences(text) {
const lines = text.split("\n");
while (lines.length && /^```/.test(lines[0].trim())) {
lines.shift();
}
while (lines.length && /^```/.test(lines[lines.length - 1].trim())) {
lines.pop();
}
return lines.join("\n");
}
/**
* Heuristic: decide whether chunk output looks unsafe / non-code.
*/
function isSuspiciousChunkOutput(text, originalChunk) {
const trimmed = text.trim();
if (!trimmed)
return true;
// Explanations or meta text
if (/here is|transformed|updated code|explanation/i.test(trimmed)) {
return true;
}
// JSON-ish output (we do NOT want JSON in chunk mode)
if (trimmed.startsWith("{") &&
trimmed.endsWith("}")) {
return true;
}
// Extremely short compared to original
if (trimmed.length < originalChunk.trim().length * 0.3) {
return true;
}
return false;
}
export const codeTransformModule = {
name: "codeTransform",
description: "Transforms a single file specified in the current plan step based on user instruction. " +
"Outputs full rewritten file content (materialized state).",
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) {
return { query, data: { files: [], errors: ["No context provided"] } };
}
const workingFiles = context.workingFiles ?? [];
const step = context.currentStep;
const targetFile = step?.targetFile;
if (!targetFile) {
return {
query,
data: { files: [], errors: ["No targetFile specified in current plan step"] },
};
}
const file = workingFiles.find(f => f.path === targetFile);
if (!file || typeof file.code !== "string") {
return {
query,
data: { files: [], errors: [`Target file not found or missing code: ${targetFile}`] },
};
}
const normalizedQuery = context.analysis?.intent?.normalizedQuery ?? query;
const outputs = [];
const perFileErrors = [];
const tokenCount = countTokens(file.code);
// =========================================================================
// 🔹 PATH 1 — SMALL FILE (JSON, strict)
// =========================================================================
if (tokenCount <= SINGLE_SHOT_TOKEN_LIMIT) {
const prompt = `
You are a precise code transformation assistant.
User instruction (normalized):
${normalizedQuery}
File to transform:
---\nFILE: ${file.path}\n${file.code}
Rules:
- You MUST apply the user instruction.
- You MUST return the FULL rewritten file content.
- Do NOT return diffs or partial snippets.
JSON schema:
{
"files": [
{
"filePath": "<path>",
"content": "<FULL rewritten file content>",
"notes": "<optional>"
}
],
"errors": []
}
`.trim();
try {
const llmResponse = await generate({ content: prompt, query });
const cleaned = await cleanupModule.run({
query,
content: llmResponse.data,
});
const structured = typeof cleaned.data === "object"
? cleaned.data
: JSON.parse(cleaned.data ?? "{}");
const out = Array.isArray(structured.files)
? structured.files.find((f) => f.filePath === file.path)
: null;
if (!out || typeof out.content !== "string" || !out.content.trim()) {
perFileErrors.push(`Model did not return full content for ${file.path}`);
}
else {
outputs.push({
filePath: file.path,
content: out.content,
notes: out.notes,
});
}
perFileErrors.push(...(structured.errors ?? []));
}
catch (err) {
return {
query,
data: { files: [], errors: [`LLM call or parsing failed: ${err.message}`] },
};
}
}
// =========================================================================
// 🔹 PATH 2 — LARGE FILE (chunked, raw text)
// =========================================================================
else {
const chunks = splitCodeIntoChunks(file.code, CHUNK_TOKEN_LIMIT);
const transformedChunks = [];
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const prompt = `
You are a precise code transformation assistant.
User instruction (normalized):
${normalizedQuery}
You are given ONE CHUNK of a larger file.
Rules:
- Apply the instruction ONLY if relevant to this chunk.
- If no change is needed, return the chunk UNCHANGED.
- Do NOT add or remove unrelated code.
- Do NOT reference other chunks.
- Return ONLY code. No JSON. No explanations. No markdown fences.
FILE: ${file.path}
CHUNK ${i + 1} / ${chunks.length}
---
${chunk}
`.trim();
try {
logInputOutput("chunks", "output", chunk);
const llmResponse = await generate({ content: prompt, query });
const raw = typeof llmResponse.data === "string"
? llmResponse.data
: String(llmResponse.data ?? "");
const stripped = stripCodeFences(raw);
if (isSuspiciousChunkOutput(stripped, chunk)) {
transformedChunks.push(chunk);
perFileErrors.push(`Chunk ${i + 1} suspicious output; original preserved.`);
}
else {
transformedChunks.push(stripped);
}
}
catch (err) {
transformedChunks.push(chunk);
perFileErrors.push(`Chunk ${i + 1} failed; original preserved. Error: ${err.message}`);
}
}
outputs.push({
filePath: file.path,
content: transformedChunks.join("\n"),
});
}
// =========================================================================
// 🔹 Persist execution artifacts
// =========================================================================
context.execution || (context.execution = {});
(_a = context.execution).codeTransformArtifacts || (_a.codeTransformArtifacts = { files: [] });
context.execution.codeTransformArtifacts.files.push(...outputs);
context.plan || (context.plan = {});
(_b = context.plan).touchedFiles || (_b.touchedFiles = []);
if (!context.plan.touchedFiles.includes(file.path)) {
context.plan.touchedFiles.push(file.path);
}
const output = {
query,
data: { files: outputs, errors: perFileErrors },
};
logInputOutput("codeTransform", "output", output.data);
return output;
},
};