UNPKG

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