scai 0.1.83 → 0.1.85
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 +452 -0
- package/dist/commands/RefactorCmd.js +3 -8
- package/dist/commands/ReviewCmd.js +38 -71
- package/dist/github/github.js +10 -12
- package/dist/utils/normalizePath.js +4 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# ⚙️ scai — Smart Commit AI ✨
|
|
2
|
+
|
|
3
|
+
> AI-powered CLI tool for commit messages **and** pull request reviews — using local models.
|
|
4
|
+
|
|
5
|
+
**scai** is your AI pair‑programmer in the terminal. Focus on coding while scai:
|
|
6
|
+
|
|
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
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🚀 Features
|
|
17
|
+
|
|
18
|
+
- ⚡ Powered by open-source models (e.g. `llama3`, `codellama`)
|
|
19
|
+
- 🔍 Full-text indexing & semantic search (ALPHA)
|
|
20
|
+
- 🛠️ Built with Node.js and TypeScript
|
|
21
|
+
- ✅ Easily configurable via CLI or global flags
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## ❤️ Why Local AI?
|
|
26
|
+
|
|
27
|
+
**Your code stays yours.**
|
|
28
|
+
scai runs entirely on your machine and doesn't require cloud APIs or API keys. That means:
|
|
29
|
+
|
|
30
|
+
- ✅ **Privacy-first**: no telemetry, no server round-trips
|
|
31
|
+
- ✅ **EU & GDPR-friendly**: designed with compliance in mind
|
|
32
|
+
- ✅ **Developer control**: full transparency and override options
|
|
33
|
+
- ✅ **Offline support**: works even without an internet connection
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 📦 Installation
|
|
38
|
+
|
|
39
|
+
1. **Install Ollama (for local models)**
|
|
40
|
+
- macOS: `brew install ollama`
|
|
41
|
+
- Windows: [Download here](https://ollama.com/download)
|
|
42
|
+
- Start Ollama after installing.
|
|
43
|
+
|
|
44
|
+
2. **Install scai globally:**
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g scai
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
3. **Initialize models:**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
scai init
|
|
53
|
+
```
|
|
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
|
+
Ping [@ticcr](https://bsky.app/profile/ticcr.xyz) on Bluesky — I'd love to hear your thoughts!
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### 🔑 Setting Up Authentication (Required)
|
|
65
|
+
|
|
66
|
+
To interact with GitHub and create pull requests, `scai` needs a personal access token with **repo** permissions.
|
|
67
|
+
|
|
68
|
+
1. **Create your GitHub Access Token**
|
|
69
|
+
Follow this link to generate a token: [https://github.com/settings/personal-access-tokens](https://github.com/settings/personal-access-tokens)
|
|
70
|
+
|
|
71
|
+
Make sure you enable at least:
|
|
72
|
+
|
|
73
|
+
* `repo` (Full control of private repositories)
|
|
74
|
+
* `workflow` (If you want PRs to trigger CI)
|
|
75
|
+
|
|
76
|
+
2. **Set the token in scai:**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
scai auth set
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This stores your token locally in a secure config file. You can inspect the setup at any time:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
scai auth check
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
3. **Set the index dir:**
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
scai index set /path/to/repo
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This is the repo from which scai will look up pull requests that can be reviewed.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
## ⚒️ Usage Overview
|
|
98
|
+
### 🧠 How to Use `scai git review`
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
scai git review
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This will show you pull requests assigned to you for review:
|
|
105
|
+
|
|
106
|
+
* Understand the diffs using a local model
|
|
107
|
+
* Generate a structured pull request:
|
|
108
|
+
|
|
109
|
+
* ✅ Title
|
|
110
|
+
* ✅ Summary of changes
|
|
111
|
+
* ✅ Explanation of why the changes matter
|
|
112
|
+
* ✅ Optional changelog bullets
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
SCAI supports an integrated review flow for GitHub pull requests. To get started:
|
|
116
|
+
|
|
117
|
+
1. **Set your working index directory (once per repo):**
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
scai index set /path/to/repo
|
|
121
|
+
```
|
|
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
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
scai auth set
|
|
133
|
+
scai auth check
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
3. **Fetch and review pull requests:**
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
scai git review
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Use `-a` to list all PRs that require a review:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
scai git review -a
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### Example Workflow
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
$ scai git review -a
|
|
152
|
+
📦 Resolving GitHub repo info from indexDir: ./
|
|
153
|
+
🔗 Git origin URL: git@github.com:org/repo.git
|
|
154
|
+
✅ Parsed: owner='org', repo='repo'
|
|
155
|
+
👤 Authenticated user: dev-user123
|
|
156
|
+
|
|
157
|
+
🔍 Fetching pull requests and diffs...
|
|
158
|
+
✅ Fetched 5 PR(s) with diffs.
|
|
159
|
+
|
|
160
|
+
📦 Open Pull Requests with review requested:
|
|
161
|
+
| # | ID | TITLE | AUTHOR | STATUS | CREATED | REVIEWERS | REVIEWS |
|
|
162
|
+
| - | ---- | ----------------------------- | ---------- | ------ | ---------- | ----------------------------- | ------------------- |
|
|
163
|
+
| 1 | #120 | fix/session-timeout | dev-alice | Open | 2025-08-08 | code-analyzer\[bot], dev-bob | ✅ Approved |
|
|
164
|
+
| 2 | #118 | feature/1482-support-wfs2 | dev-carol | Open | 2025-08-07 | code-analyzer\[bot], dev-dave | ✅ Approved |
|
|
165
|
+
| 3 | #117 | refactor/win-server-support | dev-erin | Open | 2025-08-06 | dev-frank, dev-alice | ❌ Changes Requested |
|
|
166
|
+
| 4 | #114 | bump/vue-i18n-9.14.5 | dependabot | Open | 2025-08-04 | code-analyzer\[bot] | ✅ Approved |
|
|
167
|
+
| 5 | #113 | bugfix/null-navigator-check | dev-bob | Open | 2025-08-03 | dev-alice, dev-carol | ✅ Approved |
|
|
168
|
+
|
|
169
|
+
👉 Choose a PR to review [1-2]: 1
|
|
170
|
+
✅ Model response received.
|
|
171
|
+
|
|
172
|
+
💡 AI-suggested review:
|
|
173
|
+
|
|
174
|
+
Solid improvement — this patch improves session stability and handles async state more reliably.
|
|
175
|
+
You might consider renaming `sessionManager` to better reflect its dual role in auth and persistence.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
1) ✅ Approve
|
|
179
|
+
2) ❌ Reject
|
|
180
|
+
3) ✍️ Edit
|
|
181
|
+
4) Write your own review
|
|
182
|
+
5) 🚪 Cancel
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
### 🔧 How to Use `scai git commit`
|
|
188
|
+
|
|
189
|
+
Use AI to suggest a meaningful commit message based on your staged code:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
git add .
|
|
193
|
+
scai git commit
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
You can also include a changelog entry along with the commit:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
scai git commit --changelog
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
This will:
|
|
203
|
+
1. Suggest a commit message based on your `git diff --cached`
|
|
204
|
+
2. Propose a changelog entry (if relevant)
|
|
205
|
+
3. Allow you to approve, regenerate, or skip the changelog
|
|
206
|
+
4. Automatically stage and commit the changes
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### 📝 Generate a Standalone Changelog Entry
|
|
211
|
+
|
|
212
|
+
If you want to generate a changelog entry without committing:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
scai gen changelog
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This will:
|
|
219
|
+
- Analyze the current `git diff` (staged or unstaged)
|
|
220
|
+
- Propose a list of **user-facing changes** in clean markdown bullet points
|
|
221
|
+
- Let you accept, regenerate, or skip the update
|
|
222
|
+
- Append the entry to `CHANGELOG.md` and stage it if accepted
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
### 🛠️ Code Generation Commands (`gen` group)
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
scai gen summ <file>
|
|
229
|
+
scai gen comm <file>
|
|
230
|
+
scai gen changelog
|
|
231
|
+
scai gen tests <file>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
* `summ`: Summarize a file
|
|
235
|
+
* `comm`: Add comments to a file
|
|
236
|
+
* `changelog`: Update or create `CHANGELOG.md` from Git diff
|
|
237
|
+
* `tests`: Create Jest test stubs (ALPHA)
|
|
238
|
+
|
|
239
|
+
You can also pipe file content directly:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
cat src/utils/math.ts | scai gen summ
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## ⚙️ Configuration
|
|
248
|
+
|
|
249
|
+
scai stores settings in `~/.scai/config.json`. You can override or view them:
|
|
250
|
+
|
|
251
|
+
* **Set model:**
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
scai set model codellama:7b
|
|
255
|
+
```
|
|
256
|
+
* **Set language:**
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
scai set lang ts
|
|
260
|
+
```
|
|
261
|
+
* **Show config:**
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
scai config
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
<br>
|
|
268
|
+
|
|
269
|
+
## 🔁 Background Daemon and Indexing ⚠️ ALPHA Notice
|
|
270
|
+
|
|
271
|
+
These features are experimental and subject to change:
|
|
272
|
+
|
|
273
|
+
* `index`, `find`, `ask`
|
|
274
|
+
* `daemon`, `stop-daemon`
|
|
275
|
+
* `gen tests` (test generation)
|
|
276
|
+
|
|
277
|
+
<br>
|
|
278
|
+
|
|
279
|
+
## Commands
|
|
280
|
+
|
|
281
|
+
### `index`
|
|
282
|
+
The `index` command is used to manage and perform operations on the indexed files and repositories.
|
|
283
|
+
|
|
284
|
+
#### `scai index start`
|
|
285
|
+
|
|
286
|
+
Index supported files in the configured index directory.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
scai index set <dir>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Set and activate the index directory.
|
|
293
|
+
```bash
|
|
294
|
+
scai index list
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
List all indexed repositories.
|
|
298
|
+
```bash
|
|
299
|
+
scai index switch
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Switch active repository (by key or indexDir). Run without input for an interactive list of repositories.
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
> **Note:** Indexing very large repositories (millions of lines) may take **hours or days**. Please be patient, and only index huge codebases if you are ok with some extra processing taking place on your computer.
|
|
308
|
+
|
|
309
|
+
The `scai index` command **automatically** starts a background daemon that continuously:
|
|
310
|
+
|
|
311
|
+
* Scans your target directory
|
|
312
|
+
* Summarizes new or changed files
|
|
313
|
+
* Updates embeddings and the search index
|
|
314
|
+
|
|
315
|
+
You won't gain much value from the index unless you scope it to one repository.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
### 🔍 Codebase Search & Ask (ALPHA)
|
|
320
|
+
|
|
321
|
+
> **Important:** You must `index` a **code repository** first or `find` and `ask` have no context to work with.
|
|
322
|
+
|
|
323
|
+
1. **Set index directory:**
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
scai index set /path/to/repo
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
2. **Index your repo (once):**
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
scai index start
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
3. The daemon is designed to **consume minimal resources** and run unobtrusively. You can control it with:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
scai daemon # Start or show daemon status
|
|
339
|
+
scai stop-daemon # Stop the background indexer
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
4. **Keyword search:**
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
scai find YourClassName
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
5. **Natural-language questions:**
|
|
350
|
+
|
|
351
|
+
### 🧠 Natural-language questions
|
|
352
|
+
|
|
353
|
+
Ask questions about your codebase using `scai ask`.
|
|
354
|
+
|
|
355
|
+
You can run it in two ways:
|
|
356
|
+
|
|
357
|
+
1. **Inline question**
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
scai ask "How does the controller work?"
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
2. **Interactive prompt**
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
scai ask
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Press enter**
|
|
370
|
+
, then type your question when prompted:
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
> How does the controller work?
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
</br>
|
|
377
|
+
|
|
378
|
+
### 🚨 **OBS** 🚨
|
|
379
|
+
|
|
380
|
+
`find` and `ask` rely on that index—without it they will return no useful results.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
Note 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.
|
|
385
|
+
|
|
386
|
+
If you're upgrading from an earlier version, please run the following commands to avoid indexing issues:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
scai reset-db
|
|
390
|
+
scai index
|
|
391
|
+
```
|
|
392
|
+
</br>
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## 🛍️ Maintenance & Utilities
|
|
397
|
+
|
|
398
|
+
* **Reset database (w/ backup):**
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
scai reset-db
|
|
402
|
+
```
|
|
403
|
+
* **Backup only of `~/.scai`:**
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
scai backup
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## 🧺 Module Pipeline Mode
|
|
412
|
+
|
|
413
|
+
For custom pipelines on a single file:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
scai src/file.ts -m summary,comments
|
|
417
|
+
```
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 🔐 License & Fair Use
|
|
421
|
+
|
|
422
|
+
**scai is free to use** for individuals, teams, and commercial projects.
|
|
423
|
+
|
|
424
|
+
You may:
|
|
425
|
+
|
|
426
|
+
* ✅ Use internally or commercially
|
|
427
|
+
* ✅ Fork and improve
|
|
428
|
+
* ✅ Recommend to others
|
|
429
|
+
|
|
430
|
+
You may **not**:
|
|
431
|
+
|
|
432
|
+
* ❌ Resell as a product or service
|
|
433
|
+
* ❌ Claim ownership of the tool
|
|
434
|
+
|
|
435
|
+
</br>
|
|
436
|
+
|
|
437
|
+
### 📄 License
|
|
438
|
+
|
|
439
|
+
Free for personal and internal company use only.
|
|
440
|
+
Commercial use (resale, SaaS, inclusion in paid tools) is prohibited.
|
|
441
|
+
Contact me for commercial licensing.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
</br>
|
|
446
|
+
|
|
447
|
+
## 🙌 Feedback
|
|
448
|
+
|
|
449
|
+
Questions, ideas, or bugs?
|
|
450
|
+
Ping [@ticcr](https://bsky.app/profile/ticcr.xyz) on Bluesky — I'd love to hear your thoughts!
|
|
451
|
+
|
|
452
|
+
---
|
|
@@ -2,15 +2,13 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { runModulePipeline } from '../pipeline/runModulePipeline.js';
|
|
4
4
|
import { addCommentsModule } from '../pipeline/modules/commentModule.js';
|
|
5
|
+
import { normalizePath } from '../utils/normalizePath.js';
|
|
5
6
|
export async function handleRefactor(filepath, options = {}) {
|
|
6
7
|
try {
|
|
7
|
-
// Normalize
|
|
8
|
-
|
|
9
|
-
filepath = `./${filepath}`;
|
|
10
|
-
}
|
|
8
|
+
// Normalize and resolve filepath (includes expanding ~ and consistent slashes)
|
|
9
|
+
filepath = normalizePath(filepath);
|
|
11
10
|
const { dir, name, ext } = path.parse(filepath);
|
|
12
11
|
const refactoredPath = path.join(dir, `${name}.refactored${ext}`);
|
|
13
|
-
// --apply flag: use existing refactored file and overwrite original
|
|
14
12
|
if (options.apply) {
|
|
15
13
|
try {
|
|
16
14
|
const refactoredCode = await fs.readFile(refactoredPath, 'utf-8');
|
|
@@ -23,13 +21,10 @@ export async function handleRefactor(filepath, options = {}) {
|
|
|
23
21
|
}
|
|
24
22
|
return;
|
|
25
23
|
}
|
|
26
|
-
// Read source code
|
|
27
24
|
const content = await fs.readFile(filepath, 'utf-8');
|
|
28
|
-
// Run through pipeline modules
|
|
29
25
|
const response = await runModulePipeline([addCommentsModule], { content });
|
|
30
26
|
if (!response.content.trim())
|
|
31
27
|
throw new Error('⚠️ Model returned empty result');
|
|
32
|
-
// Save refactored output
|
|
33
28
|
await fs.writeFile(refactoredPath, response.content, 'utf-8');
|
|
34
29
|
console.log(`✅ Refactored code saved to: ${refactoredPath}`);
|
|
35
30
|
console.log(`ℹ️ Run again with '--apply' to overwrite the original.`);
|
|
@@ -164,24 +164,6 @@ function askReviewApproval() {
|
|
|
164
164
|
});
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
|
-
function askFinalReviewApproval() {
|
|
168
|
-
return new Promise((resolve) => {
|
|
169
|
-
console.log('\n---');
|
|
170
|
-
console.log('1) ✅ Approve');
|
|
171
|
-
console.log('2) ❌ Request Changes');
|
|
172
|
-
console.log('3) 🚫 Cancel');
|
|
173
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
174
|
-
rl.question(`\n👉 Choose an option [1-3]: `, (answer) => {
|
|
175
|
-
rl.close();
|
|
176
|
-
if (answer === '1')
|
|
177
|
-
resolve('approve');
|
|
178
|
-
else if (answer === '2')
|
|
179
|
-
resolve('request-changes');
|
|
180
|
-
else
|
|
181
|
-
resolve('cancel');
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
167
|
// Prompt for custom review
|
|
186
168
|
function promptCustomReview() {
|
|
187
169
|
return new Promise((resolve) => {
|
|
@@ -212,23 +194,20 @@ function chunkDiff(diff, review_id) {
|
|
|
212
194
|
const fullChunk = 'diff --git ' + chunk;
|
|
213
195
|
const filePathMatch = fullChunk.match(/^diff --git a\/(.+?) b\//);
|
|
214
196
|
const filePath = filePathMatch ? filePathMatch[1] : 'unknown';
|
|
215
|
-
// Now we extract hunks and lines as per the DiffHunk type
|
|
216
197
|
const hunks = [];
|
|
217
198
|
let currentHunk = null;
|
|
218
|
-
//
|
|
199
|
+
// This counts diff lines for *this file only* (context/+/- lines after first @@)
|
|
200
|
+
let positionCounter = 0;
|
|
219
201
|
const lines = fullChunk.split('\n');
|
|
220
202
|
lines.forEach(line => {
|
|
221
|
-
const hunkHeaderMatch = line.match(/^@@ -(\d+)
|
|
203
|
+
const hunkHeaderMatch = line.match(/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
|
|
222
204
|
if (hunkHeaderMatch) {
|
|
223
|
-
|
|
224
|
-
if (currentHunk) {
|
|
205
|
+
if (currentHunk)
|
|
225
206
|
hunks.push(currentHunk);
|
|
226
|
-
}
|
|
227
|
-
// Parse the hunk header
|
|
228
207
|
const oldStart = parseInt(hunkHeaderMatch[1], 10);
|
|
229
208
|
const newStart = parseInt(hunkHeaderMatch[3], 10);
|
|
230
|
-
const oldLines = parseInt(hunkHeaderMatch[2], 10);
|
|
231
|
-
const newLines = parseInt(hunkHeaderMatch[4], 10);
|
|
209
|
+
const oldLines = parseInt(hunkHeaderMatch[2] || '0', 10);
|
|
210
|
+
const newLines = parseInt(hunkHeaderMatch[4] || '0', 10);
|
|
232
211
|
currentHunk = {
|
|
233
212
|
oldStart,
|
|
234
213
|
newStart,
|
|
@@ -236,33 +215,33 @@ function chunkDiff(diff, review_id) {
|
|
|
236
215
|
newLines,
|
|
237
216
|
lines: [],
|
|
238
217
|
};
|
|
218
|
+
return; // don’t count @@ header line in positionCounter
|
|
239
219
|
}
|
|
240
|
-
|
|
241
|
-
//
|
|
220
|
+
if (currentHunk) {
|
|
221
|
+
// Each line after @@ counts towards the diff position
|
|
222
|
+
positionCounter++;
|
|
242
223
|
let lineType = 'context';
|
|
243
224
|
if (line.startsWith('+'))
|
|
244
225
|
lineType = 'add';
|
|
245
226
|
if (line.startsWith('-'))
|
|
246
227
|
lineType = 'del';
|
|
247
|
-
// Create the DiffLine object
|
|
248
228
|
currentHunk.lines.push({
|
|
249
229
|
line,
|
|
250
230
|
type: lineType,
|
|
251
231
|
lineNumberOld: lineType === 'del' ? currentHunk.oldStart++ : undefined,
|
|
252
232
|
lineNumberNew: lineType === 'add' ? currentHunk.newStart++ : undefined,
|
|
253
|
-
|
|
233
|
+
position: positionCounter, // <-- key for GitHub inline API
|
|
234
|
+
review_id,
|
|
254
235
|
});
|
|
255
236
|
}
|
|
256
237
|
});
|
|
257
|
-
|
|
258
|
-
if (currentHunk) {
|
|
238
|
+
if (currentHunk)
|
|
259
239
|
hunks.push(currentHunk);
|
|
260
|
-
}
|
|
261
240
|
return {
|
|
262
241
|
filePath,
|
|
263
242
|
content: fullChunk,
|
|
264
|
-
hunks,
|
|
265
|
-
review_id,
|
|
243
|
+
hunks,
|
|
244
|
+
review_id,
|
|
266
245
|
};
|
|
267
246
|
});
|
|
268
247
|
}
|
|
@@ -419,48 +398,36 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
|
419
398
|
for (let i = 0; i < chunks.length; i++) {
|
|
420
399
|
const chunk = chunks[i];
|
|
421
400
|
const { choice, summary } = await reviewChunk(chunk, i, chunks.length);
|
|
422
|
-
if (choice === '
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
body: summary,
|
|
426
|
-
line: chunk.hunks[0]?.newStart || 1,
|
|
427
|
-
side: 'RIGHT',
|
|
428
|
-
});
|
|
429
|
-
console.log(`💬 Posted AI review for chunk ${i + 1}`);
|
|
430
|
-
}
|
|
431
|
-
else if (choice === 'reject') {
|
|
432
|
-
allApproved = false;
|
|
433
|
-
reviewComments.push({
|
|
434
|
-
path: chunk.filePath,
|
|
435
|
-
body: summary,
|
|
436
|
-
line: chunk.hunks[0]?.newStart || 1,
|
|
437
|
-
side: 'RIGHT',
|
|
438
|
-
});
|
|
401
|
+
if (choice === 'cancel' || choice === 'skip') {
|
|
402
|
+
console.log(chalk.gray(`⏭️ Skipped chunk ${i + 1}`));
|
|
403
|
+
continue;
|
|
439
404
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
});
|
|
405
|
+
// Find the first line in the chunk with a valid position (usually the first 'add' or 'context' line)
|
|
406
|
+
const firstLineWithPosition = chunk.hunks
|
|
407
|
+
.flatMap(hunk => hunk.lines)
|
|
408
|
+
.find(line => line.position !== undefined);
|
|
409
|
+
if (!firstLineWithPosition) {
|
|
410
|
+
console.warn(`⚠️ Could not find valid position for inline comment in chunk ${i + 1}. Skipping comment.`);
|
|
411
|
+
continue;
|
|
448
412
|
}
|
|
449
|
-
|
|
450
|
-
|
|
413
|
+
let commentBody = summary;
|
|
414
|
+
if (choice === 'custom') {
|
|
415
|
+
commentBody = await promptCustomReview();
|
|
451
416
|
}
|
|
452
|
-
else if (typeof choice === 'string') {
|
|
453
|
-
|
|
454
|
-
path: chunk.filePath,
|
|
455
|
-
body: choice,
|
|
456
|
-
line: chunk.hunks[0]?.newStart || 1,
|
|
457
|
-
side: 'RIGHT',
|
|
458
|
-
});
|
|
417
|
+
else if (typeof choice === 'string' && !['approve', 'reject', 'custom'].includes(choice)) {
|
|
418
|
+
commentBody = choice;
|
|
459
419
|
}
|
|
420
|
+
reviewComments.push({
|
|
421
|
+
path: chunk.filePath,
|
|
422
|
+
body: commentBody,
|
|
423
|
+
position: firstLineWithPosition.position,
|
|
424
|
+
});
|
|
425
|
+
if (choice === 'reject')
|
|
426
|
+
allApproved = false;
|
|
460
427
|
}
|
|
461
428
|
console.log(chalk.blueBright('\n📝 Review Comments Preview:'));
|
|
462
429
|
reviewComments.forEach((comment, idx) => {
|
|
463
|
-
console.log(`${idx + 1}. ${comment.path}:${comment.
|
|
430
|
+
console.log(`${idx + 1}. ${comment.path}:${comment.position} — ${comment.body}`);
|
|
464
431
|
});
|
|
465
432
|
const shouldApprove = allApproved;
|
|
466
433
|
const hasInlineComments = reviewComments.length > 0;
|
package/dist/github/github.js
CHANGED
|
@@ -81,30 +81,29 @@ export async function submitReview(prNumber, body, event, comments) {
|
|
|
81
81
|
const token = await ensureGitHubAuth();
|
|
82
82
|
const { owner, repo } = await getRepoDetails();
|
|
83
83
|
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`;
|
|
84
|
+
// Prepare payload
|
|
85
|
+
const payload = { body, event };
|
|
86
|
+
if (comments && comments.length > 0) {
|
|
87
|
+
payload.comments = comments;
|
|
88
|
+
}
|
|
84
89
|
const res = await fetch(url, {
|
|
85
90
|
method: 'POST',
|
|
86
91
|
headers: {
|
|
87
92
|
Authorization: `token ${token}`,
|
|
88
93
|
Accept: 'application/vnd.github.v3+json',
|
|
89
94
|
},
|
|
90
|
-
body: JSON.stringify(
|
|
91
|
-
body,
|
|
92
|
-
event,
|
|
93
|
-
comments,
|
|
94
|
-
}),
|
|
95
|
+
body: JSON.stringify(payload),
|
|
95
96
|
});
|
|
96
97
|
if (!res.ok) {
|
|
97
98
|
const errorText = await res.text();
|
|
98
|
-
// Attempt to parse error body
|
|
99
99
|
let parsed = {};
|
|
100
100
|
try {
|
|
101
101
|
parsed = JSON.parse(errorText);
|
|
102
102
|
}
|
|
103
103
|
catch (_) {
|
|
104
|
-
//
|
|
104
|
+
// fallback to raw text
|
|
105
105
|
}
|
|
106
|
-
const knownErrors = Array.isArray(parsed.errors) ? parsed.errors.join('; ') : '';
|
|
107
|
-
// Handle known error cases
|
|
106
|
+
const knownErrors = Array.isArray(parsed.errors) ? parsed.errors.map((e) => e.message || e).join('; ') : '';
|
|
108
107
|
if (res.status === 422) {
|
|
109
108
|
if (knownErrors.includes('Can not approve your own pull request')) {
|
|
110
109
|
console.warn(`⚠️ Skipping approval: You cannot approve your own pull request.`);
|
|
@@ -114,8 +113,8 @@ export async function submitReview(prNumber, body, event, comments) {
|
|
|
114
113
|
console.warn(`⚠️ Cannot post comments: PR has no diff.`);
|
|
115
114
|
return;
|
|
116
115
|
}
|
|
117
|
-
if (knownErrors.includes('path is missing') || knownErrors.includes('line is missing')) {
|
|
118
|
-
console.warn(`⚠️ Some inline comments are missing
|
|
116
|
+
if (knownErrors.includes('path is missing') || knownErrors.includes('line is missing') || knownErrors.includes('position is missing')) {
|
|
117
|
+
console.warn(`⚠️ Some inline comments are missing required fields. Skipping review.`);
|
|
119
118
|
return;
|
|
120
119
|
}
|
|
121
120
|
if (knownErrors.includes('Position is invalid') || knownErrors.includes('line must be part of the diff')) {
|
|
@@ -123,7 +122,6 @@ export async function submitReview(prNumber, body, event, comments) {
|
|
|
123
122
|
return;
|
|
124
123
|
}
|
|
125
124
|
}
|
|
126
|
-
// Unknown error
|
|
127
125
|
throw new Error(`Failed to submit review: ${res.status} ${res.statusText} - ${errorText}`);
|
|
128
126
|
}
|
|
129
127
|
console.log(`✅ Submitted ${event} review for PR #${prNumber}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/utils/normalizePath.ts
|
|
2
|
+
import os from 'os';
|
|
2
3
|
import path from "path";
|
|
3
4
|
/**
|
|
4
5
|
* Normalizes a path string for loose, fuzzy matching:
|
|
@@ -11,6 +12,9 @@ export function normalizePathForLooseMatch(p) {
|
|
|
11
12
|
}
|
|
12
13
|
// Helper to normalize and resolve paths to a consistent format (forward slashes)
|
|
13
14
|
export function normalizePath(p) {
|
|
15
|
+
if (p.startsWith('~')) {
|
|
16
|
+
p = path.join(os.homedir(), p.slice(1));
|
|
17
|
+
}
|
|
14
18
|
return path.resolve(p).replace(/\\/g, '/');
|
|
15
19
|
}
|
|
16
20
|
export function getRepoKeyForPath(pathToMatch, config) {
|