scai 0.1.50 → 0.1.52
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 +89 -12
- package/dist/commands/ReviewCmd.js +178 -0
- package/dist/config.js +11 -0
- package/dist/github/api.js +43 -0
- package/dist/github/auth.js +15 -0
- package/dist/github/github.js +71 -0
- package/dist/github/githubAuthCheck.js +17 -0
- package/dist/github/repo.js +57 -0
- package/dist/github/token.js +14 -0
- package/dist/github/types.js +1 -0
- package/dist/index.js +59 -24
- package/dist/pipeline/modules/reviewModule.js +21 -0
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
# ⚙️ scai — Smart Commit AI ✨
|
|
1
|
+
# ⚙️ scai — Smart Commit & Review AI ✨
|
|
2
2
|
|
|
3
|
+
> AI-powered CLI tool for commit messages **and** pull request reviews — using local models.
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
**scai** is your AI pair‑programmer in the terminal. Focus on coding while scai:
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- 🔍 Search and ask questions across your codebase (ALPHA)
|
|
13
|
-
- 🔐 100% local — no API keys, no cloud, no telemetry
|
|
7
|
+
- 🤖 **Reviews open pull requests** and provides AI‑driven feedback (BETA)
|
|
8
|
+
- 💬 **Suggests intelligent Git commit messages** based on your staged diff
|
|
9
|
+
- 📝 Summarizes files in plain English
|
|
10
|
+
- 📜 Auto‑updates your changelog
|
|
11
|
+
- 🔍 (ALPHA) Search & ask questions across your codebase
|
|
12
|
+
- 🔐 100% local — no API keys, no cloud, no telemetry
|
|
14
13
|
|
|
15
14
|
---
|
|
16
15
|
|
|
@@ -53,6 +52,80 @@ scai runs entirely on your machine and doesn't require cloud APIs or API keys. T
|
|
|
53
52
|
scai init
|
|
54
53
|
```
|
|
55
54
|
|
|
55
|
+
## ✨ AI Code Review, Powered by Your Terminal
|
|
56
|
+
|
|
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
|
+
|
|
59
|
+
> ⚠️ These features are in **beta** — feedback welcome!
|
|
60
|
+
|
|
61
|
+
## 🙌 Feedback
|
|
62
|
+
|
|
63
|
+
Questions, ideas, or bugs?
|
|
64
|
+
Ping [@ticcr](https://bsky.app/profile/ticcr.xyz) on Bluesky — I'd love to hear your thoughts!
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### 🔑 Setting Up Authentication (Required)
|
|
70
|
+
|
|
71
|
+
To interact with GitHub and create pull requests, `scai` needs a personal access token with **repo** permissions.
|
|
72
|
+
|
|
73
|
+
1. **Create your GitHub Access Token**
|
|
74
|
+
Follow this link to generate a token: [https://github.com/settings/tokens?type=beta](https://github.com/settings/tokens?type=beta)
|
|
75
|
+
|
|
76
|
+
Make sure you enable at least:
|
|
77
|
+
|
|
78
|
+
* `repo` (Full control of private repositories)
|
|
79
|
+
* `workflow` (If you want PRs to trigger CI)
|
|
80
|
+
|
|
81
|
+
2. **Set the token in scai:**
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
scai auth set
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This stores your token locally in a secure config file. You can inspect the setup at any time:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
scai auth check
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
3. **Set the index dir:**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
scai set index-dir <repo path>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This is the repo from which scai will look up pull requests that can be reviewed.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### 🧠 How to Use `scai git review`
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
scai git review
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This will show you pull requests assigned to you for review:
|
|
110
|
+
|
|
111
|
+
* Understand the diffs using a local model
|
|
112
|
+
* Generate a structured pull request:
|
|
113
|
+
|
|
114
|
+
* ✅ Title
|
|
115
|
+
* ✅ Summary of changes
|
|
116
|
+
* ✅ Explanation of why the changes matter
|
|
117
|
+
* ✅ Optional changelog bullets
|
|
118
|
+
|
|
119
|
+
You’ll be shown a preview and asked to confirm before pushing or opening a PR.
|
|
120
|
+
|
|
121
|
+
#### Flags
|
|
122
|
+
|
|
123
|
+
You can override behavior like this:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
scai git review -a
|
|
127
|
+
```
|
|
128
|
+
This will show you all pull requests that have not yet been reviewed.
|
|
56
129
|
|
|
57
130
|
## ⚒️ Usage Overview
|
|
58
131
|
|
|
@@ -206,7 +279,7 @@ You won't gain much value from the index unless you scope it to one repository.
|
|
|
206
279
|
|
|
207
280
|
## 🔄 Breaking Change in v0.1.47
|
|
208
281
|
|
|
209
|
-
> 🛠️ As of `v0.1.47`, the internal database schema has changed.
|
|
282
|
+
> 🛠️ As of `v0.1.47`, the internal database schema has changed, and may well change in the future.
|
|
210
283
|
|
|
211
284
|
🚨 **OBS**: The **Migrate** command is for **internal use only**. Do **not** run it unless explicitly instructed, as it may delete existing data or corrupt your local database.
|
|
212
285
|
|
|
@@ -259,6 +332,10 @@ You may **not**:
|
|
|
259
332
|
* ❌ Resell as a product or service
|
|
260
333
|
* ❌ Claim ownership of the tool
|
|
261
334
|
|
|
262
|
-
|
|
335
|
+
### 📄 License
|
|
336
|
+
|
|
337
|
+
Free for personal and internal company use only.
|
|
338
|
+
Commercial use (resale, SaaS, inclusion in paid tools) is prohibited.
|
|
339
|
+
Contact me for commercial licensing.
|
|
263
340
|
|
|
264
341
|
```
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import { reviewModule } from '../pipeline/modules/reviewModule.js';
|
|
3
|
+
import { fetchOpenPullRequests, fetchPullRequestDiff, getGitHubUsername, submitReview } from '../github/github.js';
|
|
4
|
+
import { getRepoDetails } from '../github/repo.js';
|
|
5
|
+
import { ensureGitHubAuth } from '../github/auth.js';
|
|
6
|
+
// Function to fetch the PRs with requested reviews for a specific branch (default to 'main')
|
|
7
|
+
export async function getPullRequestsForReview(token, owner, repo, username, branch = 'main', filterForUser = true) {
|
|
8
|
+
const prs = await fetchOpenPullRequests(token, owner, repo);
|
|
9
|
+
const filtered = [];
|
|
10
|
+
const failedPRs = [];
|
|
11
|
+
for (const pr of prs) {
|
|
12
|
+
const isDraft = pr.draft;
|
|
13
|
+
const isMerged = pr.merged_at != null;
|
|
14
|
+
const shouldInclude = !isDraft &&
|
|
15
|
+
!isMerged &&
|
|
16
|
+
(!filterForUser || pr.requested_reviewers?.some(r => r.login === username));
|
|
17
|
+
if (shouldInclude) {
|
|
18
|
+
const diffUrl = `https://api.github.com/repos/${owner}/${repo}/pulls/${pr.number}.diff`;
|
|
19
|
+
try {
|
|
20
|
+
const diffRes = await fetch(diffUrl, {
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `token ${token}`,
|
|
23
|
+
Accept: 'application/vnd.github.v3.diff',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (!diffRes.ok) {
|
|
27
|
+
throw new Error(`${diffRes.status} ${diffRes.statusText}`);
|
|
28
|
+
}
|
|
29
|
+
const diff = await diffRes.text();
|
|
30
|
+
filtered.push({ pr, diff });
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.warn(`⚠️ Could not fetch diff for PR #${pr.number}: ${err.message}`);
|
|
34
|
+
failedPRs.push(pr);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
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');
|
|
44
|
+
}
|
|
45
|
+
return filtered;
|
|
46
|
+
}
|
|
47
|
+
// Ask user to pick a PR to review
|
|
48
|
+
function askUserToPickPR(prs) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
if (prs.length === 0) {
|
|
51
|
+
console.log("⚠️ No pull requests with review requested.");
|
|
52
|
+
return resolve(null);
|
|
53
|
+
}
|
|
54
|
+
console.log("\n📦 Open Pull Requests with review requested:");
|
|
55
|
+
prs.forEach((pr, i) => {
|
|
56
|
+
console.log(`${i + 1}) #${pr.number} - ${pr.title}`);
|
|
57
|
+
});
|
|
58
|
+
const rl = readline.createInterface({
|
|
59
|
+
input: process.stdin,
|
|
60
|
+
output: process.stdout,
|
|
61
|
+
});
|
|
62
|
+
rl.question(`\n👉 Choose a PR to review [1-${prs.length}]: `, (answer) => {
|
|
63
|
+
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
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
resolve(null);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Ask user to approve or reject the review suggestion
|
|
75
|
+
function askReviewApproval(suggestion) {
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
console.log('\n💡 AI-suggested review:\n');
|
|
78
|
+
console.log(suggestion);
|
|
79
|
+
console.log('\n---');
|
|
80
|
+
console.log('1) ✅ Approve');
|
|
81
|
+
console.log('2) ❌ Reject');
|
|
82
|
+
console.log('3) ✍️ Edit');
|
|
83
|
+
console.log('4) Write your own review');
|
|
84
|
+
console.log('5) 🚪 Cancel');
|
|
85
|
+
const rl = readline.createInterface({
|
|
86
|
+
input: process.stdin,
|
|
87
|
+
output: process.stdout,
|
|
88
|
+
});
|
|
89
|
+
rl.question(`\n👉 Choose an option [1-5]: `, (answer) => {
|
|
90
|
+
rl.close();
|
|
91
|
+
if (answer === '1') {
|
|
92
|
+
resolve('approve');
|
|
93
|
+
}
|
|
94
|
+
else if (answer === '2') {
|
|
95
|
+
resolve('reject');
|
|
96
|
+
}
|
|
97
|
+
else if (answer === '3') {
|
|
98
|
+
resolve('edit');
|
|
99
|
+
}
|
|
100
|
+
else if (answer === '4') {
|
|
101
|
+
resolve('custom');
|
|
102
|
+
}
|
|
103
|
+
else if (answer === '5') {
|
|
104
|
+
resolve('cancel');
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log('⚠️ Invalid selection. Defaulting to "approve".');
|
|
108
|
+
resolve('approve');
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Prompt for custom review input
|
|
114
|
+
function promptCustomReview() {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
const rl = readline.createInterface({
|
|
117
|
+
input: process.stdin,
|
|
118
|
+
output: process.stdout,
|
|
119
|
+
});
|
|
120
|
+
rl.question('\n📝 Enter your custom review:\n> ', (input) => {
|
|
121
|
+
rl.close();
|
|
122
|
+
resolve(input.trim());
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
127
|
+
try {
|
|
128
|
+
console.log("🔍 Fetching pull requests and diffs...");
|
|
129
|
+
const token = await ensureGitHubAuth();
|
|
130
|
+
const username = await getGitHubUsername(token);
|
|
131
|
+
const { owner, repo } = getRepoDetails();
|
|
132
|
+
console.log(`👤 Authenticated user: ${username}`);
|
|
133
|
+
console.log(`📦 GitHub repo: ${owner}/${repo}`);
|
|
134
|
+
console.log(`🔍 Filtering ${showAll ? "all" : "user-specific"} PRs for branch: ${branch}`);
|
|
135
|
+
const prsWithReviewRequested = await getPullRequestsForReview(token, owner, repo, username, branch, !showAll);
|
|
136
|
+
console.log(`🔍 Found ${prsWithReviewRequested.length} PR(s) requiring review.`);
|
|
137
|
+
if (prsWithReviewRequested.length === 0) {
|
|
138
|
+
console.log("⚠️ No PRs found with review requested.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const selectedIndex = await askUserToPickPR(prsWithReviewRequested.map(p => p.pr));
|
|
142
|
+
if (selectedIndex === null)
|
|
143
|
+
return;
|
|
144
|
+
const { pr, diff } = prsWithReviewRequested[selectedIndex];
|
|
145
|
+
let prDiff = diff;
|
|
146
|
+
if (!prDiff) {
|
|
147
|
+
console.log(`🔍 Fetching diff for PR #${pr.number}...`);
|
|
148
|
+
prDiff = await fetchPullRequestDiff(pr, token);
|
|
149
|
+
}
|
|
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
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
reviewEvent = 'COMMENT';
|
|
170
|
+
const customReview = await promptCustomReview();
|
|
171
|
+
console.log(`💬 Custom review: ${customReview}`);
|
|
172
|
+
await submitReview(pr.number, customReview, reviewEvent);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
console.error("❌ Error reviewing PR:", err.message);
|
|
177
|
+
}
|
|
178
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ const defaultConfig = {
|
|
|
5
5
|
model: 'llama3',
|
|
6
6
|
language: 'ts',
|
|
7
7
|
indexDir: INDEX_DIR, // Default index directory from constants
|
|
8
|
+
githubToken: '', // Add githubToken to default config
|
|
8
9
|
};
|
|
9
10
|
// Function to ensure the configuration directory exists
|
|
10
11
|
function ensureConfigDir() {
|
|
@@ -57,6 +58,15 @@ export const Config = {
|
|
|
57
58
|
writeConfig({ indexDir });
|
|
58
59
|
console.log(`📁 Index directory set to: ${indexDir}`);
|
|
59
60
|
},
|
|
61
|
+
// Get the GitHub token from the config
|
|
62
|
+
getGitHubToken() {
|
|
63
|
+
return readConfig().githubToken || null;
|
|
64
|
+
},
|
|
65
|
+
// Set the GitHub token in the config
|
|
66
|
+
setGitHubToken(token) {
|
|
67
|
+
writeConfig({ githubToken: token });
|
|
68
|
+
console.log("✅ GitHub token updated");
|
|
69
|
+
},
|
|
60
70
|
// Show the current configuration
|
|
61
71
|
show() {
|
|
62
72
|
const cfg = readConfig();
|
|
@@ -64,5 +74,6 @@ export const Config = {
|
|
|
64
74
|
console.log(` Model : ${cfg.model}`);
|
|
65
75
|
console.log(` Language : ${cfg.language}`);
|
|
66
76
|
console.log(` Index dir : ${cfg.indexDir}`);
|
|
77
|
+
console.log(` GitHub Token: ${cfg.githubToken ? '*****' : 'Not Set'}`);
|
|
67
78
|
}
|
|
68
79
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export async function fetchOpenPullRequests(token, owner, repo) {
|
|
2
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
|
|
3
|
+
const res = await fetch(url, {
|
|
4
|
+
headers: {
|
|
5
|
+
Authorization: `token ${token}`,
|
|
6
|
+
Accept: "application/vnd.github.v3+json",
|
|
7
|
+
},
|
|
8
|
+
});
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
|
|
11
|
+
}
|
|
12
|
+
const prs = await res.json();
|
|
13
|
+
return prs.map((pr) => ({
|
|
14
|
+
number: pr.number,
|
|
15
|
+
title: pr.title,
|
|
16
|
+
diff_url: pr.diff_url,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
export async function fetchPullRequestDiff(pr, token) {
|
|
20
|
+
const res = await fetch(pr.diff_url, {
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `token ${token}`,
|
|
23
|
+
Accept: "application/vnd.github.v3.diff",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
throw new Error(`Error fetching PR diff: ${res.status} ${res.statusText}`);
|
|
28
|
+
}
|
|
29
|
+
return await res.text();
|
|
30
|
+
}
|
|
31
|
+
export async function getGitHubUsername(token) {
|
|
32
|
+
const res = await fetch('https://api.github.com/user', {
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `token ${token}`,
|
|
35
|
+
Accept: "application/vnd.github.v3+json",
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
throw new Error(`Error fetching user info: ${res.status} ${res.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
const user = await res.json();
|
|
42
|
+
return user.login; // GitHub username
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Config } from '../config.js';
|
|
2
|
+
import { promptForToken } from './token.js';
|
|
3
|
+
export async function ensureGitHubAuth() {
|
|
4
|
+
// First check if the token exists in the config
|
|
5
|
+
let token = Config.getGitHubToken();
|
|
6
|
+
if (token) {
|
|
7
|
+
return token; // Token already exists in config, return it
|
|
8
|
+
}
|
|
9
|
+
// Token doesn't exist in config, prompt the user for it
|
|
10
|
+
console.log("🔐 GitHub token not found.");
|
|
11
|
+
token = await promptForToken();
|
|
12
|
+
// Save the token in the config
|
|
13
|
+
Config.setGitHubToken(token.trim());
|
|
14
|
+
return token.trim();
|
|
15
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ensureGitHubAuth } from './auth.js';
|
|
2
|
+
import { getRepoDetails } from './repo.js';
|
|
3
|
+
export async function fetchOpenPullRequests(token, owner, repo) {
|
|
4
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=open&per_page=100`;
|
|
5
|
+
const res = await fetch(url, {
|
|
6
|
+
headers: {
|
|
7
|
+
Authorization: `token ${token}`,
|
|
8
|
+
Accept: 'application/vnd.github.v3+json',
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
|
|
13
|
+
}
|
|
14
|
+
const prs = await res.json();
|
|
15
|
+
return prs.map((pr) => ({
|
|
16
|
+
number: pr.number,
|
|
17
|
+
title: pr.title,
|
|
18
|
+
url: pr.url,
|
|
19
|
+
diff_url: pr.diff_url,
|
|
20
|
+
draft: pr.draft,
|
|
21
|
+
merged_at: pr.merged_at,
|
|
22
|
+
base: pr.base,
|
|
23
|
+
requested_reviewers: pr.requested_reviewers,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
export async function getGitHubUsername(token) {
|
|
27
|
+
const res = await fetch('https://api.github.com/user', {
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `token ${token}`,
|
|
30
|
+
Accept: "application/vnd.github.v3+json",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
throw new Error(`Error fetching user info: ${res.status} ${res.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
const user = await res.json();
|
|
37
|
+
return user.login; // GitHub username
|
|
38
|
+
}
|
|
39
|
+
export async function fetchPullRequestDiff(pr, token) {
|
|
40
|
+
const res = await fetch(pr.diff_url, {
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: `token ${token}`,
|
|
43
|
+
Accept: "application/vnd.github.v3.diff",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
throw new Error(`Error fetching PR diff: ${res.status} ${res.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
return await res.text();
|
|
50
|
+
}
|
|
51
|
+
export async function submitReview(prNumber, body, event = 'COMMENT') {
|
|
52
|
+
const token = await ensureGitHubAuth();
|
|
53
|
+
const { owner, repo } = getRepoDetails();
|
|
54
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`;
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `token ${token}`,
|
|
59
|
+
Accept: 'application/vnd.github.v3+json',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
body,
|
|
63
|
+
event,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const errorText = await res.text();
|
|
68
|
+
throw new Error(`Failed to submit review: ${res.status} ${res.statusText} - ${errorText}`);
|
|
69
|
+
}
|
|
70
|
+
console.log(`✅ Submitted ${event} review for PR #${prNumber}`);
|
|
71
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ensureGitHubAuth } from './auth.js';
|
|
2
|
+
import { getRepoDetails } from './repo.js';
|
|
3
|
+
export async function validateGitHubTokenAgainstRepo() {
|
|
4
|
+
const token = await ensureGitHubAuth();
|
|
5
|
+
const { owner, repo } = getRepoDetails();
|
|
6
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
7
|
+
headers: {
|
|
8
|
+
Authorization: `Bearer ${token}`,
|
|
9
|
+
'User-Agent': 'scai-cli',
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
const error = await response.json().catch(() => ({}));
|
|
14
|
+
throw new Error(`❌ Token is invalid or lacks access to ${owner}/${repo}. GitHub says: ${response.status} ${response.statusText}${error.message ? ` – ${error.message}` : ''}`);
|
|
15
|
+
}
|
|
16
|
+
return `✅ GitHub token is valid for ${owner}/${repo}`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { Config } from '../config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Executes a Git command inside the specified working directory.
|
|
6
|
+
*/
|
|
7
|
+
function runGitCommand(cmd, cwd) {
|
|
8
|
+
return execSync(cmd, { cwd, encoding: 'utf-8' }).trim();
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Retrieves the owner and repo name from the Git remote URL inside the indexDir.
|
|
12
|
+
* This ensures we get the correct GitHub repo owner and name, regardless of current working directory.
|
|
13
|
+
*/
|
|
14
|
+
function getRepoOwnerAndNameFromGit(indexDir) {
|
|
15
|
+
try {
|
|
16
|
+
const originUrl = runGitCommand('git config --get remote.origin.url', indexDir);
|
|
17
|
+
console.log(`🔗 Git origin URL from '${indexDir}': ${originUrl}`);
|
|
18
|
+
const match = originUrl.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
19
|
+
if (!match)
|
|
20
|
+
throw new Error("❌ Could not parse GitHub repo from origin URL.");
|
|
21
|
+
const [owner, repo] = match[1].split('/');
|
|
22
|
+
console.log(`✅ Parsed from Git: owner='${owner}', repo='${repo}'`);
|
|
23
|
+
return { owner, repo };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.warn(`⚠️ Failed to parse GitHub info from Git config in '${indexDir}': ${error instanceof Error ? error.message : error}`);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fallback: Extracts GitHub repo owner and name from the index directory path.
|
|
32
|
+
*/
|
|
33
|
+
function getRepoOwnerAndNameFromIndexDir(indexDir) {
|
|
34
|
+
const parts = path.resolve(indexDir).split(path.sep);
|
|
35
|
+
const repo = parts.pop();
|
|
36
|
+
const owner = parts.pop();
|
|
37
|
+
console.log(`📁 Parsed from indexDir: owner='${owner}', repo='${repo}'`);
|
|
38
|
+
return { owner, repo };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get the GitHub repo details, always from the configured indexDir.
|
|
42
|
+
* Prefers Git config, falls back to parsing the path.
|
|
43
|
+
*/
|
|
44
|
+
export function getRepoDetails() {
|
|
45
|
+
const indexDir = Config.getIndexDir();
|
|
46
|
+
if (!indexDir) {
|
|
47
|
+
throw new Error("❌ indexDir is not configured.");
|
|
48
|
+
}
|
|
49
|
+
console.log(`📦 Resolving GitHub repo info from indexDir: ${indexDir}`);
|
|
50
|
+
try {
|
|
51
|
+
return getRepoOwnerAndNameFromGit(indexDir);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
console.log("🔁 Falling back to extracting from indexDir path...");
|
|
55
|
+
return getRepoOwnerAndNameFromIndexDir(indexDir);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Utility function to prompt for GitHub token
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
export function promptForToken() {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const rl = readline.createInterface({
|
|
6
|
+
input: process.stdin,
|
|
7
|
+
output: process.stdout,
|
|
8
|
+
});
|
|
9
|
+
rl.question('Paste your GitHub Personal Access Token (scopes: `repo`): ', (token) => {
|
|
10
|
+
rl.close();
|
|
11
|
+
resolve(token);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
//!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { Config } from './config.js';
|
|
@@ -22,19 +22,14 @@ import { runAskCommand } from './commands/AskCmd.js';
|
|
|
22
22
|
import { runBackupCommand } from './commands/BackupCmd.js';
|
|
23
23
|
import { runMigrateCommand } from "./commands/MigrateCmd.js";
|
|
24
24
|
import { runInspectCommand } from "./commands/InspectCmd.js";
|
|
25
|
+
import { reviewPullRequestCmd } from "./commands/ReviewCmd.js";
|
|
26
|
+
import { promptForToken } from "./github/token.js";
|
|
27
|
+
import { validateGitHubTokenAgainstRepo } from "./github/githubAuthCheck.js";
|
|
25
28
|
// 🎛️ CLI Setup
|
|
26
29
|
const cmd = new Command('scai')
|
|
27
30
|
.version(version)
|
|
28
31
|
.option('--model <model>', 'Set the model to use (e.g., codellama:34b)')
|
|
29
32
|
.option('--lang <lang>', 'Set the target language (ts, java, rust)');
|
|
30
|
-
function defineSuggCommand(cmd) {
|
|
31
|
-
cmd
|
|
32
|
-
.command('sugg')
|
|
33
|
-
.description('Suggest a commit message from staged changes')
|
|
34
|
-
.option('-c, --commit', 'Automatically commit with suggested message')
|
|
35
|
-
.option('-l, --changelog', 'Generate and optionally stage a changelog entry')
|
|
36
|
-
.action((options) => suggestCommitMessage(options));
|
|
37
|
-
}
|
|
38
33
|
// 🔧 Main command group
|
|
39
34
|
cmd
|
|
40
35
|
.command('init')
|
|
@@ -43,12 +38,58 @@ cmd
|
|
|
43
38
|
await bootstrap();
|
|
44
39
|
console.log('✅ Model initialization completed!');
|
|
45
40
|
});
|
|
46
|
-
// Register top-level `sugg` command
|
|
47
|
-
defineSuggCommand(cmd);
|
|
48
41
|
// 🔧 Group: Git-related commands
|
|
49
42
|
const git = cmd.command('git').description('Git utilities');
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
git
|
|
44
|
+
.command('review')
|
|
45
|
+
.description('Review an open pull request using AI')
|
|
46
|
+
.option('-a, --all', 'Show all PRs requiring a review (not just for the current user)', false)
|
|
47
|
+
.action(async (cmd) => {
|
|
48
|
+
const showAll = cmd.all;
|
|
49
|
+
await reviewPullRequestCmd('main', showAll);
|
|
50
|
+
});
|
|
51
|
+
git
|
|
52
|
+
.command('sugg')
|
|
53
|
+
.description('Suggest a commit message from staged changes')
|
|
54
|
+
.option('-c, --commit', 'Automatically commit with suggested message')
|
|
55
|
+
.option('-l, --changelog', 'Generate and optionally stage a changelog entry')
|
|
56
|
+
.action((options) => suggestCommitMessage(options));
|
|
57
|
+
// Add auth-related commands
|
|
58
|
+
const auth = cmd.command('auth').description('GitHub authentication commands');
|
|
59
|
+
auth
|
|
60
|
+
.command('check')
|
|
61
|
+
.description('Check if GitHub authentication is set up and valid')
|
|
62
|
+
.action(async () => {
|
|
63
|
+
try {
|
|
64
|
+
const token = Config.getGitHubToken();
|
|
65
|
+
if (!token) {
|
|
66
|
+
console.log('❌ GitHub authentication not found. Please set your token.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const result = await validateGitHubTokenAgainstRepo();
|
|
70
|
+
console.log(result);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
console.error(typeof err === 'string' ? err : err.message);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
auth
|
|
77
|
+
.command('reset')
|
|
78
|
+
.description('Reset GitHub authentication credentials')
|
|
79
|
+
.action(() => {
|
|
80
|
+
Config.setGitHubToken('');
|
|
81
|
+
console.log('🔄 GitHub authentication has been reset.');
|
|
82
|
+
const token = Config.getGitHubToken();
|
|
83
|
+
console.log(token ? '❌ Token still exists in the configuration.' : '✅ Token successfully removed.');
|
|
84
|
+
});
|
|
85
|
+
auth
|
|
86
|
+
.command('set')
|
|
87
|
+
.description('Set your GitHub Personal Access Token')
|
|
88
|
+
.action(async () => {
|
|
89
|
+
const token = await promptForToken();
|
|
90
|
+
Config.setGitHubToken(token.trim());
|
|
91
|
+
console.log('🔑 GitHub token set successfully.');
|
|
92
|
+
});
|
|
52
93
|
// 🛠️ Group: `gen` commands for content generation
|
|
53
94
|
const gen = cmd.command('gen').description('Generate code-related output');
|
|
54
95
|
gen
|
|
@@ -93,14 +134,12 @@ set
|
|
|
93
134
|
Config.setIndexDir(path.resolve(dir));
|
|
94
135
|
Config.show();
|
|
95
136
|
});
|
|
96
|
-
// 🧪 Diagnostics and info
|
|
97
137
|
cmd
|
|
98
138
|
.command('config')
|
|
99
139
|
.description('Show the currently active model and language settings')
|
|
100
140
|
.action(() => {
|
|
101
141
|
Config.show();
|
|
102
142
|
});
|
|
103
|
-
// 🔍 Indexing
|
|
104
143
|
cmd
|
|
105
144
|
.command('index [targetDir]')
|
|
106
145
|
.description('Index supported files in the given directory (or current folder if none)')
|
|
@@ -112,24 +151,22 @@ cmd
|
|
|
112
151
|
.command('backup')
|
|
113
152
|
.description('Backup the current .scai folder')
|
|
114
153
|
.action(runBackupCommand);
|
|
115
|
-
// 🧠 Query and assistant
|
|
116
154
|
cmd
|
|
117
155
|
.command('find <query>')
|
|
118
156
|
.description('Search indexed files by keyword')
|
|
119
157
|
.action(runFindCommand);
|
|
120
158
|
cmd
|
|
121
|
-
.command('ask [question...]')
|
|
159
|
+
.command('ask [question...]')
|
|
122
160
|
.description('Ask a question based on indexed files')
|
|
123
161
|
.action((questionParts) => {
|
|
124
162
|
const fullQuery = questionParts?.join(' ');
|
|
125
163
|
runAskCommand(fullQuery);
|
|
126
164
|
});
|
|
127
|
-
// 🛠️ Background tasks and maintenance
|
|
128
165
|
cmd
|
|
129
166
|
.command('daemon')
|
|
130
167
|
.description('Run background summarization of indexed files')
|
|
131
168
|
.action(async () => {
|
|
132
|
-
await startDaemon();
|
|
169
|
+
await startDaemon();
|
|
133
170
|
});
|
|
134
171
|
cmd
|
|
135
172
|
.command('stop-daemon')
|
|
@@ -150,9 +187,10 @@ cmd
|
|
|
150
187
|
.command('reset-db')
|
|
151
188
|
.description('Delete and reset the SQLite database')
|
|
152
189
|
.action(() => resetDatabase());
|
|
153
|
-
// 🧬 Fallback: Pipeline mode
|
|
154
190
|
cmd
|
|
155
|
-
.
|
|
191
|
+
.command('pipe')
|
|
192
|
+
.description('Run a module pipeline on a given file')
|
|
193
|
+
.argument('<file>', 'Target file')
|
|
156
194
|
.option('-m, --modules <modules>', 'Comma-separated list of modules to run (e.g., comments,cleanup,summary)')
|
|
157
195
|
.action((file, options) => {
|
|
158
196
|
if (!options.modules) {
|
|
@@ -161,7 +199,6 @@ cmd
|
|
|
161
199
|
}
|
|
162
200
|
runModulePipelineFromCLI(file, options);
|
|
163
201
|
});
|
|
164
|
-
// Add explanation about alpha features directly in the help menu
|
|
165
202
|
cmd.addHelpText('after', `
|
|
166
203
|
🚨 Alpha Features:
|
|
167
204
|
- The "index", "daemon", "stop-daemon", "reset-db" commands are considered alpha features.
|
|
@@ -169,9 +206,7 @@ cmd.addHelpText('after', `
|
|
|
169
206
|
|
|
170
207
|
💡 Use with caution and expect possible changes or instability.
|
|
171
208
|
`);
|
|
172
|
-
// ✅ Parse CLI args
|
|
173
209
|
cmd.parse(process.argv);
|
|
174
|
-
// 🔁 Apply global options post-parse
|
|
175
210
|
const opts = cmd.opts();
|
|
176
211
|
if (opts.model)
|
|
177
212
|
Config.setModel(opts.model);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { generate } from '../../lib/generate.js';
|
|
2
|
+
import { Config } from '../../config.js';
|
|
3
|
+
export const reviewModule = {
|
|
4
|
+
name: 'review',
|
|
5
|
+
description: 'Reviews code diff or PR content and provides feedback',
|
|
6
|
+
async run({ content, filepath }) {
|
|
7
|
+
const model = Config.getModel();
|
|
8
|
+
const prompt = `
|
|
9
|
+
You are a senior software engineer reviewing a pull request.
|
|
10
|
+
Give clear, constructive feedback based on the code changes below.
|
|
11
|
+
|
|
12
|
+
Changes:
|
|
13
|
+
${content}
|
|
14
|
+
`.trim();
|
|
15
|
+
const response = await generate({ content: prompt, filepath }, model);
|
|
16
|
+
return {
|
|
17
|
+
content: response.content,
|
|
18
|
+
filepath,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.52",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"scai": "./dist/index.js"
|
|
@@ -16,9 +16,19 @@
|
|
|
16
16
|
"ai",
|
|
17
17
|
"refactor",
|
|
18
18
|
"devtools",
|
|
19
|
+
"developer-tools",
|
|
19
20
|
"local",
|
|
21
|
+
"offline",
|
|
20
22
|
"typescript",
|
|
21
|
-
"llm"
|
|
23
|
+
"llm",
|
|
24
|
+
"code-review",
|
|
25
|
+
"commit-message",
|
|
26
|
+
"git",
|
|
27
|
+
"changelog",
|
|
28
|
+
"productivity",
|
|
29
|
+
"scai",
|
|
30
|
+
"review",
|
|
31
|
+
"commit"
|
|
22
32
|
],
|
|
23
33
|
"scripts": {
|
|
24
34
|
"build": "rm -rfd dist && tsc && git add .",
|
|
@@ -46,4 +56,4 @@
|
|
|
46
56
|
"dist/",
|
|
47
57
|
"README.md"
|
|
48
58
|
]
|
|
49
|
-
}
|
|
59
|
+
}
|