scai 0.1.96 → 0.1.98
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 +10 -1
- package/dist/commands/ReviewCmd.js +141 -27
- package/dist/commands/SummaryCmd.js +3 -3
- package/dist/context.js +16 -11
- package/dist/index.js +126 -76
- package/dist/modelSetup.js +0 -1
- package/dist/pipeline/modules/reviewModule.js +17 -6
- package/dist/utils/outputFormatter.js +53 -0
- package/package.json +1 -1
- package/dist/utils/summarizer.js +0 -17
package/dist/CHANGELOG.md
CHANGED
|
@@ -131,4 +131,13 @@ Type handling with the module pipeline
|
|
|
131
131
|
|
|
132
132
|
• Add DB-related commands to db subcommand (check, reset, migrate)
|
|
133
133
|
• Update table view limits for files and functions in dbcheck.ts
|
|
134
|
-
• Improved resetDatabase command with confirmation prompt before deleting database
|
|
134
|
+
• Improved resetDatabase command with confirmation prompt before deleting database
|
|
135
|
+
|
|
136
|
+
## 2025-08-23
|
|
137
|
+
|
|
138
|
+
* Improved CLI configuration settings with context-aware actions
|
|
139
|
+
* Improved logging and added active repo change detection
|
|
140
|
+
|
|
141
|
+
## 2025-08-24
|
|
142
|
+
|
|
143
|
+
• Improved CLI review command with AI-generated suggestions and enhanced user interface.
|
|
@@ -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/context.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { readConfig, writeConfig } from "./config.js";
|
|
3
3
|
import { normalizePath } from "./utils/normalizePath.js";
|
|
4
4
|
import { getHashedRepoKey } from "./utils/repoKey.js";
|
|
5
|
-
import { getDbForRepo } from "./db/client.js";
|
|
6
|
-
import path from "path";
|
|
5
|
+
import { getDbForRepo, getDbPathForRepo } from "./db/client.js";
|
|
7
6
|
import fs from "fs";
|
|
8
7
|
import chalk from "chalk";
|
|
9
8
|
export async function updateContext() {
|
|
@@ -11,33 +10,38 @@ export async function updateContext() {
|
|
|
11
10
|
const cfg = readConfig();
|
|
12
11
|
// 🔑 Find repoKey by matching indexDir to cwd
|
|
13
12
|
let repoKey = Object.keys(cfg.repos || {}).find((key) => normalizePath(cfg.repos[key]?.indexDir || "") === cwd);
|
|
14
|
-
//
|
|
13
|
+
// Initialize new repo config if not found
|
|
14
|
+
let isNewRepo = false;
|
|
15
15
|
if (!repoKey) {
|
|
16
16
|
repoKey = getHashedRepoKey(cwd);
|
|
17
17
|
if (!cfg.repos[repoKey])
|
|
18
18
|
cfg.repos[repoKey] = {};
|
|
19
19
|
cfg.repos[repoKey].indexDir = cwd;
|
|
20
|
-
|
|
20
|
+
isNewRepo = true;
|
|
21
21
|
}
|
|
22
|
+
// Check if active repo has changed
|
|
23
|
+
const activeRepoChanged = cfg.activeRepo !== repoKey;
|
|
22
24
|
// Always set this as active repo
|
|
23
25
|
cfg.activeRepo = repoKey;
|
|
24
26
|
writeConfig(cfg);
|
|
25
27
|
const repoCfg = cfg.repos[repoKey];
|
|
26
28
|
let ok = true;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// Only log detailed info if new repo or active repo changed
|
|
30
|
+
if (isNewRepo || activeRepoChanged) {
|
|
31
|
+
console.log(chalk.yellow("\n🔁 Updating context...\n"));
|
|
32
|
+
console.log(`✅ Active repo: ${chalk.green(repoKey)}`);
|
|
33
|
+
console.log(`✅ Index dir: ${chalk.cyan(repoCfg.indexDir || cwd)}`);
|
|
34
|
+
}
|
|
30
35
|
// GitHub token is optional
|
|
31
36
|
const token = repoCfg.githubToken || cfg.githubToken;
|
|
32
37
|
if (!token) {
|
|
33
38
|
console.log(`ℹ️ No GitHub token found. You can set one with the: ${chalk.bold(chalk.bgGreen("scai auth set"))} command`);
|
|
34
39
|
}
|
|
35
|
-
else {
|
|
40
|
+
else if (isNewRepo || activeRepoChanged) {
|
|
36
41
|
console.log(`✅ GitHub token present`);
|
|
37
42
|
}
|
|
38
43
|
// Ensure DB exists
|
|
39
|
-
const
|
|
40
|
-
const dbPath = path.join(scaiRepoRoot, "db.sqlite");
|
|
44
|
+
const dbPath = getDbPathForRepo();
|
|
41
45
|
if (!fs.existsSync(dbPath)) {
|
|
42
46
|
console.log(chalk.yellow(`📦 Initializing DB at ${dbPath}`));
|
|
43
47
|
try {
|
|
@@ -47,9 +51,10 @@ export async function updateContext() {
|
|
|
47
51
|
ok = false; // DB init failed
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
|
-
else {
|
|
54
|
+
else if (isNewRepo || activeRepoChanged) {
|
|
51
55
|
console.log(chalk.green("✅ Database present"));
|
|
52
56
|
}
|
|
57
|
+
// Final context status
|
|
53
58
|
if (ok) {
|
|
54
59
|
console.log(chalk.bold.green("\n✅ Context OK\n"));
|
|
55
60
|
}
|
package/dist/index.js
CHANGED
|
@@ -53,55 +53,70 @@ git
|
|
|
53
53
|
.description('Review an open pull request using AI')
|
|
54
54
|
.option('-a, --all', 'Show all PRs requiring a review (not just for the current user)', false)
|
|
55
55
|
.action(async (cmd) => {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
await withContext(async () => {
|
|
57
|
+
const showAll = cmd.all;
|
|
58
|
+
await reviewPullRequestCmd('main', showAll);
|
|
59
|
+
});
|
|
58
60
|
});
|
|
59
61
|
git
|
|
60
62
|
.command('commit')
|
|
61
63
|
.description('Suggest a commit message from staged changes and optionally commit')
|
|
62
64
|
.option('-l, --changelog', 'Generate and optionally stage a changelog entry')
|
|
63
|
-
.action((options) =>
|
|
65
|
+
.action(async (options) => {
|
|
66
|
+
await withContext(async () => {
|
|
67
|
+
suggestCommitMessage(options);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
64
70
|
git
|
|
65
71
|
.command('check')
|
|
66
72
|
.description('Check Git working directory and branch status')
|
|
67
|
-
.action(() => {
|
|
68
|
-
|
|
73
|
+
.action(async () => {
|
|
74
|
+
await withContext(async () => {
|
|
75
|
+
checkGit();
|
|
76
|
+
});
|
|
69
77
|
});
|
|
70
78
|
// Add auth-related commands
|
|
71
79
|
const auth = cmd.command('auth').description('GitHub authentication commands');
|
|
80
|
+
// ⚡ Auth commands
|
|
72
81
|
auth
|
|
73
82
|
.command('check')
|
|
74
83
|
.description('Check if GitHub authentication is set up and valid')
|
|
75
84
|
.action(async () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
await withContext(async () => {
|
|
86
|
+
try {
|
|
87
|
+
const token = Config.getGitHubToken();
|
|
88
|
+
if (!token) {
|
|
89
|
+
console.log('❌ GitHub authentication not found. Please set your token.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const result = await validateGitHubTokenAgainstRepo();
|
|
93
|
+
console.log(result);
|
|
81
94
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
console.error(typeof err === 'string' ? err : err.message);
|
|
87
|
-
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.error(typeof err === 'string' ? err : err.message);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
88
99
|
});
|
|
89
100
|
auth
|
|
90
101
|
.command('reset')
|
|
91
102
|
.description('Reset GitHub authentication credentials')
|
|
92
|
-
.action(() => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
.action(async () => {
|
|
104
|
+
await withContext(async () => {
|
|
105
|
+
Config.setGitHubToken('');
|
|
106
|
+
console.log('🔄 GitHub authentication has been reset.');
|
|
107
|
+
const token = Config.getGitHubToken();
|
|
108
|
+
console.log(token ? '❌ Token still exists in the configuration.' : '✅ Token successfully removed.');
|
|
109
|
+
});
|
|
97
110
|
});
|
|
98
111
|
auth
|
|
99
112
|
.command('set')
|
|
100
113
|
.description('Set your GitHub Personal Access Token')
|
|
101
114
|
.action(async () => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
await withContext(async () => {
|
|
116
|
+
const token = await promptForToken();
|
|
117
|
+
Config.setGitHubToken(token.trim());
|
|
118
|
+
console.log('🔑 GitHub token set successfully.');
|
|
119
|
+
});
|
|
105
120
|
});
|
|
106
121
|
// 🛠️ Group: `gen` commands for content generation
|
|
107
122
|
const gen = cmd.command('gen').description('Generate code-related output');
|
|
@@ -109,43 +124,57 @@ gen
|
|
|
109
124
|
.command("comm <targets...>")
|
|
110
125
|
.description("Write comments for the given file(s) or folder(s)")
|
|
111
126
|
.action(async (targets) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
await withContext(async () => {
|
|
128
|
+
const files = await resolveTargetsToFiles(targets, [".ts", ".js"]);
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
await handleAgentRun(file, [addCommentsModule, preserveCodeModule]);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
116
133
|
});
|
|
117
134
|
gen
|
|
118
135
|
.command('changelog')
|
|
119
136
|
.description('Update or create the CHANGELOG.md based on current Git diff')
|
|
120
137
|
.action(async () => {
|
|
121
|
-
await
|
|
138
|
+
await withContext(async () => {
|
|
139
|
+
await handleStandaloneChangelogUpdate();
|
|
140
|
+
});
|
|
122
141
|
});
|
|
123
142
|
gen
|
|
124
143
|
.command('summ [file]')
|
|
125
144
|
.description('Print a summary of the given file to the terminal')
|
|
126
|
-
.action((file) =>
|
|
145
|
+
.action(async (file) => {
|
|
146
|
+
await withContext(async () => {
|
|
147
|
+
summarizeFile(file);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
127
150
|
gen
|
|
128
151
|
.command('testgen <file>')
|
|
129
152
|
.description('Generate tests for the given file')
|
|
130
153
|
.option('-a, --apply', 'Apply the output to the original file')
|
|
131
|
-
.action((file) => {
|
|
132
|
-
|
|
154
|
+
.action(async (file) => {
|
|
155
|
+
await withContext(async () => {
|
|
156
|
+
handleAgentRun(file, [generateTestsModule]);
|
|
157
|
+
});
|
|
133
158
|
});
|
|
134
159
|
// ⚙️ Group: Configuration settings
|
|
135
160
|
const config = cmd.command('config').description('Manage SCAI configuration');
|
|
136
161
|
config
|
|
137
162
|
.command('set-model <model>')
|
|
138
163
|
.description('Set the model to use')
|
|
139
|
-
.action((model) => {
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
.action(async (model) => {
|
|
165
|
+
await withContext(async () => {
|
|
166
|
+
Config.setModel(model);
|
|
167
|
+
Config.show();
|
|
168
|
+
});
|
|
142
169
|
});
|
|
143
170
|
config
|
|
144
171
|
.command('set-lang <lang>')
|
|
145
172
|
.description('Set the programming language')
|
|
146
|
-
.action((lang) => {
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
.action(async (lang) => {
|
|
174
|
+
await withContext(async () => {
|
|
175
|
+
Config.setLanguage(lang);
|
|
176
|
+
Config.show();
|
|
177
|
+
});
|
|
149
178
|
});
|
|
150
179
|
config
|
|
151
180
|
.command("show")
|
|
@@ -165,37 +194,39 @@ const index = cmd.command('index').description('index operations');
|
|
|
165
194
|
index
|
|
166
195
|
.command('start')
|
|
167
196
|
.description('Index supported files in the configured index directory')
|
|
168
|
-
.action(
|
|
197
|
+
.action(async () => await withContext(async () => {
|
|
198
|
+
await runIndexCommand();
|
|
199
|
+
}));
|
|
169
200
|
index
|
|
170
201
|
.command('set [dir]')
|
|
171
202
|
.description('Set and activate index directory')
|
|
172
|
-
.action((dir = process.cwd()) => {
|
|
203
|
+
.action(async (dir = process.cwd()) => await withContext(async () => {
|
|
173
204
|
Config.setIndexDir(dir);
|
|
174
205
|
Config.show();
|
|
175
|
-
});
|
|
206
|
+
}));
|
|
176
207
|
index
|
|
177
208
|
.command('list')
|
|
178
209
|
.description('List all indexed repositories')
|
|
179
|
-
.action(() => {
|
|
180
|
-
Config.printAllRepos();
|
|
181
|
-
});
|
|
210
|
+
.action(async () => await withContext(async () => {
|
|
211
|
+
await Config.printAllRepos();
|
|
212
|
+
}));
|
|
182
213
|
index
|
|
183
214
|
.command('switch')
|
|
184
215
|
.description('Switch active repository (by interactive list only)')
|
|
185
|
-
.action(() => {
|
|
186
|
-
runInteractiveSwitch();
|
|
187
|
-
});
|
|
216
|
+
.action(async () => await withContext(async () => {
|
|
217
|
+
await runInteractiveSwitch();
|
|
218
|
+
}));
|
|
188
219
|
index
|
|
189
220
|
.command('delete')
|
|
190
221
|
.description('Delete a repository from the index (interactive)')
|
|
191
|
-
.action(() => {
|
|
192
|
-
runInteractiveDelete();
|
|
193
|
-
});
|
|
222
|
+
.action(async () => await withContext(async () => {
|
|
223
|
+
await runInteractiveDelete();
|
|
224
|
+
}));
|
|
194
225
|
const db = cmd.command('db').description('Database operations');
|
|
195
226
|
db
|
|
196
227
|
.command('check')
|
|
197
228
|
.description('Run the dbcheck script to check the database status')
|
|
198
|
-
.action(() => {
|
|
229
|
+
.action(async () => await withContext(async () => {
|
|
199
230
|
const __filename = fileURLToPath(import.meta.url);
|
|
200
231
|
const __dirname = dirname(__filename);
|
|
201
232
|
const scriptPath = resolve(__dirname, '..', 'dist/scripts', 'dbcheck.js');
|
|
@@ -206,47 +237,66 @@ db
|
|
|
206
237
|
catch (err) {
|
|
207
238
|
console.error('❌ Error running dbcheck script:', err instanceof Error ? err.message : err);
|
|
208
239
|
}
|
|
209
|
-
});
|
|
240
|
+
}));
|
|
210
241
|
db
|
|
211
242
|
.command('reset')
|
|
212
243
|
.description('Delete and reset the SQLite database')
|
|
213
|
-
.action(() =>
|
|
244
|
+
.action(async () => await withContext(async () => {
|
|
245
|
+
await resetDatabase();
|
|
246
|
+
}));
|
|
214
247
|
db
|
|
215
248
|
.command('migrate')
|
|
216
249
|
.description('Run DB migration scripts')
|
|
217
|
-
.action(
|
|
250
|
+
.action(async () => await withContext(async () => {
|
|
251
|
+
await runMigrateCommand();
|
|
252
|
+
}));
|
|
253
|
+
db
|
|
254
|
+
.command('inspect')
|
|
255
|
+
.argument('<filepath>', 'Path to the file to inspect')
|
|
256
|
+
.description('Inspect a specific file and print its indexed summary and functions')
|
|
257
|
+
.action(async (filepath) => await withContext(async () => {
|
|
258
|
+
await runInspectCommand(filepath);
|
|
259
|
+
}));
|
|
260
|
+
const daemon = cmd
|
|
261
|
+
.command('daemon')
|
|
262
|
+
.description('Background summarizer operations');
|
|
263
|
+
// Start the daemon
|
|
264
|
+
daemon
|
|
265
|
+
.command('start')
|
|
266
|
+
.description('Run background summarization of indexed files')
|
|
267
|
+
.action(async () => {
|
|
268
|
+
await withContext(async () => {
|
|
269
|
+
await startDaemon();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
// Stop the daemon
|
|
273
|
+
daemon
|
|
274
|
+
.command('stop')
|
|
275
|
+
.description('Stop the background summarizer daemon')
|
|
276
|
+
.action(async () => {
|
|
277
|
+
await withContext(async () => {
|
|
278
|
+
await runStopDaemonCommand();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
218
281
|
cmd
|
|
219
282
|
.command('backup')
|
|
220
283
|
.description('Backup the current .scai folder')
|
|
221
|
-
.action(
|
|
284
|
+
.action(async () => await withContext(async () => {
|
|
285
|
+
await runBackupCommand();
|
|
286
|
+
}));
|
|
222
287
|
cmd
|
|
223
288
|
.command('find <query>')
|
|
224
289
|
.description('Search indexed files by keyword')
|
|
225
|
-
.action(
|
|
290
|
+
.action(async (query) => await withContext(async () => {
|
|
291
|
+
await runFindCommand(query);
|
|
292
|
+
}));
|
|
226
293
|
cmd
|
|
227
294
|
.command('ask [question...]')
|
|
228
295
|
.description('Ask a question based on indexed files')
|
|
229
|
-
.action((questionParts) => {
|
|
296
|
+
.action(async (questionParts) => await withContext(async () => {
|
|
230
297
|
const fullQuery = questionParts?.join(' ');
|
|
231
|
-
runAskCommand(fullQuery);
|
|
232
|
-
});
|
|
233
|
-
cmd
|
|
234
|
-
.command('daemon')
|
|
235
|
-
.description('Run background summarization of indexed files')
|
|
236
|
-
.action(async () => {
|
|
237
|
-
await startDaemon();
|
|
238
|
-
});
|
|
239
|
-
cmd
|
|
240
|
-
.command('stop-daemon')
|
|
241
|
-
.description('Stop the background summarizer daemon')
|
|
242
|
-
.action(runStopDaemonCommand);
|
|
243
|
-
cmd
|
|
244
|
-
.command('inspect')
|
|
245
|
-
.argument('<filepath>', 'Path to the file to inspect')
|
|
246
|
-
.description('Inspect a specific file and print its indexed summary and functions')
|
|
247
|
-
.action(async (filepath) => {
|
|
248
|
-
await runInspectCommand(filepath);
|
|
249
|
-
});
|
|
298
|
+
await runAskCommand(fullQuery);
|
|
299
|
+
}));
|
|
250
300
|
cmd
|
|
251
301
|
.command('pipe')
|
|
252
302
|
.description('Run a module pipeline on a given file')
|
package/dist/modelSetup.js
CHANGED
|
@@ -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
|
+
}
|
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
|
-
}
|