scai 0.1.98 → 0.1.100

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  - 💬 **Suggests intelligent Git commit messages** based on your staged diff
8
8
  - 🤖 **Reviews open pull requests** and provides AI‑driven feedback (BETA)
9
- - 📝 Summarizes files in plain English
9
+ - 📝 **Generates comments for multiple files** while you keep coding
10
10
  - 📜 Auto‑updates your changelog
11
11
  - 🔍 (ALPHA) Search & ask questions across your codebase
12
12
  - 🔐 100% local — no API keys, no cloud, no telemetry
@@ -225,22 +225,12 @@ This will:
225
225
  ### 🛠️ Code Generation Commands (`gen` group)
226
226
 
227
227
  ```bash
228
- scai gen summ <file>
229
228
  scai gen comm <file|folder...>
229
+ scai gen summ <file>
230
230
  scai gen changelog
231
231
  scai gen tests <file>
232
232
  ```
233
233
 
234
- * `summ`: Summarize a file
235
-
236
- You can also pipe file content directly:
237
-
238
- ```bash
239
- cat src/utils/math.ts | scai gen summ
240
- ```
241
-
242
- </br>
243
-
244
234
  * `comm`: Add comments to one or more files, or to all matching files in a folder (recursive).
245
235
 
246
236
  </br>
@@ -255,6 +245,17 @@ scai gen tests <file>
255
245
 
256
246
  </br>
257
247
 
248
+ * `summ`: Summarize a file
249
+
250
+ You can also pipe file content directly:
251
+
252
+ ```bash
253
+ cat src/utils/math.ts | scai gen summ
254
+ ```
255
+
256
+ </br>
257
+
258
+
258
259
  * `changelog`: Update or create `CHANGELOG.md` from Git diff
259
260
 
260
261
  * `tests`: Create Jest test stubs (ALPHA)
@@ -268,17 +269,24 @@ scai stores settings in `~/.scai/config.json`. You can override or view them:
268
269
  * **Set model:**
269
270
 
270
271
  ```bash
271
- scai set model codellama:7b
272
+ scai config set-model codellama:7b
272
273
  ```
273
274
  * **Set language:**
274
275
 
275
276
  ```bash
276
- scai set lang ts
277
+ scai config set-lang ts
277
278
  ```
278
279
  * **Show config:**
279
280
 
281
+ To see the config for the active repo
280
282
  ```bash
281
- scai config
283
+ scai config show
284
+ ```
285
+
286
+ To see the config for all repos
287
+
288
+ ```bash
289
+ scai config show --raw
282
290
  ```
283
291
 
284
292
  ---
package/dist/CHANGELOG.md CHANGED
@@ -140,4 +140,10 @@ Type handling with the module pipeline
140
140
 
141
141
  ## 2025-08-24
142
142
 
143
- • Improved CLI review command with AI-generated suggestions and enhanced user interface.
143
+ • Improved CLI review command with AI-generated suggestions and enhanced user interface.
144
+
145
+ ## 2025-08-26
146
+
147
+ • Fixed bug where entire block was returned as a single line for multi-line comments
148
+ • Add multi-line comment handling with ~90% accuracy
149
+ • Update CLI config file to use codellama:13b model and 4096 context length
package/dist/config.js CHANGED
@@ -6,8 +6,8 @@ import { normalizePath } from './utils/normalizePath.js';
6
6
  import chalk from 'chalk';
7
7
  import { getHashedRepoKey } from './utils/repoKey.js';
8
8
  const defaultConfig = {
9
- model: 'llama3',
10
- contextLength: 8192,
9
+ model: 'codellama:13b',
10
+ contextLength: 4096,
11
11
  language: 'ts',
12
12
  indexDir: '',
13
13
  githubToken: '',
@@ -5,9 +5,7 @@ export async function generate(input, model) {
5
5
  const contextLength = readConfig().contextLength ?? 8192;
6
6
  let prompt = input.content;
7
7
  if (prompt.length > contextLength) {
8
- console.warn(`⚠️ Warning: Input prompt length (${prompt.length}) exceeds model context length (${contextLength}). ` +
9
- `The model may truncate or not handle the entire prompt. Truncating input.`);
10
- prompt = prompt.slice(0, contextLength);
8
+ console.warn(`⚠️ Warning: Input prompt length (${prompt.length}) exceeds model context length (${contextLength}). `);
11
9
  }
12
10
  const spinner = new Spinner(`🧠 Thinking with ${model}...`);
13
11
  spinner.start();
@@ -8,126 +8,131 @@ export const preserveCodeModule = {
8
8
  throw new Error("Requires `originalContent`.");
9
9
  const syntax = {
10
10
  singleLine: ["//"],
11
- multiLine: [{ start: "/*", end: "*/" }]
11
+ multiLine: [{ start: "/*", end: "*/" }, { start: "/**", end: "*/" }]
12
12
  };
13
13
  // --- Normalize line endings ---
14
14
  const normalize = (txt) => txt.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
15
15
  const origLines = normalize(originalContent).split("\n");
16
16
  const newLines = normalize(content).split("\n");
17
- // Detect if a line is a comment line, and classify
17
+ // --- Classify line ---
18
18
  let inBlockComment = false;
19
+ let blockLines = [];
19
20
  const classifyLine = (line) => {
20
21
  const trimmed = line.trimStart();
21
- // Single-line
22
+ // --- Single-line comment ---
22
23
  for (const s of syntax.singleLine) {
23
24
  if (trimmed.startsWith(s))
24
- return "single-comment";
25
+ return line; // return actual line
25
26
  }
26
- // Multi-line start/end
27
+ // --- Multi-line comment ---
27
28
  for (const { start, end } of syntax.multiLine) {
28
- if (trimmed.startsWith(start) && trimmed.includes(end)) {
29
- return "multi-comment (start+end)";
30
- }
31
- if (trimmed.startsWith(start)) {
32
- inBlockComment = true;
33
- return "multi-comment (start)";
29
+ if (!inBlockComment) {
30
+ if (trimmed.startsWith(start) && trimmed.includes(end)) {
31
+ return line; // entire block on a single line
32
+ }
33
+ if (trimmed.startsWith(start)) {
34
+ inBlockComment = true;
35
+ blockLines = [line];
36
+ return line; // start of multi-line block
37
+ }
34
38
  }
35
- if (inBlockComment) {
39
+ else {
40
+ blockLines.push(line);
36
41
  if (trimmed.includes(end)) {
37
42
  inBlockComment = false;
38
- return "multi-comment (end)";
43
+ const fullBlock = blockLines.join("\n");
44
+ blockLines = [];
45
+ return fullBlock; // return entire multi-line block
39
46
  }
40
- return "multi-comment (mid)";
47
+ return ""; // middle lines, wait until block ends
41
48
  }
42
49
  }
43
50
  return "code";
44
51
  };
45
- // Collect consecutive comment lines as one block
46
- function collectBlock(lines, startIndex) {
47
- const block = [];
48
- let i = startIndex;
49
- while (i < lines.length && classifyLine(lines[i]) !== "code") {
50
- block.push(lines[i]);
51
- i++;
52
- }
53
- return block;
54
- }
55
- const trimBlock = (block) => block.map(line => line.trim());
56
- const blocksEqual = (a, b) => JSON.stringify(trimBlock(a)) === JSON.stringify(trimBlock(b));
57
- const fixedLines = [];
58
- let origIndex = 0;
59
- let newIndex = 0;
60
- // Track all inserted comment blocks globally
61
- const insertedBlocks = new Set();
62
- while (origIndex < origLines.length) {
63
- const origLine = origLines[origIndex];
64
- // If this is a comment line in original or model
65
- if (classifyLine(origLine) !== "code" || classifyLine(newLines[newIndex] ?? "") !== "code") {
66
- const origBlock = collectBlock(origLines, origIndex);
67
- const modelBlock = collectBlock(newLines, newIndex);
68
- // Merge: model block first, then any orig lines not in model
69
- const seen = new Set(trimBlock(modelBlock));
70
- const mergedBlock = [...modelBlock];
71
- for (const line of origBlock) {
72
- if (!seen.has(line.trim())) {
73
- mergedBlock.push(line);
74
- }
52
+ // --- Helper: collect comment blocks into map ---
53
+ function collectCommentsMap(lines) {
54
+ const map = new Map();
55
+ let commentBuffer = [];
56
+ for (const line of lines) {
57
+ const type = classifyLine(line);
58
+ if (type && type !== "code") {
59
+ // Collect comment lines
60
+ commentBuffer.push(type.trim());
75
61
  }
76
- // Create a key for duplicate detection
77
- const mergedKey = JSON.stringify(trimBlock(mergedBlock));
78
- // Insert only if this block was never inserted before
79
- if (!insertedBlocks.has(mergedKey)) {
80
- fixedLines.push(...mergedBlock);
81
- insertedBlocks.add(mergedKey);
82
- }
83
- else {
84
- console.log("Skipping duplicate block (already inserted)");
62
+ else if (type === "code") {
63
+ // Flush buffer when hitting code
64
+ if (commentBuffer.length > 0) {
65
+ const key = line.trim().toLowerCase();
66
+ if (!map.has(key))
67
+ map.set(key, new Set());
68
+ // Join consecutive comments into one block
69
+ const commentBlock = commentBuffer.join("\n").toLowerCase();
70
+ map.get(key).add(commentBlock);
71
+ commentBuffer = [];
72
+ }
85
73
  }
86
- // Advance indices past the entire blocks
87
- origIndex += origBlock.length;
88
- newIndex += modelBlock.length;
89
- continue;
90
74
  }
91
- // Non-comment line
92
- const newLine = newLines[newIndex] ?? "";
93
- fixedLines.push(origLine.trim() === newLine.trim() ? newLine : origLine);
94
- origIndex++;
95
- newIndex++;
75
+ // Flush remaining comments at EOF
76
+ if (commentBuffer.length > 0) {
77
+ const key = "";
78
+ if (!map.has(key))
79
+ map.set(key, new Set());
80
+ const commentBlock = commentBuffer.join("\n").toLowerCase();
81
+ map.get(key).add(commentBlock);
82
+ }
83
+ return map;
96
84
  }
97
- // Add any remaining original lines if model ran out
98
- while (origIndex < origLines.length) {
99
- fixedLines.push(origLines[origIndex++]);
85
+ // --- Step 1: Collect comments ---
86
+ const modelComments = collectCommentsMap(newLines); // model first
87
+ const origComments = collectCommentsMap(origLines); // original
88
+ // --- Step 2: Remove duplicates ---
89
+ for (const [key, commentSet] of modelComments.entries()) {
90
+ if (origComments.has(key)) {
91
+ commentSet.forEach(c => {
92
+ origComments.get(key).delete(c.trim().toLowerCase());
93
+ });
94
+ if (origComments.get(key).size === 0)
95
+ origComments.delete(key);
96
+ }
100
97
  }
101
- // Add trailing comments from model if any
102
- while (newIndex < newLines.length) {
103
- if (classifyLine(newLines[newIndex]) !== "code") {
104
- fixedLines.push(newLines[newIndex]);
98
+ // --- Step 3: Build fixed lines with model comments inserted above original ---
99
+ const fixedLines = [];
100
+ for (const origLine of origLines) {
101
+ const key = origLine.trim().toLowerCase();
102
+ // Insert model comment blocks if any
103
+ if (modelComments.has(key)) {
104
+ modelComments.get(key).forEach(block => {
105
+ const lines = block.split("\n");
106
+ for (const line of lines) {
107
+ if (!fixedLines.includes(line)) {
108
+ fixedLines.push(line);
109
+ console.log(chalk.blue("Inserted comment:"), line.trim());
110
+ }
111
+ else {
112
+ console.log(chalk.gray("Skipped duplicate comment:"), line.trim());
113
+ }
114
+ }
115
+ });
105
116
  }
106
- newIndex++;
117
+ fixedLines.push(origLine);
107
118
  }
108
119
  // --- Logging for debugging ---
109
120
  console.log(chalk.bold.blue("\n=== LINE CLASSIFICATION (original) ==="));
110
121
  origLines.forEach((line, i) => {
111
122
  const type = classifyLine(line);
112
- const colored = type === "code"
113
- ? chalk.green(line)
114
- : chalk.yellow(line);
123
+ const colored = type === "code" ? chalk.green(line) : chalk.yellow(line);
115
124
  console.log(`${i + 1}: ${colored} ${chalk.gray(`[${type}]`)}`);
116
125
  });
117
126
  console.log(chalk.bold.blue("\n=== LINE CLASSIFICATION (model) ==="));
118
127
  newLines.forEach((line, i) => {
119
128
  const type = classifyLine(line);
120
- const colored = type === "code"
121
- ? chalk.green(line)
122
- : chalk.yellow(line);
129
+ const colored = type === "code" ? chalk.green(line) : chalk.yellow(line);
123
130
  console.log(`${i + 1}: ${colored} ${chalk.gray(`[${type}]`)}`);
124
131
  });
125
132
  console.log(chalk.bold.blue("\n=== FIXED CONTENT ==="));
126
133
  fixedLines.forEach((line, i) => {
127
134
  const type = classifyLine(line);
128
- const colored = type === "code"
129
- ? chalk.green(line)
130
- : chalk.yellow(line);
135
+ const colored = type === "code" ? chalk.green(line) : chalk.yellow(line);
131
136
  console.log(`${i + 1}: ${colored} ${chalk.gray(`[${type}]`)}`);
132
137
  });
133
138
  return { content: fixedLines.join("\n"), filepath };
@@ -4,24 +4,41 @@ export function splitCodeIntoChunks(text, maxTokens) {
4
4
  const chunks = [];
5
5
  let currentChunkLines = [];
6
6
  let currentTokens = 0;
7
+ let inMultiComment = false;
8
+ const start = '/*';
9
+ const end = '*/';
7
10
  for (const line of lines) {
11
+ const trimmed = line.trim();
12
+ // --- Track multi-line comments ---
13
+ if (trimmed.includes(start) && !trimmed.includes(end)) {
14
+ // Starts a block comment but does not end on the same line
15
+ inMultiComment = true;
16
+ }
17
+ else if (trimmed.includes(start) && trimmed.includes(end)) {
18
+ // Inline comment: "/* ... */" on same line → ignore, don't toggle state
19
+ // do nothing with inMultiComment
20
+ }
21
+ else if (trimmed.includes(end)) {
22
+ // End of a block comment
23
+ inMultiComment = false;
24
+ }
8
25
  const lineTokens = encode(line + '\n').length;
9
26
  if (currentTokens + lineTokens > maxTokens) {
10
- // Try to split at a more natural point
27
+ // Split at natural points but never inside a multi-line comment
11
28
  let splitIndex = currentChunkLines.length;
12
29
  for (let i = currentChunkLines.length - 1; i >= 0; i--) {
13
- const trimmed = currentChunkLines[i].trim();
14
- if (trimmed === '' ||
15
- trimmed.startsWith('function ') ||
16
- trimmed.startsWith('class ') ||
17
- trimmed.endsWith('}') ||
18
- trimmed.endsWith(';')) {
30
+ const t = currentChunkLines[i].trim();
31
+ if (!inMultiComment &&
32
+ (t === '' ||
33
+ t.startsWith('function ') ||
34
+ t.startsWith('class ') ||
35
+ t.endsWith('}') ||
36
+ t.endsWith(';'))) {
19
37
  splitIndex = i + 1;
20
38
  break;
21
39
  }
22
40
  }
23
41
  chunks.push(currentChunkLines.slice(0, splitIndex).join('\n'));
24
- // Move leftover lines into the next chunk
25
42
  currentChunkLines = currentChunkLines.slice(splitIndex);
26
43
  currentTokens = encode(currentChunkLines.join('\n')).length;
27
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.98",
3
+ "version": "0.1.100",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"