scai 0.1.117 → 0.1.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/MainAgent.js +255 -0
- package/dist/agents/contextReviewStep.js +104 -0
- package/dist/agents/finalPlanGenStep.js +123 -0
- package/dist/agents/infoPlanGenStep.js +126 -0
- package/dist/agents/planGeneratorStep.js +118 -0
- package/dist/agents/planResolverStep.js +95 -0
- package/dist/agents/planTargetFilesStep.js +48 -0
- package/dist/agents/preFileSearchCheckStep.js +95 -0
- package/dist/agents/selectRelevantSourcesStep.js +100 -0
- package/dist/agents/semanticAnalysisStep.js +144 -0
- package/dist/agents/structuralAnalysisStep.js +46 -0
- package/dist/agents/transformPlanGenStep.js +107 -0
- package/dist/agents/understandIntentStep.js +72 -0
- package/dist/agents/validationAnalysisStep.js +87 -0
- package/dist/commands/AskCmd.js +47 -116
- package/dist/commands/ChangeLogUpdateCmd.js +11 -5
- package/dist/commands/CommitSuggesterCmd.js +50 -75
- package/dist/commands/DaemonCmd.js +119 -29
- package/dist/commands/IndexCmd.js +41 -24
- package/dist/commands/InspectCmd.js +0 -1
- package/dist/commands/ReadlineSingleton.js +18 -0
- package/dist/commands/ResetDbCmd.js +20 -21
- package/dist/commands/ReviewCmd.js +89 -54
- package/dist/commands/SummaryCmd.js +12 -18
- package/dist/commands/WorkflowCmd.js +41 -0
- package/dist/commands/factory.js +254 -0
- package/dist/config.js +67 -15
- package/dist/constants.js +20 -4
- package/dist/context.js +10 -11
- package/dist/daemon/daemonQueues.js +63 -0
- package/dist/daemon/daemonWorker.js +40 -63
- package/dist/daemon/generateSummaries.js +58 -0
- package/dist/daemon/runFolderCapsuleBatch.js +247 -0
- package/dist/daemon/runIndexingBatch.js +147 -0
- package/dist/daemon/runKgBatch.js +104 -0
- package/dist/db/fileIndex.js +168 -63
- package/dist/db/functionExtractors/extractFromJava.js +210 -6
- package/dist/db/functionExtractors/extractFromJs.js +173 -214
- package/dist/db/functionExtractors/extractFromTs.js +159 -160
- package/dist/db/functionExtractors/index.js +7 -5
- package/dist/db/schema.js +55 -20
- package/dist/db/sqlTemplates.js +50 -19
- package/dist/fileRules/builtins.js +31 -14
- package/dist/fileRules/codeAllowedExtensions.js +4 -0
- package/dist/fileRules/fileExceptions.js +0 -13
- package/dist/fileRules/ignoredExtensions.js +10 -0
- package/dist/index.js +128 -325
- package/dist/lib/generate.js +37 -14
- package/dist/lib/generateFolderCapsules.js +109 -0
- package/dist/lib/spinner.js +12 -5
- package/dist/modelSetup.js +0 -10
- package/dist/pipeline/modules/changeLogModule.js +16 -19
- package/dist/pipeline/modules/chunkManagerModule.js +24 -0
- package/dist/pipeline/modules/cleanupModule.js +96 -91
- package/dist/pipeline/modules/codeTransformModule.js +208 -0
- package/dist/pipeline/modules/commentModule.js +20 -11
- package/dist/pipeline/modules/commitSuggesterModule.js +36 -14
- package/dist/pipeline/modules/contextReviewModule.js +52 -0
- package/dist/pipeline/modules/fileReaderModule.js +72 -0
- package/dist/pipeline/modules/fileSearchModule.js +136 -0
- package/dist/pipeline/modules/finalAnswerModule.js +53 -0
- package/dist/pipeline/modules/gatherInfoModule.js +176 -0
- package/dist/pipeline/modules/generateTestsModule.js +63 -54
- package/dist/pipeline/modules/kgModule.js +26 -11
- package/dist/pipeline/modules/preserveCodeModule.js +91 -49
- package/dist/pipeline/modules/refactorModule.js +19 -7
- package/dist/pipeline/modules/repairTestsModule.js +44 -36
- package/dist/pipeline/modules/reviewModule.js +23 -13
- package/dist/pipeline/modules/summaryModule.js +27 -35
- package/dist/pipeline/modules/writeFileModule.js +86 -0
- package/dist/pipeline/registry/moduleRegistry.js +38 -93
- package/dist/pipeline/runModulePipeline.js +22 -19
- package/dist/scripts/dbcheck.js +143 -228
- package/dist/utils/buildContextualPrompt.js +245 -172
- package/dist/utils/debugContext.js +24 -0
- package/dist/utils/fileTree.js +16 -6
- package/dist/utils/loadRelevantFolderCapsules.js +64 -0
- package/dist/utils/log.js +2 -0
- package/dist/utils/normalizeData.js +23 -0
- package/dist/utils/planActions.js +60 -0
- package/dist/utils/promptBuilderHelper.js +67 -0
- package/dist/utils/promptLogHelper.js +52 -0
- package/dist/utils/sanitizeQuery.js +20 -8
- package/dist/utils/sleep.js +3 -0
- package/dist/utils/splitCodeIntoChunk.js +65 -32
- package/dist/utils/vscode.js +49 -0
- package/dist/workflow/workflowResolver.js +14 -0
- package/dist/workflow/workflowRunner.js +103 -0
- package/package.json +6 -5
- package/dist/agent/agentManager.js +0 -39
- package/dist/agent/workflowManager.js +0 -95
- package/dist/commands/ModulePipelineCmd.js +0 -31
- package/dist/daemon/daemonBatch.js +0 -186
- package/dist/fileRules/scoreFiles.js +0 -71
- package/dist/lib/generateEmbedding.js +0 -22
package/dist/lib/spinner.js
CHANGED
|
@@ -3,14 +3,19 @@ export class Spinner {
|
|
|
3
3
|
this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
4
4
|
this.interval = null;
|
|
5
5
|
this.i = 0;
|
|
6
|
-
|
|
6
|
+
if (message) {
|
|
7
|
+
this.text = message;
|
|
8
|
+
}
|
|
7
9
|
}
|
|
8
10
|
start() {
|
|
9
11
|
process.stdout.write('\x1b[?25l'); // hide cursor
|
|
10
|
-
|
|
12
|
+
if (this.text) {
|
|
13
|
+
process.stdout.write(` ${this.text}`);
|
|
14
|
+
}
|
|
11
15
|
this.interval = setInterval(() => {
|
|
12
|
-
|
|
13
|
-
process.stdout.write(
|
|
16
|
+
const text = this.text ?? '';
|
|
17
|
+
process.stdout.write('\r');
|
|
18
|
+
process.stdout.write(`${this.frames[this.i]} ${text}`);
|
|
14
19
|
this.i = (this.i + 1) % this.frames.length;
|
|
15
20
|
}, 80);
|
|
16
21
|
}
|
|
@@ -20,7 +25,9 @@ export class Spinner {
|
|
|
20
25
|
succeed(msg) {
|
|
21
26
|
this.stop();
|
|
22
27
|
process.stdout.write('\r'); // go to start
|
|
23
|
-
|
|
28
|
+
if (msg) {
|
|
29
|
+
console.log(`✅ ${msg}`);
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
fail(msg) {
|
|
26
33
|
this.stop();
|
package/dist/modelSetup.js
CHANGED
|
@@ -194,13 +194,3 @@ export async function bootstrap() {
|
|
|
194
194
|
await ensureModelsDownloaded();
|
|
195
195
|
await ensureVSCodeInstalled();
|
|
196
196
|
}
|
|
197
|
-
// 🔗 Helper: open file or diff in VSCode
|
|
198
|
-
export function openInVSCode(filePath, options) {
|
|
199
|
-
const args = options?.diffWith ? ['--diff', options.diffWith, filePath] : [filePath];
|
|
200
|
-
try {
|
|
201
|
-
spawn('code', args, { stdio: 'inherit' });
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
console.log(chalk.red('❌ Failed to launch VSCode CLI. Make sure it is installed and in PATH.'));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
// src/pipeline/modules/changeLogModule.ts
|
|
1
2
|
import { Config } from '../../config.js';
|
|
2
3
|
import { generate } from '../../lib/generate.js';
|
|
3
4
|
export const changelogModule = {
|
|
4
5
|
name: 'changelogModule',
|
|
5
|
-
description: 'Generates changelog entry based on Git diff',
|
|
6
|
+
description: 'Generates a changelog entry based on Git diff',
|
|
6
7
|
async run(input) {
|
|
7
8
|
const model = Config.getModel();
|
|
9
|
+
const diffContent = typeof input.content === 'string' ? input.content : '';
|
|
8
10
|
const prompt = `
|
|
9
11
|
Using the Git diff below, return **only meaningful user-facing changes** as clean markdown bullet points.
|
|
10
12
|
|
|
@@ -15,33 +17,28 @@ ONLY return the bullet points!
|
|
|
15
17
|
If no meaningful changes are present, return the text: "NO UPDATE".
|
|
16
18
|
|
|
17
19
|
--- DIFF START ---
|
|
18
|
-
${
|
|
20
|
+
${diffContent}
|
|
19
21
|
--- DIFF END ---
|
|
20
22
|
`.trim();
|
|
21
|
-
const response = await generate({
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const response = await generate({
|
|
24
|
+
query: input.query,
|
|
25
|
+
content: prompt,
|
|
26
|
+
});
|
|
27
|
+
const rawContent = typeof response?.data === 'string' ? response.data.trim() : '';
|
|
28
|
+
if (!rawContent || rawContent === 'NO UPDATE') {
|
|
25
29
|
console.log("⚠️ No meaningful updates found. Returning 'NO UPDATE'.");
|
|
26
30
|
return {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
suggestions: response?.suggestions ?? [],
|
|
30
|
-
filepath: input.filepath,
|
|
31
|
+
query: input.query,
|
|
32
|
+
data: 'NO UPDATE',
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
|
-
// Split
|
|
34
|
-
const lines =
|
|
35
|
-
// Filter out non-bullet lines (now including the '•' symbol)
|
|
35
|
+
// Split into lines and keep only bullet points
|
|
36
|
+
const lines = rawContent.split('\n').map(line => line.trim());
|
|
36
37
|
const bulletLines = lines.filter(line => /^([*-+•]|\d+\.)\s/.test(line));
|
|
37
|
-
// Join the filtered lines into the final changelog entry
|
|
38
38
|
const filtered = bulletLines.join('\n');
|
|
39
|
-
// Return the processed content and filtered changelog
|
|
40
39
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
suggestions: response?.suggestions ?? [],
|
|
44
|
-
filepath: input.filepath,
|
|
40
|
+
query: input.query,
|
|
41
|
+
data: filtered || 'NO UPDATE',
|
|
45
42
|
};
|
|
46
43
|
}
|
|
47
44
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/pipeline/modules/chunkManagerModule.ts
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { normalizePath } from '../../utils/contentUtils.js';
|
|
4
|
+
import { splitCodeIntoChunks } from '../../utils/splitCodeIntoChunk.js';
|
|
5
|
+
export const chunkManagerModule = {
|
|
6
|
+
name: "chunkManager",
|
|
7
|
+
description: "Splits large files into manageable chunks for processing.",
|
|
8
|
+
async run(input) {
|
|
9
|
+
const filepath = normalizePath(input.filepath ?? "");
|
|
10
|
+
const fileContent = fs.readFileSync(filepath, "utf-8");
|
|
11
|
+
const maxTokens = 1500;
|
|
12
|
+
const baseChunks = splitCodeIntoChunks(fileContent, maxTokens);
|
|
13
|
+
return {
|
|
14
|
+
content: fileContent,
|
|
15
|
+
data: {
|
|
16
|
+
baseChunks,
|
|
17
|
+
workingChunks: [...baseChunks],
|
|
18
|
+
chunkCount: baseChunks.length,
|
|
19
|
+
filepath,
|
|
20
|
+
},
|
|
21
|
+
filepath,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// File: src/modules/cleanupModule.ts
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
/** --- Helper: detect noise at top/bottom of content --- */
|
|
2
4
|
function isTopOrBottomNoise(line) {
|
|
3
5
|
const trimmed = line.trim();
|
|
4
6
|
if (/^```(?:\w+)?$/.test(trimmed))
|
|
@@ -6,7 +8,7 @@ function isTopOrBottomNoise(line) {
|
|
|
6
8
|
if (/^<!--.*-->$/.test(trimmed))
|
|
7
9
|
return true;
|
|
8
10
|
const lower = trimmed.toLowerCase();
|
|
9
|
-
if (!trimmed.startsWith(
|
|
11
|
+
if (!trimmed.startsWith("//") && !trimmed.startsWith("/*")) {
|
|
10
12
|
return [
|
|
11
13
|
/^i\s/i,
|
|
12
14
|
/^here/,
|
|
@@ -23,107 +25,110 @@ function isTopOrBottomNoise(line) {
|
|
|
23
25
|
/example/,
|
|
24
26
|
/summary/,
|
|
25
27
|
/added comments/,
|
|
26
|
-
].some(pattern => pattern.test(lower));
|
|
28
|
+
].some((pattern) => pattern.test(lower));
|
|
27
29
|
}
|
|
28
30
|
return false;
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
/** Extract first object slice { ... } */
|
|
33
|
+
function extractObject(text) {
|
|
34
|
+
const start = text.indexOf("{");
|
|
35
|
+
const end = text.lastIndexOf("}");
|
|
36
|
+
return start !== -1 && end !== -1 && end > start ? text.slice(start, end + 1) : null;
|
|
37
|
+
}
|
|
38
|
+
/** Extract first array slice [ ... ] */
|
|
39
|
+
function extractArray(text) {
|
|
40
|
+
const start = text.indexOf("[");
|
|
41
|
+
const end = text.lastIndexOf("]");
|
|
42
|
+
return start !== -1 && end !== -1 && end > start ? text.slice(start, end + 1) : null;
|
|
43
|
+
}
|
|
44
|
+
/** Try parsing with fallback slice logic */
|
|
45
|
+
function parseJsonWithFallback(content) {
|
|
46
|
+
const trimmed = content.trim();
|
|
47
|
+
// --- 1) JSON Array strategy ---
|
|
48
|
+
if (trimmed.startsWith("[")) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(trimmed);
|
|
43
51
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (/^```(?:\w+)?$/.test(lines[i].trim())) {
|
|
50
|
-
closingIndex = i;
|
|
51
|
-
break;
|
|
52
|
+
catch {
|
|
53
|
+
const slice = extractArray(trimmed);
|
|
54
|
+
if (slice) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(slice);
|
|
52
57
|
}
|
|
58
|
+
catch { }
|
|
53
59
|
}
|
|
54
|
-
if (closingIndex !== -1) {
|
|
55
|
-
console.log(chalk.red(`[cleanupModule] Found closing fenced block at line ${closingIndex + 1}, removing fence lines.`));
|
|
56
|
-
lines.splice(closingIndex, 1);
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
console.log(chalk.yellow(`[cleanupModule] No closing fenced block found, only removed opening fence.`));
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
// --- CLEAN BOTTOM ---
|
|
63
|
-
if (lines.length && /^```(?:\w+)?$/.test(lines[lines.length - 1].trim())) {
|
|
64
|
-
console.log(chalk.red(`[cleanupModule] Removing closing fenced block line at bottom.`));
|
|
65
|
-
lines.pop();
|
|
66
60
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
}
|
|
62
|
+
// --- 2) JSON Object strategy ---
|
|
63
|
+
if (trimmed.startsWith("{")) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(trimmed);
|
|
70
66
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
return true;
|
|
79
|
-
});
|
|
80
|
-
// --- FINAL CLEANUP: KEEP ONLY JSON LINES INSIDE BRACES ---
|
|
81
|
-
let jsonLines = [];
|
|
82
|
-
let braceDepth = 0;
|
|
83
|
-
let insideBraces = false;
|
|
84
|
-
for (let line of lines) {
|
|
85
|
-
const trimmed = line.trim();
|
|
86
|
-
// Detect start of JSON object/array
|
|
87
|
-
if (!insideBraces && (trimmed.startsWith('{') || trimmed.startsWith('['))) {
|
|
88
|
-
insideBraces = true;
|
|
89
|
-
}
|
|
90
|
-
if (insideBraces) {
|
|
91
|
-
// Track nested braces/brackets
|
|
92
|
-
for (const char of trimmed) {
|
|
93
|
-
if (char === '{' || char === '[')
|
|
94
|
-
braceDepth++;
|
|
95
|
-
if (char === '}' || char === ']')
|
|
96
|
-
braceDepth--;
|
|
97
|
-
}
|
|
98
|
-
// Skip lines that are clearly non-JSON inside braces
|
|
99
|
-
if (!trimmed.startsWith('//') && !/^\/\*/.test(trimmed) && trimmed !== '') {
|
|
100
|
-
jsonLines.push(line);
|
|
67
|
+
catch {
|
|
68
|
+
const slice = extractObject(trimmed);
|
|
69
|
+
if (slice) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(slice);
|
|
101
72
|
}
|
|
102
|
-
|
|
103
|
-
if (braceDepth === 0)
|
|
104
|
-
break;
|
|
73
|
+
catch { }
|
|
105
74
|
}
|
|
106
75
|
}
|
|
107
|
-
|
|
108
|
-
|
|
76
|
+
}
|
|
77
|
+
// --- 3) Fallback: if mixed text, extract first object ---
|
|
78
|
+
const fallbackObj = extractObject(content);
|
|
79
|
+
if (fallbackObj) {
|
|
109
80
|
try {
|
|
110
|
-
JSON.parse(
|
|
111
|
-
return { content: finalContent };
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
console.warn(chalk.yellow(`[cleanupModule] Initial JSON.parse failed:`), chalk.red(err instanceof Error ? err.message : err));
|
|
115
|
-
// Attempt simple cleanup: remove trailing commas before } or ]
|
|
116
|
-
finalContent = finalContent.replace(/,\s*([}\]])/g, '$1');
|
|
117
|
-
try {
|
|
118
|
-
JSON.parse(finalContent);
|
|
119
|
-
console.log(chalk.green(`[cleanupModule] Fixed JSON by removing trailing commas.`));
|
|
120
|
-
return { content: finalContent };
|
|
121
|
-
}
|
|
122
|
-
catch (err2) {
|
|
123
|
-
console.error(chalk.red(`[cleanupModule] JSON still invalid after cleanup:`), chalk.yellow(err2 instanceof Error ? err2.message : err2));
|
|
124
|
-
// Return best-effort content even if invalid, so downstream can log/debug
|
|
125
|
-
return { content: finalContent };
|
|
126
|
-
}
|
|
81
|
+
return JSON.parse(fallbackObj);
|
|
127
82
|
}
|
|
83
|
+
catch { }
|
|
128
84
|
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/** --- Module --- */
|
|
88
|
+
export const cleanupModule = {
|
|
89
|
+
name: "cleanup",
|
|
90
|
+
description: "Removes markdown fences, fluff, and reasoning text; extracts valid JSON if possible.",
|
|
91
|
+
groups: ["transform"],
|
|
92
|
+
async run(input) {
|
|
93
|
+
// --- Normalize input ---
|
|
94
|
+
let content = typeof input.content === "string"
|
|
95
|
+
? input.content
|
|
96
|
+
: JSON.stringify(input.content ?? "");
|
|
97
|
+
content = content.replace(/\r\n/g, "\n");
|
|
98
|
+
// --- Trim top/bottom noise ---
|
|
99
|
+
let lines = content.split("\n");
|
|
100
|
+
while (lines.length && (lines[0].trim() === "" || isTopOrBottomNoise(lines[0])))
|
|
101
|
+
lines.shift();
|
|
102
|
+
while (lines.length && (lines[lines.length - 1].trim() === "" || isTopOrBottomNoise(lines[lines.length - 1]))) {
|
|
103
|
+
lines.pop();
|
|
104
|
+
}
|
|
105
|
+
content = lines.join("\n");
|
|
106
|
+
// --- Strip markdown fences, comments, thinking tags ---
|
|
107
|
+
content = content
|
|
108
|
+
.replace(/```(?:json)?/gi, "")
|
|
109
|
+
.replace(/```/g, "")
|
|
110
|
+
.replace(/<!--.*?-->/gs, "")
|
|
111
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
112
|
+
.trim();
|
|
113
|
+
// If no JSON markers at all → treat as plain cleaned text
|
|
114
|
+
if (!content.includes("{") && !content.includes("[")) {
|
|
115
|
+
return { query: input.query, data: content };
|
|
116
|
+
}
|
|
117
|
+
// --- Parse JSON using simplified strategy ---
|
|
118
|
+
const parsed = parseJsonWithFallback(content);
|
|
119
|
+
if (parsed !== null) {
|
|
120
|
+
console.log(chalk.green("[cleanupModule] JSON parsed successfully."));
|
|
121
|
+
return {
|
|
122
|
+
query: input.query,
|
|
123
|
+
content,
|
|
124
|
+
data: parsed,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
console.warn(chalk.red("[cleanupModule] Failed to parse JSON — returning raw content."));
|
|
128
|
+
return {
|
|
129
|
+
query: input.query,
|
|
130
|
+
content,
|
|
131
|
+
data: content,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
129
134
|
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { generate } from "../../lib/generate.js";
|
|
2
|
+
import { cleanupModule } from "./cleanupModule.js";
|
|
3
|
+
import { logInputOutput } from "../../utils/promptLogHelper.js";
|
|
4
|
+
import { splitCodeIntoChunks, countTokens } from "../../utils/splitCodeIntoChunk.js";
|
|
5
|
+
const SINGLE_SHOT_TOKEN_LIMIT = 1800;
|
|
6
|
+
const CHUNK_TOKEN_LIMIT = 1500;
|
|
7
|
+
/**
|
|
8
|
+
* Remove leading/trailing markdown code fences.
|
|
9
|
+
*/
|
|
10
|
+
function stripCodeFences(text) {
|
|
11
|
+
const lines = text.split("\n");
|
|
12
|
+
while (lines.length && /^```/.test(lines[0].trim())) {
|
|
13
|
+
lines.shift();
|
|
14
|
+
}
|
|
15
|
+
while (lines.length && /^```/.test(lines[lines.length - 1].trim())) {
|
|
16
|
+
lines.pop();
|
|
17
|
+
}
|
|
18
|
+
return lines.join("\n");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Heuristic: decide whether chunk output looks unsafe / non-code.
|
|
22
|
+
*/
|
|
23
|
+
function isSuspiciousChunkOutput(text, originalChunk) {
|
|
24
|
+
const trimmed = text.trim();
|
|
25
|
+
if (!trimmed)
|
|
26
|
+
return true;
|
|
27
|
+
// Explanations or meta text
|
|
28
|
+
if (/here is|transformed|updated code|explanation/i.test(trimmed)) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
// JSON-ish output (we do NOT want JSON in chunk mode)
|
|
32
|
+
if (trimmed.startsWith("{") &&
|
|
33
|
+
trimmed.endsWith("}")) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
// Extremely short compared to original
|
|
37
|
+
if (trimmed.length < originalChunk.trim().length * 0.3) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
export const codeTransformModule = {
|
|
43
|
+
name: "codeTransform",
|
|
44
|
+
description: "Transforms a single file specified in the current plan step based on user instruction. " +
|
|
45
|
+
"Outputs full rewritten file content (materialized state).",
|
|
46
|
+
groups: ["transform"],
|
|
47
|
+
run: async (input) => {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
const query = typeof input.query === "string"
|
|
50
|
+
? input.query
|
|
51
|
+
: String(input.query ?? "");
|
|
52
|
+
const context = input.context;
|
|
53
|
+
if (!context) {
|
|
54
|
+
return { query, data: { files: [], errors: ["No context provided"] } };
|
|
55
|
+
}
|
|
56
|
+
const workingFiles = context.workingFiles ?? [];
|
|
57
|
+
const step = context.currentStep;
|
|
58
|
+
const targetFile = step?.targetFile;
|
|
59
|
+
if (!targetFile) {
|
|
60
|
+
return {
|
|
61
|
+
query,
|
|
62
|
+
data: { files: [], errors: ["No targetFile specified in current plan step"] },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const file = workingFiles.find(f => f.path === targetFile);
|
|
66
|
+
if (!file || typeof file.code !== "string") {
|
|
67
|
+
return {
|
|
68
|
+
query,
|
|
69
|
+
data: { files: [], errors: [`Target file not found or missing code: ${targetFile}`] },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const normalizedQuery = context.analysis?.intent?.normalizedQuery ?? query;
|
|
73
|
+
const outputs = [];
|
|
74
|
+
const perFileErrors = [];
|
|
75
|
+
const tokenCount = countTokens(file.code);
|
|
76
|
+
// =========================================================================
|
|
77
|
+
// 🔹 PATH 1 — SMALL FILE (JSON, strict)
|
|
78
|
+
// =========================================================================
|
|
79
|
+
if (tokenCount <= SINGLE_SHOT_TOKEN_LIMIT) {
|
|
80
|
+
const prompt = `
|
|
81
|
+
You are a precise code transformation assistant.
|
|
82
|
+
|
|
83
|
+
User instruction (normalized):
|
|
84
|
+
${normalizedQuery}
|
|
85
|
+
|
|
86
|
+
File to transform:
|
|
87
|
+
---\nFILE: ${file.path}\n${file.code}
|
|
88
|
+
|
|
89
|
+
Rules:
|
|
90
|
+
- You MUST apply the user instruction.
|
|
91
|
+
- You MUST return the FULL rewritten file content.
|
|
92
|
+
- Do NOT return diffs or partial snippets.
|
|
93
|
+
|
|
94
|
+
JSON schema:
|
|
95
|
+
{
|
|
96
|
+
"files": [
|
|
97
|
+
{
|
|
98
|
+
"filePath": "<path>",
|
|
99
|
+
"content": "<FULL rewritten file content>",
|
|
100
|
+
"notes": "<optional>"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"errors": []
|
|
104
|
+
}
|
|
105
|
+
`.trim();
|
|
106
|
+
try {
|
|
107
|
+
const llmResponse = await generate({ content: prompt, query });
|
|
108
|
+
const cleaned = await cleanupModule.run({
|
|
109
|
+
query,
|
|
110
|
+
content: llmResponse.data,
|
|
111
|
+
});
|
|
112
|
+
const structured = typeof cleaned.data === "object"
|
|
113
|
+
? cleaned.data
|
|
114
|
+
: JSON.parse(cleaned.data ?? "{}");
|
|
115
|
+
const out = Array.isArray(structured.files)
|
|
116
|
+
? structured.files.find((f) => f.filePath === file.path)
|
|
117
|
+
: null;
|
|
118
|
+
if (!out || typeof out.content !== "string" || !out.content.trim()) {
|
|
119
|
+
perFileErrors.push(`Model did not return full content for ${file.path}`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
outputs.push({
|
|
123
|
+
filePath: file.path,
|
|
124
|
+
content: out.content,
|
|
125
|
+
notes: out.notes,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
perFileErrors.push(...(structured.errors ?? []));
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return {
|
|
132
|
+
query,
|
|
133
|
+
data: { files: [], errors: [`LLM call or parsing failed: ${err.message}`] },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// =========================================================================
|
|
138
|
+
// 🔹 PATH 2 — LARGE FILE (chunked, raw text)
|
|
139
|
+
// =========================================================================
|
|
140
|
+
else {
|
|
141
|
+
const chunks = splitCodeIntoChunks(file.code, CHUNK_TOKEN_LIMIT);
|
|
142
|
+
const transformedChunks = [];
|
|
143
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
144
|
+
const chunk = chunks[i];
|
|
145
|
+
const prompt = `
|
|
146
|
+
You are a precise code transformation assistant.
|
|
147
|
+
|
|
148
|
+
User instruction (normalized):
|
|
149
|
+
${normalizedQuery}
|
|
150
|
+
|
|
151
|
+
You are given ONE CHUNK of a larger file.
|
|
152
|
+
|
|
153
|
+
Rules:
|
|
154
|
+
- Apply the instruction ONLY if relevant to this chunk.
|
|
155
|
+
- If no change is needed, return the chunk UNCHANGED.
|
|
156
|
+
- Do NOT add or remove unrelated code.
|
|
157
|
+
- Do NOT reference other chunks.
|
|
158
|
+
- Return ONLY code. No JSON. No explanations. No markdown fences.
|
|
159
|
+
|
|
160
|
+
FILE: ${file.path}
|
|
161
|
+
CHUNK ${i + 1} / ${chunks.length}
|
|
162
|
+
---
|
|
163
|
+
${chunk}
|
|
164
|
+
`.trim();
|
|
165
|
+
try {
|
|
166
|
+
logInputOutput("chunks", "output", chunk);
|
|
167
|
+
const llmResponse = await generate({ content: prompt, query });
|
|
168
|
+
const raw = typeof llmResponse.data === "string"
|
|
169
|
+
? llmResponse.data
|
|
170
|
+
: String(llmResponse.data ?? "");
|
|
171
|
+
const stripped = stripCodeFences(raw);
|
|
172
|
+
if (isSuspiciousChunkOutput(stripped, chunk)) {
|
|
173
|
+
transformedChunks.push(chunk);
|
|
174
|
+
perFileErrors.push(`Chunk ${i + 1} suspicious output; original preserved.`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
transformedChunks.push(stripped);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
transformedChunks.push(chunk);
|
|
182
|
+
perFileErrors.push(`Chunk ${i + 1} failed; original preserved. Error: ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
outputs.push({
|
|
186
|
+
filePath: file.path,
|
|
187
|
+
content: transformedChunks.join("\n"),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// =========================================================================
|
|
191
|
+
// 🔹 Persist execution artifacts
|
|
192
|
+
// =========================================================================
|
|
193
|
+
context.execution || (context.execution = {});
|
|
194
|
+
(_a = context.execution).codeTransformArtifacts || (_a.codeTransformArtifacts = { files: [] });
|
|
195
|
+
context.execution.codeTransformArtifacts.files.push(...outputs);
|
|
196
|
+
context.plan || (context.plan = {});
|
|
197
|
+
(_b = context.plan).touchedFiles || (_b.touchedFiles = []);
|
|
198
|
+
if (!context.plan.touchedFiles.includes(file.path)) {
|
|
199
|
+
context.plan.touchedFiles.push(file.path);
|
|
200
|
+
}
|
|
201
|
+
const output = {
|
|
202
|
+
query,
|
|
203
|
+
data: { files: outputs, errors: perFileErrors },
|
|
204
|
+
};
|
|
205
|
+
logInputOutput("codeTransform", "output", output.data);
|
|
206
|
+
return output;
|
|
207
|
+
},
|
|
208
|
+
};
|
|
@@ -4,10 +4,12 @@ import { detectFileType } from '../../fileRules/detectFileType.js';
|
|
|
4
4
|
export const addCommentsModule = {
|
|
5
5
|
name: 'comments',
|
|
6
6
|
description: 'Adds meaningful comments to any file type (code, config, or data)',
|
|
7
|
-
|
|
7
|
+
groups: ["transform"],
|
|
8
|
+
run: async (input) => {
|
|
8
9
|
const model = Config.getModel();
|
|
9
|
-
const
|
|
10
|
-
|
|
10
|
+
const filepath = typeof input.query === 'string' ? input.query : '';
|
|
11
|
+
const content = typeof input.content === 'string' ? input.content : '';
|
|
12
|
+
const fileType = detectFileType(filepath);
|
|
11
13
|
const commentMap = {
|
|
12
14
|
javascript: '//',
|
|
13
15
|
typescript: '//',
|
|
@@ -42,7 +44,7 @@ You are a senior engineer reviewing a ${fileType} file.
|
|
|
42
44
|
|
|
43
45
|
Please:
|
|
44
46
|
|
|
45
|
-
- Add summary comments (2
|
|
47
|
+
- Add summary comments (2–3 lines) at relevant points for greater class insights.
|
|
46
48
|
- Add clear, helpful inline-comments to explain non-obvious logic inside functions.
|
|
47
49
|
- Use "${commentSyntax}" as the comment syntax appropriate for ${fileType}.
|
|
48
50
|
|
|
@@ -50,14 +52,21 @@ Rules:
|
|
|
50
52
|
- Return the full original chunk of code with added comments.
|
|
51
53
|
- Inline comments should clarify complex or tricky parts.
|
|
52
54
|
|
|
53
|
-
${
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const response = await generate({
|
|
57
|
-
|
|
55
|
+
${content}
|
|
56
|
+
`.trim();
|
|
57
|
+
// ✅ Generate now uses ModuleIO (query + content)
|
|
58
|
+
const response = await generate({
|
|
59
|
+
query: filepath,
|
|
60
|
+
content: prompt,
|
|
61
|
+
});
|
|
62
|
+
// ✅ The result is now in `data`
|
|
63
|
+
const commentedContent = typeof response.data === 'string' && response.data !== 'NO UPDATE'
|
|
64
|
+
? response.data
|
|
65
|
+
: content;
|
|
66
|
+
// ✅ Return proper ModuleIO shape (output → data)
|
|
58
67
|
return {
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
query: filepath,
|
|
69
|
+
data: commentedContent,
|
|
61
70
|
};
|
|
62
71
|
},
|
|
63
72
|
};
|