scai 0.1.117 → 0.1.119

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.
Files changed (96) hide show
  1. package/README.md +88 -503
  2. package/dist/agents/MainAgent.js +255 -0
  3. package/dist/agents/contextReviewStep.js +104 -0
  4. package/dist/agents/finalPlanGenStep.js +123 -0
  5. package/dist/agents/infoPlanGenStep.js +126 -0
  6. package/dist/agents/planGeneratorStep.js +118 -0
  7. package/dist/agents/planResolverStep.js +95 -0
  8. package/dist/agents/planTargetFilesStep.js +48 -0
  9. package/dist/agents/preFileSearchCheckStep.js +95 -0
  10. package/dist/agents/selectRelevantSourcesStep.js +100 -0
  11. package/dist/agents/semanticAnalysisStep.js +144 -0
  12. package/dist/agents/structuralAnalysisStep.js +46 -0
  13. package/dist/agents/transformPlanGenStep.js +107 -0
  14. package/dist/agents/understandIntentStep.js +72 -0
  15. package/dist/agents/validationAnalysisStep.js +87 -0
  16. package/dist/commands/AskCmd.js +47 -116
  17. package/dist/commands/ChangeLogUpdateCmd.js +11 -5
  18. package/dist/commands/CommitSuggesterCmd.js +50 -75
  19. package/dist/commands/DaemonCmd.js +119 -29
  20. package/dist/commands/IndexCmd.js +41 -24
  21. package/dist/commands/InspectCmd.js +0 -1
  22. package/dist/commands/ReadlineSingleton.js +18 -0
  23. package/dist/commands/ResetDbCmd.js +20 -21
  24. package/dist/commands/ReviewCmd.js +89 -54
  25. package/dist/commands/SummaryCmd.js +12 -18
  26. package/dist/commands/WorkflowCmd.js +41 -0
  27. package/dist/commands/factory.js +254 -0
  28. package/dist/config.js +67 -15
  29. package/dist/constants.js +20 -4
  30. package/dist/context.js +10 -11
  31. package/dist/daemon/daemonQueues.js +63 -0
  32. package/dist/daemon/daemonWorker.js +40 -63
  33. package/dist/daemon/generateSummaries.js +58 -0
  34. package/dist/daemon/runFolderCapsuleBatch.js +247 -0
  35. package/dist/daemon/runIndexingBatch.js +147 -0
  36. package/dist/daemon/runKgBatch.js +104 -0
  37. package/dist/db/fileIndex.js +168 -63
  38. package/dist/db/functionExtractors/extractFromJava.js +210 -6
  39. package/dist/db/functionExtractors/extractFromJs.js +173 -214
  40. package/dist/db/functionExtractors/extractFromTs.js +159 -160
  41. package/dist/db/functionExtractors/index.js +7 -5
  42. package/dist/db/schema.js +55 -20
  43. package/dist/db/sqlTemplates.js +50 -19
  44. package/dist/fileRules/builtins.js +31 -14
  45. package/dist/fileRules/codeAllowedExtensions.js +4 -0
  46. package/dist/fileRules/fileExceptions.js +0 -13
  47. package/dist/fileRules/ignoredExtensions.js +10 -0
  48. package/dist/index.js +128 -325
  49. package/dist/lib/generate.js +37 -14
  50. package/dist/lib/generateFolderCapsules.js +109 -0
  51. package/dist/lib/spinner.js +12 -5
  52. package/dist/modelSetup.js +1 -11
  53. package/dist/pipeline/modules/changeLogModule.js +16 -19
  54. package/dist/pipeline/modules/chunkManagerModule.js +24 -0
  55. package/dist/pipeline/modules/cleanupModule.js +95 -91
  56. package/dist/pipeline/modules/codeTransformModule.js +208 -0
  57. package/dist/pipeline/modules/commentModule.js +20 -11
  58. package/dist/pipeline/modules/commitSuggesterModule.js +36 -14
  59. package/dist/pipeline/modules/contextReviewModule.js +52 -0
  60. package/dist/pipeline/modules/fileReaderModule.js +72 -0
  61. package/dist/pipeline/modules/fileSearchModule.js +136 -0
  62. package/dist/pipeline/modules/finalAnswerModule.js +53 -0
  63. package/dist/pipeline/modules/gatherInfoModule.js +176 -0
  64. package/dist/pipeline/modules/generateTestsModule.js +63 -54
  65. package/dist/pipeline/modules/kgModule.js +26 -11
  66. package/dist/pipeline/modules/preserveCodeModule.js +91 -49
  67. package/dist/pipeline/modules/refactorModule.js +19 -7
  68. package/dist/pipeline/modules/repairTestsModule.js +44 -36
  69. package/dist/pipeline/modules/reviewModule.js +23 -13
  70. package/dist/pipeline/modules/summaryModule.js +27 -35
  71. package/dist/pipeline/modules/writeFileModule.js +86 -0
  72. package/dist/pipeline/registry/moduleRegistry.js +38 -93
  73. package/dist/pipeline/runModulePipeline.js +22 -19
  74. package/dist/scripts/dbcheck.js +143 -228
  75. package/dist/utils/buildContextualPrompt.js +245 -172
  76. package/dist/utils/debugContext.js +24 -0
  77. package/dist/utils/fileTree.js +16 -6
  78. package/dist/utils/loadRelevantFolderCapsules.js +64 -0
  79. package/dist/utils/log.js +2 -0
  80. package/dist/utils/normalizeData.js +23 -0
  81. package/dist/utils/planActions.js +60 -0
  82. package/dist/utils/promptBuilderHelper.js +67 -0
  83. package/dist/utils/promptLogHelper.js +52 -0
  84. package/dist/utils/sanitizeQuery.js +20 -8
  85. package/dist/utils/sleep.js +3 -0
  86. package/dist/utils/splitCodeIntoChunk.js +65 -32
  87. package/dist/utils/vscode.js +49 -0
  88. package/dist/workflow/workflowResolver.js +14 -0
  89. package/dist/workflow/workflowRunner.js +103 -0
  90. package/package.json +6 -5
  91. package/dist/agent/agentManager.js +0 -39
  92. package/dist/agent/workflowManager.js +0 -95
  93. package/dist/commands/ModulePipelineCmd.js +0 -31
  94. package/dist/daemon/daemonBatch.js +0 -186
  95. package/dist/fileRules/scoreFiles.js +0 -71
  96. package/dist/lib/generateEmbedding.js +0 -22
@@ -3,14 +3,19 @@ export class Spinner {
3
3
  this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
4
4
  this.interval = null;
5
5
  this.i = 0;
6
- this.text = message;
6
+ if (message) {
7
+ this.text = message;
8
+ }
7
9
  }
8
10
  start() {
9
11
  process.stdout.write('\x1b[?25l'); // hide cursor
10
- process.stdout.write(` ${this.text}`); // print text once
12
+ if (this.text) {
13
+ process.stdout.write(` ${this.text}`);
14
+ }
11
15
  this.interval = setInterval(() => {
12
- process.stdout.write('\r'); // return to start of line
13
- process.stdout.write(`${this.frames[this.i]} ${this.text}`);
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
- console.log(`✅ ${msg}`);
28
+ if (msg) {
29
+ console.log(`✅ ${msg}`);
30
+ }
24
31
  }
25
32
  fail(msg) {
26
33
  this.stop();
@@ -9,7 +9,7 @@ import { readConfig, writeConfig } from './config.js';
9
9
  import { CONFIG_PATH } from './constants.js';
10
10
  // Constants
11
11
  const MODEL_PORT = 11434;
12
- const REQUIRED_MODELS = ['llama3:8b'];
12
+ const REQUIRED_MODELS = ['qwen3-coder:30b'];
13
13
  const OLLAMA_URL = 'https://ollama.com/download';
14
14
  const VSCODE_URL = 'https://code.visualstudio.com/download';
15
15
  const isYesMode = process.argv.includes('--yes') || process.env.SCAI_YES === '1';
@@ -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
- ${input.content}
20
+ ${diffContent}
19
21
  --- DIFF END ---
20
22
  `.trim();
21
- const response = await generate({ content: prompt });
22
- // Check if we received a meaningful result or "NO UPDATE"
23
- const content = response?.content?.trim();
24
- if (content === 'NO UPDATE') {
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
- content: response.content,
28
- summary: 'NO UPDATE',
29
- suggestions: response?.suggestions ?? [],
30
- filepath: input.filepath,
31
+ query: input.query,
32
+ data: 'NO UPDATE',
31
33
  };
32
34
  }
33
- // Split the content into lines
34
- const lines = content?.split('\n').map(line => line.trim()) ?? [];
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
- content: response.content,
42
- summary: filtered,
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
- import chalk from 'chalk';
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('//') && !trimmed.startsWith('/*')) {
11
+ if (!trimmed.startsWith("//") && !trimmed.startsWith("/*")) {
10
12
  return [
11
13
  /^i\s/i,
12
14
  /^here/,
@@ -23,107 +25,109 @@ 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
- export const cleanupModule = {
31
- name: 'cleanup',
32
- description: 'Remove markdown fences, fluff, and non-JSON lines with colored logging',
33
- async run(input) {
34
- // Normalize line endings to \n
35
- let content = input.content.replace(/\r\n/g, '\n');
36
- let lines = content.split('\n');
37
- // --- CLEAN TOP ---
38
- while (lines.length && (lines[0].trim() === '' || isTopOrBottomNoise(lines[0]))) {
39
- if (/^```(?:\w+)?$/.test(lines[0].trim()))
40
- break;
41
- console.log(chalk.red(`[cleanupModule] Removing noise from top:`), chalk.yellow(`"${lines[0].trim()}"`));
42
- lines.shift();
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
- if (lines.length && /^```(?:\w+)?$/.test(lines[0].trim())) {
45
- console.log(chalk.red(`[cleanupModule] Found opening fenced block at top.`));
46
- lines.shift();
47
- let closingIndex = -1;
48
- for (let i = 0; i < lines.length; i++) {
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
- while (lines.length && (lines[lines.length - 1].trim() === '' || isTopOrBottomNoise(lines[lines.length - 1]))) {
68
- console.log(chalk.red(`[cleanupModule] Removing noise from bottom after fenced block:`), chalk.yellow(`"${lines[lines.length - 1].trim()}"`));
69
- lines.pop();
61
+ }
62
+ // --- 2) JSON Object strategy ---
63
+ if (trimmed.startsWith("{")) {
64
+ try {
65
+ return JSON.parse(trimmed);
70
66
  }
71
- // --- REMOVE ANY LINGERING TRIPLE TICK LINES ANYWHERE ---
72
- lines = lines.filter(line => {
73
- const trimmed = line.trim();
74
- if (/^```(?:\w+)?$/.test(trimmed)) {
75
- console.log(chalk.red(`[cleanupModule] Removing lingering triple tick line:`), chalk.yellow(`"${line}"`));
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
- // Stop collecting after outermost brace closed
103
- if (braceDepth === 0)
104
- break;
73
+ catch { }
105
74
  }
106
75
  }
107
- let finalContent = jsonLines.join('\n').trim();
108
- // --- VALIDATE JSON ---
76
+ }
77
+ // --- 3) Fallback: if mixed text, extract first object ---
78
+ const fallbackObj = extractObject(content);
79
+ if (fallbackObj) {
109
80
  try {
110
- JSON.parse(finalContent);
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
+ return {
121
+ query: input.query,
122
+ content,
123
+ data: parsed,
124
+ };
125
+ }
126
+ console.warn(chalk.red("[cleanupModule] Failed to parse JSON — returning raw content."));
127
+ return {
128
+ query: input.query,
129
+ content,
130
+ data: content,
131
+ };
132
+ },
129
133
  };
@@ -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
- async run(input) {
7
+ groups: ["transform"],
8
+ run: async (input) => {
8
9
  const model = Config.getModel();
9
- const fileType = detectFileType(input.filepath || '');
10
- // Map file types to valid comment syntax
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-3 lines) at relevant points for greater class insights
47
+ - Add summary comments (23 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
- ${input.content}
54
-
55
- `.trim();
56
- const response = await generate({ content: prompt });
57
- const contentToReturn = (response.content && response.content !== 'NO UPDATE') ? response.content : input.content;
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
- content: contentToReturn,
60
- mode: 'overwrite', // <-- declares that the original file should be overwritten
68
+ query: filepath,
69
+ data: commentedContent,
61
70
  };
62
71
  },
63
72
  };