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 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
- // 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/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
- // Fail if no repoKey & no indexDir
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
- console.log(chalk.yellow(`ℹ️ Initializing new repo config for: ${cwd}`));
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
- console.log(chalk.yellow("\n🔁 Updating context...\n"));
28
- console.log(`✅ Active repo: ${chalk.green(repoKey)}`);
29
- console.log(`✅ Index dir: ${chalk.cyan(repoCfg.indexDir || cwd)}`);
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 scaiRepoRoot = path.join(path.dirname(repoCfg.indexDir || cwd), repoKey);
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
- const showAll = cmd.all;
57
- await reviewPullRequestCmd('main', showAll);
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) => suggestCommitMessage(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
- checkGit();
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
- try {
77
- const token = Config.getGitHubToken();
78
- if (!token) {
79
- console.log('❌ GitHub authentication not found. Please set your token.');
80
- return;
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
- const result = await validateGitHubTokenAgainstRepo();
83
- console.log(result);
84
- }
85
- catch (err) {
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
- Config.setGitHubToken('');
94
- console.log('🔄 GitHub authentication has been reset.');
95
- const token = Config.getGitHubToken();
96
- console.log(token ? '❌ Token still exists in the configuration.' : '✅ Token successfully removed.');
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
- const token = await promptForToken();
103
- Config.setGitHubToken(token.trim());
104
- console.log('🔑 GitHub token set successfully.');
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
- const files = await resolveTargetsToFiles(targets, [".ts", ".js"]);
113
- for (const file of files) {
114
- await handleAgentRun(file, [addCommentsModule, preserveCodeModule]);
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 handleStandaloneChangelogUpdate();
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) => summarizeFile(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
- handleAgentRun(file, [generateTestsModule]);
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
- Config.setModel(model);
141
- Config.show();
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
- Config.setLanguage(lang);
148
- Config.show();
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(runIndexCommand);
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(); // 👈 simple and clean
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(() => resetDatabase());
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(runMigrateCommand);
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(runBackupCommand);
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(runFindCommand);
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')
@@ -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,
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
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
- }