scai 0.1.55 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,6 +57,7 @@ scai runs entirely on your machine and doesn't require cloud APIs or API keys. T
57
57
  No more struggling to write pull request descriptions by hand. `scai git review` automatically generates a rich summary of your changes, complete with context, suggestions, and rationale.
58
58
 
59
59
  > ⚠️ These features are in **beta** — feedback welcome!
60
+ Ping [@ticcr](https://bsky.app/profile/ticcr.xyz) on Bluesky — I'd love to hear your thoughts!
60
61
 
61
62
  ---
62
63
 
@@ -87,7 +88,7 @@ To interact with GitHub and create pull requests, `scai` needs a personal access
87
88
  3. **Set the index dir:**
88
89
 
89
90
  ```bash
90
- scai set index-dir <repo path>
91
+ scai index set /path/to/repo
91
92
  ```
92
93
 
93
94
  This is the repo from which scai will look up pull requests that can be reviewed.
@@ -116,10 +117,16 @@ SCAI supports an integrated review flow for GitHub pull requests. To get started
116
117
  1. **Set your working index directory (once per repo):**
117
118
 
118
119
  ```sh
119
- scai set index-dir .
120
+ scai index set /path/to/repo
120
121
  ```
121
122
 
122
123
  2. **Authenticate with GitHub:**
124
+ ```sh
125
+ scai git review
126
+ ```
127
+
128
+ This command will query you for the Personal Access Token and set it for you.
129
+ You may also do this with the auth commands below
123
130
 
124
131
  ```sh
125
132
  scai auth set
@@ -170,25 +177,19 @@ You might consider renaming `sessionManager` to better reflect its dual role in
170
177
 
171
178
 
172
179
 
173
- ### 🔧 How to Use `scai git sugg`
180
+ ### 🔧 How to Use `scai git commit`
174
181
 
175
182
  Use AI to suggest a meaningful commit message based on your staged code:
176
183
 
177
184
  ```bash
178
185
  git add .
179
- scai git sugg
180
- ```
181
-
182
- To automatically commit with the selected suggestion:
183
-
184
- ```bash
185
- scai git sugg --commit
186
+ scai git commit
186
187
  ```
187
188
 
188
189
  You can also include a changelog entry along with the commit:
189
190
 
190
191
  ```bash
191
- scai git sugg --commit --changelog
192
+ scai git commit --changelog
192
193
  ```
193
194
 
194
195
  This will:
@@ -287,13 +288,13 @@ You won't gain much value from the index unless you scope it to one repository.
287
288
  1. **Set index directory:**
288
289
 
289
290
  ```bash
290
- scai set index-dir /path/to/repo
291
+ scai index set /path/to/repo
291
292
  ```
292
293
 
293
294
  2. **Index your repo (once):**
294
295
 
295
296
  ```bash
296
- scai index
297
+ scai index start
297
298
  ```
298
299
 
299
300
  3. The daemon is designed to **consume minimal resources** and run unobtrusively. You can control it with:
@@ -7,7 +7,7 @@ import { generate } from '../lib/generate.js';
7
7
  import { buildContextualPrompt } from '../utils/buildContextualPrompt.js';
8
8
  import { generateFocusedFileTree } from '../utils/fileTree.js';
9
9
  import { log } from '../utils/log.js';
10
- import { PROMPT_LOG_PATH, SCAI_HOME, INDEX_DIR, RELATED_FILES_LIMIT, MAX_SUMMARY_LINES } from '../constants.js';
10
+ import { PROMPT_LOG_PATH, SCAI_HOME, RELATED_FILES_LIMIT, MAX_SUMMARY_LINES, getIndexDir } from '../constants.js';
11
11
  export async function runAskCommand(query) {
12
12
  if (!query) {
13
13
  query = await promptOnce('💬 Ask your question:\n');
@@ -17,7 +17,7 @@ export async function runAskCommand(query) {
17
17
  console.error('❌ No question provided.\n👉 Usage: scai ask "your question"');
18
18
  return;
19
19
  }
20
- console.log(`📁 Using index root: ${INDEX_DIR}`);
20
+ console.log(`📁 Using index root: ${getIndexDir}`);
21
21
  console.log(`🔍 Searching for: "${query}"\n`);
22
22
  // 🟩 STEP 1: Semantic Search
23
23
  const start = Date.now();
@@ -103,7 +103,7 @@ export async function runAskCommand(query) {
103
103
  // 🟩 STEP 6: Generate file tree
104
104
  let fileTree = '';
105
105
  try {
106
- fileTree = generateFocusedFileTree(INDEX_DIR, filepath, 2);
106
+ fileTree = generateFocusedFileTree(filepath, 2);
107
107
  }
108
108
  catch (e) {
109
109
  console.warn('⚠️ Could not generate file tree:', e);
@@ -63,6 +63,7 @@ export async function suggestCommitMessage(options) {
63
63
  console.log('⚠️ No staged changes to suggest a message for.');
64
64
  return;
65
65
  }
66
+ // Handle changelog generation if the flag is provided
66
67
  if (options.changelog) {
67
68
  let entryFinalized = false;
68
69
  while (!entryFinalized) {
@@ -122,6 +123,7 @@ export async function suggestCommitMessage(options) {
122
123
  console.log("👉 Please stage your changes with 'git add <files>' and rerun the command.");
123
124
  return;
124
125
  }
126
+ // Automatically commit the suggested message
125
127
  execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
126
128
  console.log('✅ Committed with selected message.');
127
129
  }
@@ -1,3 +1,4 @@
1
+ // indexCmd.ts
1
2
  import fg from 'fast-glob';
2
3
  import path from 'path';
3
4
  import { initSchema } from '../db/schema.js';
@@ -6,14 +7,13 @@ import { detectFileType } from '../fileRules/detectFileType.js';
6
7
  import { startDaemon } from './DaemonCmd.js';
7
8
  import { IGNORED_FOLDER_GLOBS } from '../fileRules/ignoredPaths.js';
8
9
  import { Config } from '../config.js';
9
- import { DB_PATH } from '../constants.js';
10
10
  import { log } from '../utils/log.js';
11
11
  import lockfile from 'proper-lockfile';
12
12
  import { classifyFile } from '../fileRules/classifyFile.js';
13
- // 🧠 Lock the database to prevent simultaneous access
13
+ import { getDbPathForRepo } from '../db/client.js';
14
14
  async function lockDb() {
15
15
  try {
16
- const lock = await lockfile.lock(DB_PATH); // DB_PATH from constants.ts
16
+ const lock = await lockfile.lock(getDbPathForRepo());
17
17
  return lock;
18
18
  }
19
19
  catch (err) {
@@ -21,7 +21,7 @@ async function lockDb() {
21
21
  throw err;
22
22
  }
23
23
  }
24
- export async function runIndexCommand(targetDir, options = {}) {
24
+ export async function runIndexCommand() {
25
25
  try {
26
26
  initSchema();
27
27
  }
@@ -29,31 +29,17 @@ export async function runIndexCommand(targetDir, options = {}) {
29
29
  console.error('❌ Failed to initialize schema:', err);
30
30
  process.exit(1);
31
31
  }
32
- let resolvedDir;
33
- if (options.force) {
34
- // Force: use passed dir or fallback to cwd, no config updates
35
- resolvedDir = path.resolve(targetDir || process.cwd());
36
- console.warn('⚠️ Running in --force mode. Config will not be updated.');
37
- }
38
- else if (targetDir) {
39
- // User provided a directory: resolve and persist to config
40
- resolvedDir = path.resolve(targetDir);
41
- Config.setIndexDir(resolvedDir);
42
- }
43
- else {
44
- // Use configured indexDir or fallback to cwd
45
- resolvedDir = Config.getIndexDir() || process.cwd();
46
- Config.setIndexDir(resolvedDir); // persist if not yet saved
47
- }
48
- log(`📂 Indexing files in: ${resolvedDir}`);
32
+ const indexDir = Config.getIndexDir() || process.cwd();
33
+ Config.setIndexDir(indexDir); // persist if not already saved
34
+ log(`📂 Indexing files in: ${indexDir}`);
49
35
  const files = await fg('**/*.*', {
50
- cwd: resolvedDir,
36
+ cwd: indexDir,
51
37
  ignore: IGNORED_FOLDER_GLOBS,
52
38
  absolute: true,
53
39
  });
54
40
  const countByExt = {};
55
41
  let count = 0;
56
- const release = await lockDb(); // Lock the DB before starting
42
+ const release = await lockDb();
57
43
  for (const file of files) {
58
44
  const classification = classifyFile(file);
59
45
  if (classification !== 'valid') {
@@ -62,10 +48,10 @@ export async function runIndexCommand(targetDir, options = {}) {
62
48
  }
63
49
  try {
64
50
  const type = detectFileType(file);
65
- indexFile(file, null, type); // Index file without summary
51
+ indexFile(file, null, type);
66
52
  const ext = path.extname(file);
67
53
  countByExt[ext] = (countByExt[ext] || 0) + 1;
68
- log(`📄 Indexed: ${path.relative(resolvedDir, file)}`);
54
+ log(`📄 Indexed: ${path.relative(indexDir, file)}`);
69
55
  count++;
70
56
  }
71
57
  catch (err) {
@@ -74,7 +60,6 @@ export async function runIndexCommand(targetDir, options = {}) {
74
60
  }
75
61
  log('📊 Indexed files by extension:', JSON.stringify(countByExt, null, 2));
76
62
  log(`✅ Done. Indexed ${count} files.`);
77
- await release(); // Release the DB lock after indexing is done
78
- // Auto-start daemon if not already running
63
+ await release();
79
64
  startDaemon();
80
65
  }
@@ -1,7 +1,7 @@
1
- import { db } from '../db/client.js';
2
1
  import path from 'path';
3
2
  import fs from 'fs';
4
3
  import { log } from '../utils/log.js';
4
+ import { getDbForRepo } from '../db/client.js';
5
5
  export async function runInspectCommand(fileArg) {
6
6
  if (!fileArg) {
7
7
  log('❌ Please provide a file path to inspect.');
@@ -12,6 +12,7 @@ export async function runInspectCommand(fileArg) {
12
12
  log(`❌ File does not exist: ${resolvedPath}`);
13
13
  process.exit(1);
14
14
  }
15
+ const db = getDbForRepo();
15
16
  const file = db
16
17
  .prepare(`SELECT * FROM files WHERE REPLACE(path, '\\', '/') = ?`)
17
18
  .get(resolvedPath);
@@ -29,12 +30,10 @@ export async function runInspectCommand(fileArg) {
29
30
  console.log(`📌 Functions extracted: ${isExtracted ? '✅' : '❌'}`);
30
31
  console.log(`📆 Extracted at: ${file.functions_extracted_at || '❌ Not yet'}`);
31
32
  console.log(`⚙️ Processing status: ${file.processing_status || 'unknown'}`);
32
- // 📝 Show summary preview
33
33
  if (file.summary) {
34
34
  console.log('\n📝 Summary:');
35
35
  console.log(file.summary.slice(0, 300) + (file.summary.length > 300 ? '...' : ''));
36
36
  }
37
- // 🧑‍💻 Show extracted functions
38
37
  const functions = db
39
38
  .prepare(`SELECT name, start_line, end_line FROM functions WHERE file_id = ? ORDER BY start_line ASC`)
40
39
  .all(file.id);
@@ -1,21 +1,24 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import lockfile from 'proper-lockfile';
4
- import { db } from '../db/client.js';
5
- import { DB_PATH } from '../constants.js';
6
- import { backupScaiFolder } from '../db/backup.js'; // <-- New import
4
+ import { backupScaiFolder } from '../db/backup.js';
5
+ import { getDbPathForRepo, getDbForRepo } from '../db/client.js';
7
6
  export async function resetDatabase() {
8
7
  console.log('🔁 Backing up existing .scai folder...');
9
8
  await backupScaiFolder();
9
+ const dbPath = getDbPathForRepo();
10
+ // Close the DB connection
10
11
  try {
12
+ const db = getDbForRepo();
11
13
  db.close();
12
14
  console.log('🔒 Closed SQLite database connection.');
13
15
  }
14
16
  catch (err) {
15
17
  console.warn('⚠️ Could not close database:', err instanceof Error ? err.message : err);
16
18
  }
19
+ // Release lockfile if present
17
20
  try {
18
- const releaseLock = await lockfile.unlock(DB_PATH).catch(() => null);
21
+ const releaseLock = await lockfile.unlock(dbPath).catch(() => null);
19
22
  if (releaseLock) {
20
23
  console.log('🔓 Released database lock.');
21
24
  }
@@ -23,27 +26,29 @@ export async function resetDatabase() {
23
26
  catch (err) {
24
27
  console.warn('⚠️ Failed to release database lock:', err instanceof Error ? err.message : err);
25
28
  }
26
- if (fs.existsSync(DB_PATH)) {
29
+ // Delete DB file
30
+ if (fs.existsSync(dbPath)) {
27
31
  try {
28
- fs.unlinkSync(DB_PATH);
29
- console.log(`🧹 Deleted existing database at ${DB_PATH}`);
32
+ fs.unlinkSync(dbPath);
33
+ console.log(`🧹 Deleted existing database at ${dbPath}`);
30
34
  }
31
35
  catch (err) {
32
36
  console.error('❌ Failed to delete DB file:', err instanceof Error ? err.message : err);
33
- return;
34
37
  }
35
38
  }
36
39
  else {
37
- console.log('ℹ️ No existing database found at:', DB_PATH);
40
+ console.log('ℹ️ No existing database found at:', dbPath);
38
41
  }
42
+ // Ensure directory exists
39
43
  try {
40
- fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
44
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
41
45
  console.log('📁 Ensured that the database directory exists.');
42
46
  }
43
47
  catch (err) {
44
48
  console.warn('⚠️ Could not ensure DB directory exists:', err instanceof Error ? err.message : err);
45
49
  }
46
- const lockDir = `${DB_PATH}.lock`;
50
+ // Clean up lock directory
51
+ const lockDir = `${dbPath}.lock`;
47
52
  if (fs.existsSync(lockDir)) {
48
53
  try {
49
54
  fs.rmSync(lockDir, { recursive: true, force: true });
@@ -1,8 +1,10 @@
1
1
  import readline from 'readline';
2
- import { reviewModule } from '../pipeline/modules/reviewModule.js';
3
2
  import { fetchOpenPullRequests, fetchPullRequestDiff, getGitHubUsername, submitReview } from '../github/github.js';
4
3
  import { getRepoDetails } from '../github/repo.js';
5
4
  import { ensureGitHubAuth } from '../github/auth.js';
5
+ import { postReviewComment } from '../github/postComments.js';
6
+ import { reviewModule } from '../pipeline/modules/reviewModule.js';
7
+ import chalk from 'chalk';
6
8
  // Function to fetch the PRs with requested reviews for a specific branch (default to 'main')
7
9
  export async function getPullRequestsForReview(token, owner, repo, username, branch = 'main', filterForUser = true) {
8
10
  const prs = await fetchOpenPullRequests(token, owner, repo);
@@ -47,7 +49,7 @@ function askUserToPickPR(prs) {
47
49
  console.log("⚠️ No pull requests with review requested.");
48
50
  return resolve(null);
49
51
  }
50
- console.log("\n📦 Open Pull Requests with review requested:");
52
+ console.log(chalk.blue("\n📦 Open Pull Requests with review requested:"));
51
53
  prs.forEach((pr, i) => {
52
54
  console.log(`${i + 1}) #${pr.number} - ${pr.title}`);
53
55
  });
@@ -55,14 +57,43 @@ function askUserToPickPR(prs) {
55
57
  input: process.stdin,
56
58
  output: process.stdout,
57
59
  });
58
- rl.question(`\n👉 Choose a PR to review [1-${prs.length}]: `, (answer) => {
60
+ const askQuestion = () => {
61
+ rl.question(`\n👉 Choose a PR to review [1-${prs.length}]: `, (answer) => {
62
+ const index = parseInt(answer, 10);
63
+ if (!isNaN(index) && index >= 1 && index <= prs.length) {
64
+ resolve(index - 1); // Return array index, not PR number
65
+ rl.close();
66
+ }
67
+ else {
68
+ console.log('⚠️ Invalid selection. Please enter a number between 1 and ' + prs.length);
69
+ askQuestion(); // Retry asking for input
70
+ }
71
+ });
72
+ };
73
+ askQuestion(); // Initial call to ask the user
74
+ });
75
+ }
76
+ // Ask user to choose review method: whole PR or chunk-by-chunk
77
+ function askReviewMethod() {
78
+ return new Promise((resolve) => {
79
+ const rl = readline.createInterface({
80
+ input: process.stdin,
81
+ output: process.stdout,
82
+ });
83
+ console.log("\n🔍 Choose review method:");
84
+ console.log('1) Review whole PR at once');
85
+ console.log('2) Review chunk by chunk');
86
+ rl.question(`👉 Choose an option [1-2]: `, (answer) => {
59
87
  rl.close();
60
- const index = parseInt(answer, 10);
61
- if (!isNaN(index) && index >= 1 && index <= prs.length) {
62
- resolve(index - 1); // Return array index, not PR number
88
+ if (answer === '1') {
89
+ resolve('whole');
90
+ }
91
+ else if (answer === '2') {
92
+ resolve('chunk');
63
93
  }
64
94
  else {
65
- resolve(null);
95
+ console.log('⚠️ Invalid selection. Defaulting to "whole".');
96
+ resolve('whole');
66
97
  }
67
98
  });
68
99
  });
@@ -119,12 +150,52 @@ function promptCustomReview() {
119
150
  });
120
151
  });
121
152
  }
153
+ function chunkDiff(diff) {
154
+ const rawChunks = diff.split(/^diff --git /m).filter(Boolean);
155
+ return rawChunks.map(chunk => {
156
+ const fullChunk = 'diff --git ' + chunk;
157
+ // Try to extract the file name from the diff header
158
+ const match = fullChunk.match(/^diff --git a\/(.+?) b\/.+?\n/);
159
+ const filePath = match ? match[1] : 'unknown';
160
+ return { filePath, content: fullChunk };
161
+ });
162
+ }
163
+ // Function to color diff lines
164
+ function colorDiffLine(line) {
165
+ if (line.startsWith('+')) {
166
+ return chalk.green(line); // New lines (added)
167
+ }
168
+ else if (line.startsWith('-')) {
169
+ return chalk.red(line); // Deleted lines
170
+ }
171
+ else if (line.startsWith('@@')) {
172
+ return chalk.yellow(line); // Modified lines (with context)
173
+ }
174
+ return line; // Default case (unchanged lines)
175
+ }
176
+ // Updated reviewChunk function to apply coloring
177
+ export async function reviewChunk(chunk, chunkIndex, totalChunks) {
178
+ console.log(chalk.yellow(`\n🔍 Reviewing chunk ${chunkIndex + 1} of ${totalChunks}:`));
179
+ console.log(`File: ${chunk.filePath}`);
180
+ // Split the chunk content into lines and color each line accordingly
181
+ const coloredDiff = chunk.content.split('\n').map(colorDiffLine).join('\n');
182
+ // Log the colored diff with line numbers and progress
183
+ console.log(`Starting line number: 30`); // Adjust based on actual starting line logic
184
+ console.log(coloredDiff);
185
+ // Get the review suggestion from the model (as before)
186
+ const suggestion = await reviewModule.run({ content: chunk.content, filepath: chunk.filePath });
187
+ console.log("\n💡 AI-suggested review:\n");
188
+ console.log(suggestion.content);
189
+ // Ask user to approve or reject the chunk
190
+ const reviewChoice = await askReviewApproval('Do you approve this code chunk?');
191
+ return reviewChoice;
192
+ }
122
193
  export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
123
194
  try {
124
195
  console.log("🔍 Fetching pull requests and diffs...");
125
- const token = await ensureGitHubAuth();
196
+ const token = await ensureGitHubAuth(); // Get GitHub token
126
197
  const username = await getGitHubUsername(token);
127
- const { owner, repo } = getRepoDetails();
198
+ const { owner, repo } = getRepoDetails(); // Get repository details (owner, repo name)
128
199
  console.log(`👤 Authenticated user: ${username}`);
129
200
  console.log(`📦 GitHub repo: ${owner}/${repo}`);
130
201
  console.log(`🔍 Filtering ${showAll ? "all" : "user-specific"} PRs for branch: ${branch}`);
@@ -143,32 +214,101 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
143
214
  console.log(`🔍 Fetching diff for PR #${pr.number}...`);
144
215
  prDiff = await fetchPullRequestDiff(pr, token);
145
216
  }
146
- const result = await reviewModule.run({ content: prDiff });
147
- const reviewSuggestion = result.content || 'No review suggestion generated.';
148
- const reviewChoice = await askReviewApproval(reviewSuggestion);
149
- let reviewEvent;
150
- if (reviewChoice === 'approve') {
151
- reviewEvent = 'APPROVE';
152
- console.log(`✅ Review for PR #${pr.number} approved.`);
153
- await submitReview(pr.number, reviewSuggestion, reviewEvent);
154
- }
155
- else if (reviewChoice === 'reject') {
156
- reviewEvent = 'REQUEST_CHANGES';
157
- console.log(`❌ Review for PR #${pr.number} rejected.`);
158
- await submitReview(pr.number, reviewSuggestion, reviewEvent);
159
- }
160
- else if (reviewChoice === 'cancel') {
161
- console.log(`🚪 Review process for PR #${pr.number} cancelled.`);
162
- return; // Exit the function and cancel the review process
217
+ const chunkComments = {}; // Declare here for global access
218
+ const reviewMethod = await askReviewMethod(); // Ask user for review method (whole or chunk)
219
+ if (reviewMethod === 'whole') {
220
+ console.log(`🔍 Reviewing the entire PR at once...`);
221
+ const suggestion = await reviewModule.run({ content: prDiff, filepath: 'Whole PR Diff' });
222
+ console.log("\n💡 AI-suggested review:\n");
223
+ console.log(suggestion.content);
224
+ // Store the entire review choice in chunkComments
225
+ const finalReviewChoice = await askReviewApproval('Do you approve, reject, or leave a final review for this PR?');
226
+ if (finalReviewChoice === 'approve') {
227
+ console.log(`✅ Review for PR #${pr.number} approved.`);
228
+ await postReviewComments(pr, chunkComments, token);
229
+ await submitReview(pr.number, 'PR approved', 'APPROVE');
230
+ }
231
+ else if (finalReviewChoice === 'reject') {
232
+ console.log(`❌ Review for PR #${pr.number} rejected.`);
233
+ await postReviewComments(pr, chunkComments, token);
234
+ await submitReview(pr.number, 'Changes requested', 'REQUEST_CHANGES');
235
+ }
236
+ else if (finalReviewChoice === 'cancel') {
237
+ console.log(`🚪 Review process cancelled.`);
238
+ return;
239
+ }
240
+ else {
241
+ const customReview = await promptCustomReview();
242
+ console.log(`💬 Custom review: ${customReview}`);
243
+ await postReviewComments(pr, chunkComments, token);
244
+ await submitReview(pr.number, customReview, 'COMMENT');
245
+ }
163
246
  }
164
247
  else {
165
- reviewEvent = 'COMMENT';
166
- const customReview = await promptCustomReview();
167
- console.log(`💬 Custom review: ${customReview}`);
168
- await submitReview(pr.number, customReview, reviewEvent);
248
+ const chunks = chunkDiff(prDiff); // Split the diff into chunks
249
+ // Log the total number of chunks
250
+ console.log(chalk.cyan(`🔍 Total Chunks: ${chunks.length}`));
251
+ // Iterate over each chunk, passing the index and total chunk count
252
+ for (let i = 0; i < chunks.length; i++) {
253
+ const chunk = chunks[i];
254
+ // Pass the chunk, index (i), and totalChunks to the reviewChunk function
255
+ const reviewChoice = await reviewChunk(chunk, i, chunks.length); // Review each chunk
256
+ if (reviewChoice === 'approve') {
257
+ console.log('✅ Approving this chunk.');
258
+ chunkComments[chunk.filePath] = ['Approved'];
259
+ }
260
+ else if (reviewChoice === 'reject') {
261
+ console.log('❌ Rejecting this chunk.');
262
+ chunkComments[chunk.filePath] = ['Rejected'];
263
+ }
264
+ else if (reviewChoice === 'cancel') {
265
+ console.log('🚪 Review process cancelled.');
266
+ return; // Exit if user cancels
267
+ }
268
+ else {
269
+ console.log('✍️ Custom review added.');
270
+ const customReview = await promptCustomReview();
271
+ console.log(`💬 Custom review: ${customReview}`);
272
+ chunkComments[chunk.filePath] = [customReview];
273
+ }
274
+ }
275
+ // After chunk review, ask for the final approval decision
276
+ const finalReviewChoice = await askReviewApproval('Do you approve, reject, or leave a final review for this PR?');
277
+ if (finalReviewChoice === 'approve') {
278
+ console.log(`✅ Review for PR #${pr.number} approved.`);
279
+ await postReviewComments(pr, chunkComments, token);
280
+ await submitReview(pr.number, 'PR approved', 'APPROVE');
281
+ }
282
+ else if (finalReviewChoice === 'reject') {
283
+ console.log(`❌ Review for PR #${pr.number} rejected.`);
284
+ await postReviewComments(pr, chunkComments, token);
285
+ await submitReview(pr.number, 'Changes requested', 'REQUEST_CHANGES');
286
+ }
287
+ else if (finalReviewChoice === 'cancel') {
288
+ console.log(`🚪 Review process cancelled.`);
289
+ return;
290
+ }
291
+ else {
292
+ const customReview = await promptCustomReview();
293
+ console.log(`💬 Custom review: ${customReview}`);
294
+ await postReviewComments(pr, chunkComments, token);
295
+ await submitReview(pr.number, customReview, 'COMMENT');
296
+ }
169
297
  }
170
298
  }
171
299
  catch (err) {
172
300
  console.error("❌ Error reviewing PR:", err.message);
173
301
  }
174
302
  }
303
+ // Function to post all comments to GitHub after the review
304
+ async function postReviewComments(pr, chunkComments, token) {
305
+ const { owner, repo } = getRepoDetails(); // Get the repo details
306
+ for (const chunk in chunkComments) {
307
+ const comments = chunkComments[chunk];
308
+ const fileName = chunk; // Use chunk's file path
309
+ const lineNumber = 10; // Extract the actual line number from the chunk content
310
+ for (const comment of comments) {
311
+ await postReviewComment(token, owner, repo, pr.number, fileName, lineNumber, comment);
312
+ }
313
+ }
314
+ }
@@ -7,7 +7,7 @@ import { summarizeCode } from '../utils/summarizer.js';
7
7
  import { detectFileType } from '../fileRules/detectFileType.js';
8
8
  import { generateEmbedding } from '../lib/generateEmbedding.js';
9
9
  import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
10
- import { db } from '../db/client.js';
10
+ import { getDbForRepo } from '../db/client.js';
11
11
  export async function summarizeFile(filepath) {
12
12
  let content = '';
13
13
  let filePathResolved;
@@ -71,9 +71,10 @@ export async function summarizeFile(filepath) {
71
71
  console.log('💾 Summary saved to local database.');
72
72
  const embedding = await generateEmbedding(response.summary);
73
73
  if (embedding) {
74
+ const db = getDbForRepo();
74
75
  db.prepare(`
75
- UPDATE files SET embedding = ? WHERE path = ?
76
- `).run(JSON.stringify(embedding), filePathResolved.replace(/\\/g, '/'));
76
+ UPDATE files SET embedding = ? WHERE path = ?
77
+ `).run(JSON.stringify(embedding), filePathResolved.replace(/\\/g, '/'));
77
78
  console.log('📐 Embedding saved to database.');
78
79
  }
79
80
  }
@@ -0,0 +1,73 @@
1
+ // File: src/commands/switch.ts
2
+ import readline from 'readline';
3
+ import { Config, writeConfig } from '../config.js';
4
+ import { normalizePath, getRepoKeyForPath } from '../utils/normalizePath.js';
5
+ import chalk from 'chalk';
6
+ export function runSwitchCommand(inputPathOrKey) {
7
+ const config = Config.getRaw();
8
+ const normalizedInput = normalizePath(inputPathOrKey);
9
+ // Try to match by key directly
10
+ if (config.repos[normalizedInput]) {
11
+ config.activeRepo = normalizedInput;
12
+ // Update GitHub token
13
+ Config.setGitHubToken(config.repos[normalizedInput].githubToken ?? '');
14
+ console.log(`✅ Switched active repo to key: ${normalizedInput}`);
15
+ }
16
+ else {
17
+ // Try to match by indexDir path
18
+ const repoKey = getRepoKeyForPath(inputPathOrKey, config);
19
+ if (!repoKey) {
20
+ console.error(`❌ No repo found matching path or key: "${inputPathOrKey}"`);
21
+ process.exit(1);
22
+ }
23
+ config.activeRepo = repoKey;
24
+ // Update GitHub token
25
+ Config.setGitHubToken(config.repos[repoKey]?.githubToken ?? '');
26
+ console.log(`✅ Switched active repo to path match: ${repoKey}`);
27
+ }
28
+ // Ensure the active repo change is saved back to the config
29
+ writeConfig(config);
30
+ }
31
+ export async function runInteractiveSwitch() {
32
+ const config = Config.getRaw();
33
+ const keys = Object.keys(config.repos || {});
34
+ if (!keys.length) {
35
+ console.log('⚠️ No repositories configured.');
36
+ return;
37
+ }
38
+ // Auto-switch to the other repo if only 2 are present
39
+ if (keys.length === 2) {
40
+ const current = config.activeRepo;
41
+ const other = keys.find(k => k !== current);
42
+ if (other) {
43
+ runSwitchCommand(other);
44
+ return;
45
+ }
46
+ }
47
+ // Otherwise, show interactive selection
48
+ console.log('\n📁 Available Repositories:\n');
49
+ keys.forEach((key, i) => {
50
+ const isActive = config.activeRepo === key ? chalk.green('(active)') : '';
51
+ const dir = config.repos[key]?.indexDir ?? '';
52
+ // Color the number using chalk.blue and make active repo green
53
+ const numberedRepo = chalk.blue(`${i + 1})`);
54
+ // Highlight the active repo in green and list it
55
+ console.log(`${numberedRepo} ${key} ${isActive}`);
56
+ // Use light grey for the indexDir
57
+ console.log(` ↳ ${chalk.grey(dir)}`);
58
+ });
59
+ const rl = readline.createInterface({
60
+ input: process.stdin,
61
+ output: process.stdout,
62
+ });
63
+ rl.question('\n👉 Select a repository number to activate: ', (answer) => {
64
+ rl.close();
65
+ const index = parseInt(answer.trim(), 10) - 1;
66
+ if (isNaN(index) || index < 0 || index >= keys.length) {
67
+ console.log('❌ Invalid selection.');
68
+ return;
69
+ }
70
+ const selectedKey = keys[index];
71
+ runSwitchCommand(selectedKey);
72
+ });
73
+ }