scai 0.1.97 → 0.1.99

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/CHANGELOG.md CHANGED
@@ -138,7 +138,12 @@ Type handling with the module pipeline
138
138
  * Improved CLI configuration settings with context-aware actions
139
139
  * Improved logging and added active repo change detection
140
140
 
141
- ## 2025-08-23
141
+ ## 2025-08-24
142
142
 
143
- * Improved CLI configuration settings with context-aware actions
144
- * Added CLI configuration settings with context-aware actions and improved logging
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
@@ -9,11 +9,11 @@ import os from 'os';
9
9
  import path from 'path';
10
10
  import { spawnSync } from 'child_process';
11
11
  import columnify from 'columnify';
12
+ import { Spinner } from '../lib/spinner.js'; // adjust path as needed
12
13
  function truncate(str, length) {
13
14
  return str.length > length ? str.slice(0, length - 3) + '...' : str;
14
15
  }
15
16
  // Fetch open PRs with review requested
16
- import { Spinner } from '../lib/spinner.js'; // adjust path as needed
17
17
  export async function getPullRequestsForReview(token, owner, repo, username, branch = 'main', filterForUser = true) {
18
18
  const spinner = new Spinner('Fetching pull requests and diffs...');
19
19
  spinner.start();
@@ -87,7 +87,6 @@ function askUserToPickPR(prs) {
87
87
  ID: `#${pr.number}`,
88
88
  Title: chalk.gray(truncate(pr.title, 50)),
89
89
  Author: chalk.magentaBright(pr.user || '—'),
90
- Status: pr.draft ? 'Draft' : 'Open',
91
90
  Created: pr.created_at?.split('T')[0] || '',
92
91
  'Requested Reviewers': pr.requested_reviewers?.length
93
92
  ? pr.requested_reviewers.join(', ')
@@ -106,11 +105,12 @@ function askUserToPickPR(prs) {
106
105
  columnSplitter: ' ',
107
106
  headingTransform: (h) => chalk.cyan(h.toUpperCase()),
108
107
  config: {
108
+ '#': { maxWidth: 4 },
109
109
  Title: { maxWidth: 50 },
110
110
  'Requested Reviewers': { maxWidth: 30 },
111
111
  'Actual Reviewers': { maxWidth: 30 },
112
112
  Reviews: { maxWidth: 20 },
113
- }
113
+ },
114
114
  }));
115
115
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
116
116
  rl.question(`\n👉 Choose a PR to review [1-${prs.length}]: `, (answer) => {
@@ -130,9 +130,9 @@ function askUserToPickPR(prs) {
130
130
  function askReviewMethod() {
131
131
  return new Promise((resolve) => {
132
132
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
133
- console.log("\n🔍 Choose review method:");
133
+ console.log(chalk.bold("\n🔍 Choose review method:\n"));
134
134
  console.log('1) Review whole PR at once');
135
- console.log('2) Review chunk by chunk');
135
+ console.log('2) Review chunk by chunk\n');
136
136
  rl.question(`👉 Choose an option [1-2]: `, (answer) => {
137
137
  rl.close();
138
138
  resolve(answer === '2' ? 'chunk' : 'whole');
@@ -255,34 +255,134 @@ function colorDiffLine(line) {
255
255
  return chalk.yellow(line);
256
256
  return line;
257
257
  }
258
- // Review a single chunk
258
+ function parseAISuggestions(aiOutput) {
259
+ return aiOutput
260
+ .split(/\n\d+\.\s/) // Split on "1. ", "2. ", "3. "
261
+ .map(s => s.trim())
262
+ .filter(Boolean)
263
+ .map(s => s.replace(/^💬\s*/, ''));
264
+ }
265
+ async function promptAIReviewSuggestions(aiOutput, chunkContent) {
266
+ // Strip first line if it's a summary like "Here are 4 suggestions:"
267
+ const lines = aiOutput.split('\n');
268
+ if (lines.length > 3 && /^here (are|is).*:?\s*$/i.test(lines[0])) {
269
+ aiOutput = lines.slice(1).join('\n').trim();
270
+ }
271
+ let suggestions = parseAISuggestions(aiOutput);
272
+ let selected = null;
273
+ while (!selected) {
274
+ const colorFuncs = [
275
+ chalk.cyan,
276
+ chalk.green,
277
+ chalk.yellow,
278
+ chalk.magenta,
279
+ chalk.blue,
280
+ chalk.red
281
+ ];
282
+ const rows = suggestions.map((s, i) => ({
283
+ No: String(i + 1).padStart(2),
284
+ Suggestion: colorFuncs[i % colorFuncs.length](s) // cycle through colors
285
+ }));
286
+ const rendered = columnify(rows, {
287
+ columns: ['No', 'Suggestion'],
288
+ showHeaders: false,
289
+ columnSplitter: ' ',
290
+ config: {
291
+ No: {
292
+ align: 'right',
293
+ dataTransform: (val) => chalk.cyan.bold(`${val}.`)
294
+ },
295
+ Suggestion: {
296
+ maxWidth: 80,
297
+ dataTransform: (val) => chalk.white(val)
298
+ }
299
+ }
300
+ });
301
+ console.log('\n' + chalk.yellow(chalk.bold('--- Review Suggestions ---')) + '\n');
302
+ console.log(rendered.replace(/\n/g, '\n\n'));
303
+ console.log();
304
+ console.log(chalk.gray('Select an option above or:'));
305
+ console.log(chalk.cyan(' r)') + ' Regenerate suggestions');
306
+ console.log(chalk.cyan(' c)') + ' Write custom review');
307
+ console.log(chalk.cyan(' s)') + ' Skip this chunk');
308
+ console.log(chalk.cyan(' q)') + ' Cancel review');
309
+ const range = `1-${suggestions.length}`;
310
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
311
+ const answer = await new Promise(resolve => rl.question(chalk.bold(`\n👉 Choose [${range},r,c,s,q]: `), resolve));
312
+ rl.close();
313
+ const trimmed = answer.trim().toLowerCase();
314
+ if (['1', '2', '3'].includes(trimmed)) {
315
+ const idx = parseInt(trimmed, 10) - 1;
316
+ selected = suggestions[idx];
317
+ const rlEdit = readline.createInterface({ input: process.stdin, output: process.stdout });
318
+ const editAnswer = await new Promise(resolve => rlEdit.question('✍️ Edit this suggestion before submitting? [y/N]: ', resolve));
319
+ rlEdit.close();
320
+ if (editAnswer.trim().toLowerCase() === 'y') {
321
+ selected = await promptEditReview(selected);
322
+ }
323
+ }
324
+ else if (trimmed === 'r') {
325
+ console.log(chalk.yellow('\nRegenerating suggestions...\n'));
326
+ const newSuggestion = await reviewModule.run({ content: chunkContent });
327
+ const newOutput = newSuggestion.content || aiOutput;
328
+ suggestions = parseAISuggestions(newOutput);
329
+ }
330
+ else if (trimmed === 'c') {
331
+ selected = await promptCustomReview();
332
+ }
333
+ else if (trimmed === 's') {
334
+ return "skip";
335
+ }
336
+ else if (trimmed === 'q') {
337
+ console.log(chalk.red('\nReview cancelled.\n'));
338
+ return "cancel";
339
+ }
340
+ else {
341
+ console.log(chalk.red('\n⚠️ Invalid input. Try again.\n'));
342
+ }
343
+ }
344
+ console.log(chalk.green('\n✅ Selected suggestion:\n'), selected, '\n');
345
+ const action = await askReviewApproval();
346
+ if (action === 'approve')
347
+ return selected;
348
+ if (action === 'reject')
349
+ return selected;
350
+ if (action === 'edit')
351
+ return await promptEditReview(selected);
352
+ if (action === 'custom')
353
+ return await promptCustomReview();
354
+ if (action === 'cancel') {
355
+ console.log(chalk.yellow('Review cancelled.\n'));
356
+ return "cancel";
357
+ }
358
+ return null;
359
+ }
259
360
  export async function reviewChunk(chunk, chunkIndex, totalChunks) {
260
- const lines = chunk.content.split('\n');
261
- const coloredDiff = lines.map(colorDiffLine).join('\n');
262
361
  console.log(chalk.gray('\n' + '━'.repeat(60)));
263
362
  console.log(`📄 ${chalk.bold('File')}: ${chalk.cyan(chunk.filePath)}`);
264
363
  console.log(`🔢 ${chalk.bold('Chunk')}: ${chunkIndex + 1} of ${totalChunks}`);
364
+ // Build colored diff
365
+ const lines = chunk.content.split('\n');
366
+ const coloredDiff = lines.map(colorDiffLine).join('\n');
367
+ // 1️⃣ Run the AI review
265
368
  const suggestion = await reviewModule.run({
266
369
  content: chunk.content,
267
370
  filepath: chunk.filePath
268
371
  });
269
- const summary = suggestion.content?.trim() || 'AI review summary not available.';
270
- console.log(`🔍 ${chalk.bold('Summary')}: ${summary}`);
372
+ const aiOutput = suggestion.content?.trim() || '1. AI review summary not available.';
373
+ // 2️⃣ Show the diff
271
374
  console.log(`\n${chalk.bold('--- Diff ---')}\n`);
272
375
  console.log(coloredDiff);
273
- console.log(`\n${chalk.bold('--- AI Review ---')}\n`);
274
- console.log(chalk.blue(`💬 ${summary}`));
275
- console.log(chalk.gray('━'.repeat(60)));
276
- const choice = await promptChunkReviewMenu();
277
- if (choice === 'edit') {
278
- const edited = await promptEditReview(summary); // edit based on the suggestion
279
- return { choice: edited, summary: edited };
376
+ // 3️⃣ Prompt user to pick/skip/cancel
377
+ const selectedReview = await promptAIReviewSuggestions(aiOutput, chunk.content);
378
+ if (selectedReview === "cancel") {
379
+ return { choice: "cancel", summary: "" };
280
380
  }
281
- else if (choice === 'skip') {
282
- await waitForSpaceOrQ(); // pause between chunks
283
- return { choice: 'cancel', summary }; // skip this one
381
+ if (selectedReview === "skip") {
382
+ await waitForSpaceOrQ();
383
+ return { choice: "skip", summary: "" };
284
384
  }
285
- return { choice, summary };
385
+ return { choice: selectedReview ?? "", summary: selectedReview ?? "" };
286
386
  }
287
387
  function waitForSpaceOrQ() {
288
388
  return new Promise(resolve => {
@@ -371,24 +471,34 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
371
471
  const reviewMethod = chunks.length > 1 ? await askReviewMethod() : 'chunk';
372
472
  let reviewComments = [];
373
473
  if (reviewMethod === 'whole') {
374
- const suggestion = await reviewModule.run({ content: diff, filepath: 'Whole PR Diff' });
375
- console.log(chalk.yellowBright("Suggestion: ", suggestion));
474
+ const result = await reviewModule.run({ content: diff, filepath: 'Whole PR Diff' });
475
+ console.log(chalk.yellowBright("Raw AI output:\n"), result.content);
476
+ // Use the parsed array for selecting or displaying suggestions
477
+ let suggestions = result.suggestions;
478
+ if (suggestions && suggestions.length > 3 && /here (are|is) \d+ suggestions/i.test(suggestions[0])) {
479
+ suggestions = suggestions.slice(1);
480
+ }
376
481
  const finalReviewChoice = await askReviewApproval();
377
482
  let reviewText = '';
483
+ // Pick the first suggestion as default if any exist
484
+ if (suggestions && suggestions.length > 0) {
485
+ reviewText = suggestions[0];
486
+ }
378
487
  if (finalReviewChoice === 'approve') {
379
488
  reviewText = 'PR approved';
380
- await submitReview(pr.number, suggestion.content, 'APPROVE');
489
+ await submitReview(pr.number, reviewText, 'APPROVE');
381
490
  }
382
491
  else if (finalReviewChoice === 'reject') {
383
492
  reviewText = 'Changes requested';
384
- await submitReview(pr.number, suggestion.content, 'REQUEST_CHANGES');
493
+ await submitReview(pr.number, reviewText, 'REQUEST_CHANGES');
385
494
  }
386
495
  else if (finalReviewChoice === 'custom') {
387
496
  reviewText = await promptCustomReview();
388
497
  await submitReview(pr.number, reviewText, 'COMMENT');
389
498
  }
390
499
  else if (finalReviewChoice === 'edit') {
391
- reviewText = await promptEditReview(suggestion.content);
500
+ // let user edit the AI suggestion
501
+ reviewText = await promptEditReview(reviewText);
392
502
  await submitReview(pr.number, reviewText, 'COMMENT');
393
503
  }
394
504
  }
@@ -398,7 +508,11 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
398
508
  for (let i = 0; i < chunks.length; i++) {
399
509
  const chunk = chunks[i];
400
510
  const { choice, summary } = await reviewChunk(chunk, i, chunks.length);
401
- if (choice === 'cancel' || choice === 'skip') {
511
+ if (choice === 'cancel') {
512
+ console.log(chalk.red(`🚫 Review cancelled at chunk ${i + 1}`));
513
+ return; // exit reviewPullRequestCmd early
514
+ }
515
+ if (choice === 'skip') {
402
516
  console.log(chalk.gray(`⏭️ Skipped chunk ${i + 1}`));
403
517
  continue;
404
518
  }
@@ -3,11 +3,11 @@ import path from 'path';
3
3
  import readline from 'readline';
4
4
  import { queryFiles, indexFile } from '../db/fileIndex.js';
5
5
  import { summaryModule } from '../pipeline/modules/summaryModule.js';
6
- import { styleOutput } from '../utils/summarizer.js';
7
6
  import { detectFileType } from '../fileRules/detectFileType.js';
8
7
  import { generateEmbedding } from '../lib/generateEmbedding.js';
9
8
  import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
10
9
  import { getDbForRepo } from '../db/client.js';
10
+ import { styleText } from '../utils/outputFormatter.js';
11
11
  export async function summarizeFile(filepath) {
12
12
  let content = '';
13
13
  let filePathResolved;
@@ -40,7 +40,7 @@ export async function summarizeFile(filepath) {
40
40
  const match = matches.find(row => path.resolve(row.path) === filePathResolved);
41
41
  if (match?.summary) {
42
42
  console.log(`🧠 Cached summary for ${filepath}:\n`);
43
- console.log(styleOutput(match.summary));
43
+ console.log(styleText(match.summary));
44
44
  return;
45
45
  }
46
46
  try {
@@ -73,7 +73,7 @@ export async function summarizeFile(filepath) {
73
73
  console.warn('⚠️ No summary generated.');
74
74
  return;
75
75
  }
76
- console.log(styleOutput(response.summary));
76
+ console.log(styleText(response.summary));
77
77
  if (filePathResolved) {
78
78
  const fileType = detectFileType(filePathResolved);
79
79
  indexFile(filePathResolved, response.summary, fileType);
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();
@@ -82,7 +82,6 @@ async function ensureOllamaRunning() {
82
82
  return;
83
83
  }
84
84
  console.log(chalk.yellow('⚙️ Ollama is not running. Attempting to start it...'));
85
- let ollamaStarted = false;
86
85
  try {
87
86
  const child = spawn('ollama', ['serve'], {
88
87
  detached: true,
@@ -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 };
@@ -6,21 +6,32 @@ export const reviewModule = {
6
6
  async run({ content, filepath }) {
7
7
  const model = Config.getModel();
8
8
  const prompt = `
9
- You are a senior software engineer reviewing a pull request.
10
- ALWAYS make 3 concise suggestions for improvements based on the input code diff.
11
- Use this format ONLY and output ONLY those suggestions:
9
+ Suggest ALWAYS 3 concise suggestions for improvements based on the input code diff.
12
10
 
13
- 1. ...
14
- 2. ...
15
- 3. ...
11
+ - Use one of these types for each suggestion: style, refactor, bug, docs, test
12
+ - Keep each message short, clear, and actionable
13
+
14
+ Format your response exactly as:
15
+
16
+ 1. <type>: <message>
17
+ 2. <type>: <message>
18
+ 3. <type>: <message>
16
19
 
17
20
  Changes:
18
21
  ${content}
19
22
  `.trim();
20
23
  const response = await generate({ content: prompt, filepath }, model);
24
+ // Parse response: only keep numbered lines
25
+ const lines = response.content
26
+ .split('\n')
27
+ .map(line => line.trim())
28
+ .filter(line => /^\d+\.\s+/.test(line));
29
+ // Remove numbering and any surrounding quotes
30
+ const suggestions = lines.map(line => line.replace(/^\d+\.\s+/, '').replace(/^"(.*)"$/, '$1').trim());
21
31
  return {
22
32
  content: response.content,
23
33
  filepath,
34
+ suggestions
24
35
  };
25
36
  }
26
37
  };
@@ -0,0 +1,53 @@
1
+ import columnify from "columnify";
2
+ /**
3
+ * Format structured rows for terminal output.
4
+ */
5
+ export function styleRows(rows, options = {}) {
6
+ if (!rows || rows.length === 0)
7
+ return "";
8
+ const terminalWidth = process.stdout.columns || 80;
9
+ const { maxWidthFraction = 2 / 3 } = options;
10
+ return columnify(rows, {
11
+ columnSplitter: " ",
12
+ maxLineWidth: terminalWidth,
13
+ config: Object.fromEntries(Object.keys(rows[0]).map((key) => [
14
+ key,
15
+ { maxWidth: Math.floor(terminalWidth * maxWidthFraction), align: "left" },
16
+ ])),
17
+ });
18
+ }
19
+ /**
20
+ * Format a plain string for terminal output (wraps lines to terminal width).
21
+ */
22
+ export function styleText(text, options = {}) {
23
+ const terminalWidth = process.stdout.columns || 80;
24
+ const { maxWidthFraction = 2 / 3 } = options;
25
+ const maxWidth = Math.floor(terminalWidth * maxWidthFraction);
26
+ // Wrap each line to maxWidth
27
+ const wrapped = text
28
+ .split("\n")
29
+ .map((line) => {
30
+ if (line.length <= maxWidth)
31
+ return line;
32
+ const chunks = [];
33
+ let start = 0;
34
+ while (start < line.length) {
35
+ chunks.push(line.slice(start, start + maxWidth));
36
+ start += maxWidth;
37
+ }
38
+ return chunks.join("\n");
39
+ })
40
+ .join("\n");
41
+ return wrapped;
42
+ }
43
+ /**
44
+ * Parse numbered suggestions or key-value text into rows for columnify.
45
+ */
46
+ export function parseAndStyleSuggestions(text, options = {}) {
47
+ // Simple parser: each line becomes { No: n, Suggestion: line }
48
+ const rows = text
49
+ .trim()
50
+ .split("\n")
51
+ .map((line, idx) => ({ No: (idx + 1).toString(), Suggestion: line }));
52
+ return styleRows(rows, options);
53
+ }
@@ -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.97",
3
+ "version": "0.1.99",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -1,17 +0,0 @@
1
- import columnify from "columnify";
2
- export function styleOutput(summaryText) {
3
- const terminalWidth = process.stdout.columns || 80;
4
- // Split by line to simulate multiple rows instead of one long wrapped field
5
- const lines = summaryText.trim().split('\n').map(line => ({ Summary: line }));
6
- const formatted = columnify(lines, {
7
- columnSplitter: ' ',
8
- maxLineWidth: terminalWidth,
9
- config: {
10
- Summary: {
11
- maxWidth: Math.floor((terminalWidth * 2) / 3),
12
- align: "left",
13
- },
14
- },
15
- });
16
- return formatted;
17
- }