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 +8 -3
- package/dist/commands/ReviewCmd.js +141 -27
- package/dist/commands/SummaryCmd.js +3 -3
- package/dist/config.js +2 -2
- package/dist/lib/generate.js +1 -3
- package/dist/modelSetup.js +0 -1
- package/dist/pipeline/modules/preserveCodeModule.js +84 -79
- package/dist/pipeline/modules/reviewModule.js +17 -6
- package/dist/utils/outputFormatter.js +53 -0
- package/dist/utils/splitCodeIntoChunk.js +25 -8
- package/package.json +1 -1
- package/dist/utils/summarizer.js +0 -17
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-
|
|
141
|
+
## 2025-08-24
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
|
270
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
282
|
-
await waitForSpaceOrQ();
|
|
283
|
-
return { choice:
|
|
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
|
|
375
|
-
console.log(chalk.yellowBright("
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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'
|
|
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(
|
|
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(
|
|
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: '
|
|
10
|
-
contextLength:
|
|
9
|
+
model: 'codellama:13b',
|
|
10
|
+
contextLength: 4096,
|
|
11
11
|
language: 'ts',
|
|
12
12
|
indexDir: '',
|
|
13
13
|
githubToken: '',
|
package/dist/lib/generate.js
CHANGED
|
@@ -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();
|
package/dist/modelSetup.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
25
|
+
return line; // return actual line
|
|
25
26
|
}
|
|
26
|
-
// Multi-line
|
|
27
|
+
// --- Multi-line comment ---
|
|
27
28
|
for (const { start, end } of syntax.multiLine) {
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
39
|
+
else {
|
|
40
|
+
blockLines.push(line);
|
|
36
41
|
if (trimmed.includes(end)) {
|
|
37
42
|
inBlockComment = false;
|
|
38
|
-
|
|
43
|
+
const fullBlock = blockLines.join("\n");
|
|
44
|
+
blockLines = [];
|
|
45
|
+
return fullBlock; // return entire multi-line block
|
|
39
46
|
}
|
|
40
|
-
return "
|
|
47
|
+
return ""; // middle lines, wait until block ends
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
return "code";
|
|
44
51
|
};
|
|
45
|
-
//
|
|
46
|
-
function
|
|
47
|
-
const
|
|
48
|
-
let
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
//
|
|
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
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
package/dist/utils/summarizer.js
DELETED
|
@@ -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
|
-
}
|