scai 0.1.101 → 0.1.103

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.
@@ -1,52 +1,49 @@
1
1
  import path from 'path';
2
- export function detectFileType(filepath) {
3
- const ext = path.extname(filepath).toLowerCase();
2
+ export function detectFileType(filepathOrExt) {
3
+ // If it's already an extension (starts with '.'), use it directly
4
+ const ext = filepathOrExt.startsWith(".")
5
+ ? filepathOrExt.toLowerCase()
6
+ : path.extname(filepathOrExt).toLowerCase();
4
7
  const map = {
5
- // Programming languages
6
- '.ts': 'typescript',
7
- '.tsx': 'typescript',
8
- '.js': 'javascript',
9
- '.jsx': 'javascript',
10
- '.java': 'java',
11
- '.py': 'python',
12
- '.rb': 'ruby',
13
- '.php': 'php',
14
- '.go': 'go',
15
- '.rs': 'rust',
16
- '.c': 'c',
17
- '.cpp': 'cpp',
18
- '.cs': 'csharp',
19
- '.swift': 'swift',
20
- '.kt': 'kotlin',
21
- '.scala': 'scala',
22
- // Markup & docs
23
- '.md': 'markdown',
24
- '.html': 'html',
25
- '.htm': 'html',
26
- '.xml': 'xml',
27
- '.json': 'json',
28
- '.yaml': 'yaml',
29
- '.yml': 'yaml',
30
- // Configs
31
- '.ini': 'config',
32
- '.toml': 'config',
33
- '.env': 'config',
34
- // Data
35
- '.sql': 'sql',
36
- '.csv': 'csv',
37
- '.tsv': 'tsv',
38
- // Text & writing
39
- '.txt': 'text',
40
- '.log': 'log',
41
- '.rst': 'text',
42
- // Office
43
- '.doc': 'word',
44
- '.docx': 'word',
45
- '.pdf': 'pdf',
46
- '.ppt': 'powerpoint',
47
- '.pptx': 'powerpoint',
48
- '.xls': 'excel',
49
- '.xlsx': 'excel',
8
+ ".ts": "typescript",
9
+ ".tsx": "typescript",
10
+ ".js": "javascript",
11
+ ".jsx": "javascript",
12
+ ".java": "java",
13
+ ".py": "python",
14
+ ".rb": "ruby",
15
+ ".php": "php",
16
+ ".go": "go",
17
+ ".rs": "rust",
18
+ ".c": "c",
19
+ ".cpp": "cpp",
20
+ ".cs": "csharp",
21
+ ".swift": "swift",
22
+ ".kt": "kotlin",
23
+ ".scala": "scala",
24
+ ".md": "markdown",
25
+ ".html": "html",
26
+ ".htm": "html",
27
+ ".xml": "xml",
28
+ ".json": "json",
29
+ ".yaml": "yaml",
30
+ ".yml": "yaml",
31
+ ".ini": "config",
32
+ ".toml": "config",
33
+ ".env": "config",
34
+ ".sql": "sql",
35
+ ".csv": "csv",
36
+ ".tsv": "tsv",
37
+ ".txt": "text",
38
+ ".log": "log",
39
+ ".rst": "text",
40
+ ".doc": "word",
41
+ ".docx": "word",
42
+ ".pdf": "pdf",
43
+ ".ppt": "powerpoint",
44
+ ".pptx": "powerpoint",
45
+ ".xls": "excel",
46
+ ".xlsx": "excel",
50
47
  };
51
- return map[ext] || ext.replace('.', '') || 'unknown';
48
+ return map[ext] || ext.replace(".", "") || "unknown";
52
49
  }
package/dist/index.js CHANGED
@@ -125,7 +125,8 @@ gen
125
125
  .description("Write comments for the given file(s) or folder(s)")
126
126
  .action(async (targets) => {
127
127
  await withContext(async () => {
128
- const files = await resolveTargetsToFiles(targets, [".ts", ".js"]);
128
+ // Remove the file type filter to allow any file
129
+ const files = await resolveTargetsToFiles(targets);
129
130
  for (const file of files) {
130
131
  await handleAgentRun(file, [addCommentsModule, preserveCodeModule]);
131
132
  }
@@ -42,18 +42,10 @@ You are a senior engineer reviewing a ${fileType} file.
42
42
 
43
43
  Please:
44
44
 
45
- 1. Add a concise summary comment above each function describing its purpose.
46
- - Do NOT write trivial summaries like "..." or empty comments.
47
- - Do NOT overwrite or remove any existing comments.
48
-
49
- 2. Add clear, helpful inline comments to explain non-obvious logic inside functions.
50
- - Preserve all existing comments as-is.
51
-
52
- 3. Use "${commentSyntax}" as the comment syntax appropriate for ${fileType}.
53
-
54
- 4. Preserve all original formatting, whitespace, and code exactly.
55
-
56
- 5. You may only be shown parts of a file. Add comments as instructed.
45
+ - Add summary comments (2-3 lines) at relevant points for greater class insights
46
+ - Add clear, helpful inline comments to explain non-obvious logic inside functions.
47
+ - Use "${commentSyntax}" as the comment syntax appropriate for ${fileType}.
48
+ - Preserve all original formatting, whitespace, and code exactly.
57
49
 
58
50
  Rules:
59
51
  - Return the full original chunk of code with added comments only.
@@ -62,8 +54,6 @@ Rules:
62
54
  - Inline comments should clarify complex or tricky parts only.
63
55
  - Do NOT add any “chunk” start/end markers, numbering, or metadata to the output.
64
56
 
65
- Code to comments is below this line:
66
-
67
57
  ${input.content}
68
58
 
69
59
  `.trim();
@@ -1,28 +1,31 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
- import { Config } from '../../config.js';
4
3
  import { generate } from '../../lib/generate.js';
4
+ import { detectFileType } from '../../fileRules/detectFileType.js';
5
+ import { Config } from '../../config.js';
5
6
  export const generateTestsModule = {
6
7
  name: 'tests',
7
8
  description: 'Generate a Jest test file for the class/module',
8
9
  async run({ content, filepath }) {
9
- const model = Config.getModel();
10
- const lang = Config.getLanguage();
11
10
  if (!filepath)
12
11
  throw new Error('Missing filepath in pipeline context');
12
+ const model = Config.getModel();
13
+ const lang = detectFileType(filepath);
13
14
  const prompt = `
14
- You are a senior ${lang.toUpperCase()} engineer. Given the following class or module, generate a Jest test file.
15
+ You are a senior ${lang.toUpperCase()} engineer. Given the following class or module, generate a Jest test file.
15
16
 
16
- Guidelines:
17
- - Use the 'jest' test framework
18
- - Cover public methods and one edge case
19
- - Name the file <original>.test.ts
20
- - Only return valid TypeScript code
17
+ Guidelines:
18
+ - Use the 'jest' test framework
19
+ - Cover only one public method: the most relevant or central function
20
+ - Include one edge case for that method
21
+ - Preserve and consider existing code comments
22
+ - Name the file <original>.test.${path.extname(filepath).slice(1)}
23
+ - Only return valid ${lang} code
21
24
 
22
- --- CODE START ---
23
- ${content}
24
- --- CODE END ---
25
- `.trim();
25
+ --- CODE START ---
26
+ ${content}
27
+ --- CODE END ---
28
+ `.trim();
26
29
  const response = await generate({ content: prompt }, model);
27
30
  if (!response)
28
31
  throw new Error('⚠️ No test code returned from model');
@@ -1,4 +1,6 @@
1
1
  import chalk from "chalk";
2
+ import { getCommentSyntax } from "../../utils/commentMap.js";
3
+ import { detectFileType } from "../../fileRules/detectFileType.js";
2
4
  export const preserveCodeModule = {
3
5
  name: "preserveCodeModule",
4
6
  description: "Ensure code matches original exactly, preserving comments with clear before/after output",
@@ -6,10 +8,13 @@ export const preserveCodeModule = {
6
8
  const { originalContent, content, filepath } = input;
7
9
  if (!originalContent)
8
10
  throw new Error("Requires `originalContent`.");
9
- const syntax = {
10
- singleLine: ["//"],
11
- multiLine: [{ start: "/*", end: "*/" }, { start: "/**", end: "*/" }]
12
- };
11
+ // Determine language from filepath extension
12
+ console.log("Filepath: ", filepath);
13
+ const ext = "." + (filepath?.split(".").pop() || "ts");
14
+ console.log("Extension: ", ext);
15
+ const language = detectFileType(filepath ?? ext); // returns "javascript", "python", etc.
16
+ const syntax = getCommentSyntax(language);
17
+ console.log(`Using comment syntax for extension '${language}':`, syntax);
13
18
  // --- Normalize line endings ---
14
19
  const normalize = (txt) => txt.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
15
20
  const origLines = normalize(originalContent).split("\n");
@@ -17,34 +22,47 @@ export const preserveCodeModule = {
17
22
  // --- Classify line ---
18
23
  let inBlockComment = false;
19
24
  let blockLines = [];
25
+ // --- Classify line ---
26
+ // returns: "code" | comment string | null
20
27
  const classifyLine = (line) => {
21
28
  const trimmed = line.trimStart();
22
29
  // --- Single-line comment ---
23
30
  for (const s of syntax.singleLine) {
24
31
  if (trimmed.startsWith(s))
25
- return "comment";
32
+ return line;
26
33
  }
27
- // --- Multi-line comment ---
28
- for (const { start, end } of syntax.multiLine) {
29
- if (!inBlockComment) {
30
- if (trimmed.startsWith(start) && trimmed.includes(end)) {
31
- return "comment"; // entire block on one line
32
- }
34
+ // --- Multi-line comment (optional) ---
35
+ const multiLineComments = syntax.multiLine ?? [];
36
+ if (!inBlockComment) {
37
+ // check if line starts a multi-line comment
38
+ for (const { start, end } of multiLineComments) {
33
39
  if (trimmed.startsWith(start)) {
34
- inBlockComment = true;
35
40
  blockLines = [line];
36
- return ""; // wait until block ends
41
+ if (trimmed.includes(end) && trimmed.indexOf(end) > trimmed.indexOf(start)) {
42
+ // entire block on one line
43
+ return blockLines.join("\n");
44
+ }
45
+ else {
46
+ inBlockComment = true;
47
+ return null; // wait for block to finish
48
+ }
37
49
  }
38
50
  }
39
- else {
40
- blockLines.push(line);
51
+ }
52
+ else {
53
+ // currently inside a multi-line block
54
+ blockLines.push(line);
55
+ for (const { end } of multiLineComments) {
41
56
  if (trimmed.includes(end)) {
42
57
  inBlockComment = false;
43
- return "comment"; // end of block
58
+ const fullBlock = blockLines.join("\n");
59
+ blockLines = [];
60
+ return fullBlock; // emit entire block
44
61
  }
45
- return ""; // inside block
46
62
  }
63
+ return null; // still inside block
47
64
  }
65
+ // --- default: code ---
48
66
  return "code";
49
67
  };
50
68
  // --- Helper: collect comment blocks into map ---
@@ -52,97 +70,77 @@ export const preserveCodeModule = {
52
70
  const map = new Map();
53
71
  let commentBuffer = [];
54
72
  for (const line of lines) {
55
- const type = classifyLine(line);
56
- if (type === "comment") {
57
- // Collect full comment line
58
- commentBuffer.push(line);
59
- }
60
- else if (type === "code") {
61
- // Flush buffer when hitting code
73
+ const result = classifyLine(line);
74
+ if (result === "code") {
62
75
  if (commentBuffer.length > 0) {
63
76
  const key = line.trim().toLowerCase();
77
+ const block = commentBuffer.join("\n").trim().toLowerCase();
64
78
  if (!map.has(key))
65
79
  map.set(key, new Set());
66
- const commentBlock = commentBuffer.map(l => l.trimEnd()).join("\n").toLowerCase();
67
- map.get(key).add(commentBlock);
80
+ map.get(key).add(block);
68
81
  commentBuffer = [];
69
82
  }
83
+ continue;
84
+ }
85
+ if (typeof result === "string") {
86
+ // comment line or block
87
+ commentBuffer.push(result);
70
88
  }
89
+ // result === null => inside multi-line block, do nothing
71
90
  }
72
- // Flush remaining comments at EOF
91
+ // flush at EOF
73
92
  if (commentBuffer.length > 0) {
74
93
  const key = "";
94
+ const block = commentBuffer.join("\n").trim().toLowerCase();
75
95
  if (!map.has(key))
76
96
  map.set(key, new Set());
77
- const commentBlock = commentBuffer.map(l => l.trimEnd()).join("\n").toLowerCase();
78
- map.get(key).add(commentBlock);
97
+ map.get(key).add(block);
79
98
  }
80
99
  return map;
81
100
  }
82
101
  // --- Step 1: Collect comments ---
83
102
  const modelComments = collectCommentsMap(newLines); // model first
84
103
  const origComments = collectCommentsMap(origLines); // original
85
- // --- Step 2: Remove duplicates ---
86
- for (const [key, commentSet] of modelComments.entries()) {
87
- if (origComments.has(key)) {
88
- commentSet.forEach(c => {
89
- origComments.get(key).delete(c.trim().toLowerCase());
90
- });
91
- if (origComments.get(key).size === 0)
92
- origComments.delete(key);
104
+ // --- Step 2: Remove duplicates from model (since we insert model) ---
105
+ for (const [key, modelSet] of modelComments.entries()) {
106
+ const origSet = origComments.get(key);
107
+ if (!origSet)
108
+ continue;
109
+ for (const c of Array.from(modelSet)) {
110
+ if (origSet.has(c))
111
+ modelSet.delete(c);
93
112
  }
113
+ if (modelSet.size === 0)
114
+ modelComments.delete(key);
94
115
  }
95
- // --- Step 3: Build fixed lines with model comments inserted above original ---
116
+ // --- Step 3: Build fixed lines ---
96
117
  const fixedLines = [];
97
118
  for (const origLine of origLines) {
98
119
  const key = origLine.trim().toLowerCase();
99
- // Insert model comment blocks if any
100
120
  if (modelComments.has(key)) {
101
- modelComments.get(key).forEach(block => {
102
- const lines = block.split("\n");
103
- for (const line of lines) {
104
- if (!fixedLines.includes(line)) {
121
+ for (const block of modelComments.get(key)) {
122
+ for (const line of block.split("\n")) {
123
+ const norm = line.trim().toLowerCase();
124
+ const already = fixedLines.some(l => l.trim().toLowerCase() === norm);
125
+ if (!already) {
105
126
  fixedLines.push(line);
106
127
  console.log(chalk.blue("Inserted comment:"), line.trim());
107
128
  }
108
129
  else {
109
- console.log(chalk.gray("Skipped duplicate comment:"), line.trim());
130
+ console.log(chalk.gray("Skipped duplicate:"), line.trim());
110
131
  }
111
132
  }
112
- });
133
+ }
113
134
  }
114
- fixedLines.push(origLine);
135
+ fixedLines.push(origLine); // always keep original
115
136
  }
116
- // --- Logging for debugging ---
117
- console.log(chalk.bold.blue("\n=== LINE CLASSIFICATION (original) ==="));
118
- origLines.forEach((line, i) => {
119
- const type = classifyLine(line);
120
- const colored = type === "code"
121
- ? chalk.green(line)
122
- : type === "comment"
123
- ? chalk.yellow(line)
124
- : chalk.gray(line); // "" means middle of block
125
- console.log(`${i + 1}: ${colored} ${chalk.gray(`[${type}]`)}`);
126
- });
127
- console.log(chalk.bold.blue("\n=== LINE CLASSIFICATION (model) ==="));
128
- newLines.forEach((line, i) => {
129
- const type = classifyLine(line);
130
- const colored = type === "code"
131
- ? chalk.green(line)
132
- : type === "comment"
133
- ? chalk.yellow(line)
134
- : chalk.gray(line);
135
- console.log(`${i + 1}: ${colored} ${chalk.gray(`[${type}]`)}`);
136
- });
137
+ // --- Logging ---
137
138
  console.log(chalk.bold.blue("\n=== FIXED CONTENT ==="));
138
139
  fixedLines.forEach((line, i) => {
139
- // classifyLine might not be ideal here since fixedLines are final
140
- // so we treat anything starting with a comment marker as "comment"
141
140
  const trimmed = line.trimStart();
142
- const type = syntax.singleLine.some(s => trimmed.startsWith(s)) ||
143
- syntax.multiLine.some(({ start }) => trimmed.startsWith(start))
144
- ? "comment"
145
- : "code";
141
+ const isComment = syntax.singleLine.some(s => trimmed.startsWith(s)) ||
142
+ (syntax.multiLine ?? []).some(({ start }) => trimmed.startsWith(start));
143
+ const type = isComment ? "comment" : "code";
146
144
  const colored = type === "code" ? chalk.green(line) : chalk.yellow(line);
147
145
  console.log(`${i + 1}: ${colored} ${chalk.gray(`[${type}]`)}`);
148
146
  });
@@ -4,15 +4,18 @@ export async function runModulePipeline(modules, input) {
4
4
  const isDebug = false;
5
5
  for (const mod of modules) {
6
6
  try {
7
+ if (isDebug)
8
+ console.log(current.filepath);
7
9
  const response = await mod.run(current);
8
10
  console.log(`⚙️ Running: ${mod.name}`);
9
11
  if (isDebug) {
10
12
  console.log(chalk.yellow('➡️ Output:', response.content));
11
13
  }
12
- // Safeguard if originalContent is overwritten by module
14
+ // Safeguard to preserve filepath + originalContent
13
15
  current = {
14
16
  ...response,
15
- originalContent: current.originalContent
17
+ originalContent: current.originalContent,
18
+ filepath: current.filepath,
16
19
  };
17
20
  }
18
21
  catch (error) {
@@ -75,5 +75,6 @@ export const commentMap = {
75
75
  */
76
76
  export function getCommentSyntax(language) {
77
77
  const normalized = language.toLowerCase();
78
- return commentMap[normalized] || { singleLine: ["#"], multiLine: [] };
78
+ return (commentMap[normalized] ||
79
+ { singleLine: ["//"], multiLine: ["/*", "*/"] });
79
80
  }
@@ -21,7 +21,7 @@ async function collectFilesRecursive(dir, exts) {
21
21
  if (entry.isDirectory()) {
22
22
  files.push(...await collectFilesRecursive(fullPath, exts));
23
23
  }
24
- else if (exts.includes(path.extname(entry.name))) {
24
+ else if (!exts || exts.includes(path.extname(entry.name))) {
25
25
  files.push(fullPath);
26
26
  }
27
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.101",
3
+ "version": "0.1.103",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"