scai 0.1.54 → 0.1.56

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
@@ -65,7 +65,7 @@ No more struggling to write pull request descriptions by hand. `scai git review`
65
65
  To interact with GitHub and create pull requests, `scai` needs a personal access token with **repo** permissions.
66
66
 
67
67
  1. **Create your GitHub Access Token**
68
- Follow this link to generate a token: [https://github.com/settings/tokens?type=beta](https://github.com/settings/tokens?type=beta)
68
+ Follow this link to generate a token: [https://github.com/settings/personal-access-tokens](https://github.com/settings/personal-access-tokens)
69
69
 
70
70
  Make sure you enable at least:
71
71
 
@@ -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);
@@ -30,17 +32,13 @@ export async function getPullRequestsForReview(token, owner, repo, username, bra
30
32
  filtered.push({ pr, diff });
31
33
  }
32
34
  catch (err) {
33
- console.warn(`⚠️ Could not fetch diff for PR #${pr.number}: ${err.message}`);
34
35
  failedPRs.push(pr);
35
36
  }
36
37
  }
37
38
  }
38
39
  if (failedPRs.length > 0) {
39
- console.warn(`\n⚠️ Skipped ${failedPRs.length} PR(s) due to diff fetch failures:`);
40
- for (const pr of failedPRs) {
41
- console.warn(` - #${pr.number}: ${pr.title}`);
42
- }
43
- console.warn('These PRs will not be included in the review summary.\n');
40
+ const failedList = failedPRs.map(pr => `#${pr.number}`).join(', ');
41
+ console.warn(`⚠️ Skipped ${failedPRs.length} PR(s) (${failedList}) due to diff fetch errors.`);
44
42
  }
45
43
  return filtered;
46
44
  }
@@ -51,7 +49,7 @@ function askUserToPickPR(prs) {
51
49
  console.log("⚠️ No pull requests with review requested.");
52
50
  return resolve(null);
53
51
  }
54
- console.log("\n📦 Open Pull Requests with review requested:");
52
+ console.log(chalk.blue("\n📦 Open Pull Requests with review requested:"));
55
53
  prs.forEach((pr, i) => {
56
54
  console.log(`${i + 1}) #${pr.number} - ${pr.title}`);
57
55
  });
@@ -59,14 +57,43 @@ function askUserToPickPR(prs) {
59
57
  input: process.stdin,
60
58
  output: process.stdout,
61
59
  });
62
- 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) => {
63
87
  rl.close();
64
- const index = parseInt(answer, 10);
65
- if (!isNaN(index) && index >= 1 && index <= prs.length) {
66
- 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');
67
93
  }
68
94
  else {
69
- resolve(null);
95
+ console.log('⚠️ Invalid selection. Defaulting to "whole".');
96
+ resolve('whole');
70
97
  }
71
98
  });
72
99
  });
@@ -123,12 +150,52 @@ function promptCustomReview() {
123
150
  });
124
151
  });
125
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
+ }
126
193
  export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
127
194
  try {
128
195
  console.log("🔍 Fetching pull requests and diffs...");
129
- const token = await ensureGitHubAuth();
196
+ const token = await ensureGitHubAuth(); // Get GitHub token
130
197
  const username = await getGitHubUsername(token);
131
- const { owner, repo } = getRepoDetails();
198
+ const { owner, repo } = getRepoDetails(); // Get repository details (owner, repo name)
132
199
  console.log(`👤 Authenticated user: ${username}`);
133
200
  console.log(`📦 GitHub repo: ${owner}/${repo}`);
134
201
  console.log(`🔍 Filtering ${showAll ? "all" : "user-specific"} PRs for branch: ${branch}`);
@@ -147,32 +214,101 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
147
214
  console.log(`🔍 Fetching diff for PR #${pr.number}...`);
148
215
  prDiff = await fetchPullRequestDiff(pr, token);
149
216
  }
150
- const result = await reviewModule.run({ content: prDiff });
151
- const reviewSuggestion = result.content || 'No review suggestion generated.';
152
- const reviewChoice = await askReviewApproval(reviewSuggestion);
153
- let reviewEvent;
154
- if (reviewChoice === 'approve') {
155
- reviewEvent = 'APPROVE';
156
- console.log(`✅ Review for PR #${pr.number} approved.`);
157
- await submitReview(pr.number, reviewSuggestion, reviewEvent);
158
- }
159
- else if (reviewChoice === 'reject') {
160
- reviewEvent = 'REQUEST_CHANGES';
161
- console.log(`❌ Review for PR #${pr.number} rejected.`);
162
- await submitReview(pr.number, reviewSuggestion, reviewEvent);
163
- }
164
- else if (reviewChoice === 'cancel') {
165
- console.log(`🚪 Review process for PR #${pr.number} cancelled.`);
166
- 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
+ }
167
246
  }
168
247
  else {
169
- reviewEvent = 'COMMENT';
170
- const customReview = await promptCustomReview();
171
- console.log(`💬 Custom review: ${customReview}`);
172
- 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
+ }
173
297
  }
174
298
  }
175
299
  catch (err) {
176
300
  console.error("❌ Error reviewing PR:", err.message);
177
301
  }
178
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
+ }
@@ -0,0 +1,69 @@
1
+ import { Octokit } from '@octokit/rest';
2
+ /**
3
+ * Parses the PR diff to determine the correct position for inline comments.
4
+ * The position is the "index" of the changed line in the diff.
5
+ * @param diff The diff content of the PR
6
+ * @param lineNumber The line number to convert to a position in the diff
7
+ */
8
+ function getLinePositionFromDiff(diff, lineNumber) {
9
+ const lines = diff.split('\n');
10
+ let currentLine = 0;
11
+ // Iterate through the lines and determine the correct position for the lineNumber
12
+ for (let i = 0; i < lines.length; i++) {
13
+ // Only count the lines that are part of a diff chunk
14
+ if (lines[i].startsWith('+') || lines[i].startsWith('-')) {
15
+ currentLine++;
16
+ if (currentLine === lineNumber) {
17
+ return i; // Position is the index of the changed line in the diff
18
+ }
19
+ }
20
+ }
21
+ return null; // Return null if lineNumber is not found in the diff
22
+ }
23
+ /**
24
+ * Posts an inline review comment on a specific line of a PR.
25
+ *
26
+ * @param token GitHub personal access token
27
+ * @param owner Repository owner (e.g. 'my-org')
28
+ * @param repo Repository name (e.g. 'my-repo')
29
+ * @param prNumber Pull Request number
30
+ * @param fileName Path to the file in the PR (relative to repo root)
31
+ * @param lineNumber Line number to comment on in the file (not in the diff)
32
+ * @param comment Text of the comment
33
+ */
34
+ export async function postReviewComment(token, owner, repo, prNumber, fileName, lineNumber, comment) {
35
+ const octokit = new Octokit({ auth: `token ${token}` });
36
+ // First, get PR details so we can retrieve the head commit SHA
37
+ const pr = await octokit.pulls.get({
38
+ owner,
39
+ repo,
40
+ pull_number: prNumber,
41
+ });
42
+ const commitId = pr.data.head.sha;
43
+ // Fetch the PR diff by getting the full diff URL from the PR
44
+ const diffUrl = pr.data.diff_url;
45
+ const diffRes = await fetch(diffUrl);
46
+ const diff = await diffRes.text();
47
+ // Get the position of the line in the diff
48
+ const position = getLinePositionFromDiff(diff, lineNumber);
49
+ if (position === null) {
50
+ console.error(`❌ Unable to find line ${lineNumber} in the diff for PR #${prNumber}.`);
51
+ return;
52
+ }
53
+ // Now, post the inline comment
54
+ try {
55
+ await octokit.pulls.createReviewComment({
56
+ owner,
57
+ repo,
58
+ pull_number: prNumber,
59
+ commit_id: commitId,
60
+ path: fileName,
61
+ body: comment,
62
+ position: position, // Use the position calculated from the diff
63
+ });
64
+ console.log(`✅ Inline comment posted to ${fileName} at diff position ${position}.`);
65
+ }
66
+ catch (err) {
67
+ console.error(`❌ Error posting inline comment: ${err.message}`);
68
+ }
69
+ }
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ import { runInspectCommand } from "./commands/InspectCmd.js";
25
25
  import { reviewPullRequestCmd } from "./commands/ReviewCmd.js";
26
26
  import { promptForToken } from "./github/token.js";
27
27
  import { validateGitHubTokenAgainstRepo } from "./github/githubAuthCheck.js";
28
+ import { checkGit } from "./commands/GitCmd.js";
28
29
  // 🎛️ CLI Setup
29
30
  const cmd = new Command('scai')
30
31
  .version(version)
@@ -54,6 +55,12 @@ git
54
55
  .option('-c, --commit', 'Automatically commit with suggested message')
55
56
  .option('-l, --changelog', 'Generate and optionally stage a changelog entry')
56
57
  .action((options) => suggestCommitMessage(options));
58
+ git
59
+ .command('check')
60
+ .description('Check Git working directory and branch status')
61
+ .action(() => {
62
+ checkGit();
63
+ });
57
64
  // Add auth-related commands
58
65
  const auth = cmd.command('auth').description('GitHub authentication commands');
59
66
  auth
@@ -7,7 +7,8 @@ export const reviewModule = {
7
7
  const model = Config.getModel();
8
8
  const prompt = `
9
9
  You are a senior software engineer reviewing a pull request.
10
- Give clear, constructive feedback based on the code changes below.
10
+ Give clear and very short constructive feedback based on the code changes below.
11
+ Only mention issues of greater concern. Less is more.
11
12
 
12
13
  Changes:
13
14
  ${content}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -36,9 +36,11 @@
36
36
  "start": "node dist/index.js"
37
37
  },
38
38
  "dependencies": {
39
+ "@octokit/rest": "^22.0.0",
39
40
  "acorn": "^8.11.3",
40
41
  "acorn-walk": "^8.3.2",
41
42
  "better-sqlite3": "^12.1.1",
43
+ "chalk": "^5.4.1",
42
44
  "commander": "^11.0.0",
43
45
  "fast-glob": "^3.3.3",
44
46
  "proper-lockfile": "^4.1.2",
@@ -57,4 +59,4 @@
57
59
  "dist/",
58
60
  "README.md"
59
61
  ]
60
- }
62
+ }