pull-request-review 1.0.0
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 +336 -0
- package/package.json +41 -0
- package/src/ai.js +23 -0
- package/src/claude.js +59 -0
- package/src/cli.js +356 -0
- package/src/config.js +102 -0
- package/src/file.js +22 -0
- package/src/git.js +83 -0
- package/src/prompt.js +68 -0
- package/src/providers/claude.js +23 -0
- package/src/providers/codex.js +39 -0
- package/src/providers/copilot.js +38 -0
- package/src/providers/cursor.js +39 -0
- package/src/providers/gemini.js +38 -0
- package/src/providers/index.js +80 -0
- package/src/providers/openai.js +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# pr-review
|
|
2
|
+
|
|
3
|
+
> AI-powered Pull Request reviews — pick your AI, get a structured Markdown report, right from your terminal.
|
|
4
|
+
|
|
5
|
+
`pr-review` diffs two branches, sends the diff to your chosen AI, and saves a structured Markdown review to a file. If a GitHub PR is open, the file is automatically named `pr-<number>-review.md`.
|
|
6
|
+
|
|
7
|
+
**No API keys required.** All providers use their official CLI tools with your existing authenticated sessions.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g pull-request-review
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or from source:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/hardik-143/pull-request-review
|
|
21
|
+
cd pr-review
|
|
22
|
+
npm install
|
|
23
|
+
npm install -g .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Node.js ≥ 18
|
|
31
|
+
- Git in `PATH`
|
|
32
|
+
- At least one AI provider CLI installed and authenticated (see [Providers](#providers))
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quickstart
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Run inside any git repository
|
|
40
|
+
cd your-project
|
|
41
|
+
pr-review
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You'll be prompted to pick an AI provider, a model, and your branches. That's it.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Providers
|
|
49
|
+
|
|
50
|
+
All five providers use their official CLI — no API keys, no environment variables.
|
|
51
|
+
|
|
52
|
+
| Provider | CLI binary | Auth |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| **Claude** | `claude` | Already logged in via Claude Code |
|
|
55
|
+
| **GitHub Copilot** | `copilot` | `copilot /login` |
|
|
56
|
+
| **OpenAI Codex** | `codex` | Sign in on first `codex` run |
|
|
57
|
+
| **Google Gemini** | `gemini` | Sign in on first `gemini` run |
|
|
58
|
+
| **Cursor** | `agent` | Install Cursor → Cmd+Shift+P → *Install agent in PATH* |
|
|
59
|
+
|
|
60
|
+
### Installing provider CLIs
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Claude
|
|
64
|
+
# Already installed if you're using Claude Code
|
|
65
|
+
|
|
66
|
+
# GitHub Copilot
|
|
67
|
+
npm install -g @github/copilot
|
|
68
|
+
copilot /login
|
|
69
|
+
|
|
70
|
+
# OpenAI Codex
|
|
71
|
+
npm install -g @openai/codex
|
|
72
|
+
codex # sign in on first launch
|
|
73
|
+
|
|
74
|
+
# Google Gemini
|
|
75
|
+
npm install -g @google/gemini-cli
|
|
76
|
+
gemini # sign in on first launch
|
|
77
|
+
|
|
78
|
+
# Cursor
|
|
79
|
+
# Install Cursor app → Cmd+Shift+P → "Install cursor/agent in PATH"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
### Interactive review
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pr-review
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Flow:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
Select AI provider:
|
|
96
|
+
|
|
97
|
+
1) Claude (Anthropic) — Uses local claude CLI — no API key needed
|
|
98
|
+
2) GitHub Copilot — Uses copilot CLI — run `copilot /login` first
|
|
99
|
+
3) OpenAI Codex — Uses codex CLI — run `codex` to sign in
|
|
100
|
+
4) Google Gemini — Uses gemini CLI — run `gemini` to authenticate
|
|
101
|
+
5) Cursor — Uses agent CLI — install Cursor + add agent to PATH
|
|
102
|
+
|
|
103
|
+
Choice [1]:
|
|
104
|
+
|
|
105
|
+
Select Claude model:
|
|
106
|
+
|
|
107
|
+
1) claude-sonnet-4-5 · Recommended
|
|
108
|
+
2) claude-opus-4-5 · Most capable
|
|
109
|
+
3) claude-haiku-3-5 · Fastest
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
Choice [1]:
|
|
113
|
+
|
|
114
|
+
Source branch (default: feature/auth):
|
|
115
|
+
Destination branch (default: main):
|
|
116
|
+
|
|
117
|
+
🔗 PR #42 detected → output: pr-42-review.md
|
|
118
|
+
|
|
119
|
+
✔ Diff fetched — 4,821 chars
|
|
120
|
+
✔ Claude review received
|
|
121
|
+
✔ Report saved → /your/project/pr-42-review.md
|
|
122
|
+
|
|
123
|
+
✅ PR Review complete!
|
|
124
|
+
PR #42 · Claude · claude-sonnet-4-5
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### Commands & flags
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
pr-review Interactive PR review
|
|
133
|
+
pr-review review Explicit review subcommand (same as above)
|
|
134
|
+
pr-review config View and edit global config
|
|
135
|
+
pr-review review --staged Review staged (uncommitted) changes
|
|
136
|
+
pr-review review --provider=<provider> Skip provider prompt
|
|
137
|
+
pr-review review --model=<model> Skip model prompt
|
|
138
|
+
pr-review review --base=<branch> Override destination/base branch
|
|
139
|
+
pr-review review --focus=<area> Focus: security, performance, etc.
|
|
140
|
+
pr-review review --output=<file> Override output file name
|
|
141
|
+
pr-review --help Show help
|
|
142
|
+
pr-review --version Show version
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Examples
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# Standard interactive review
|
|
149
|
+
pr-review
|
|
150
|
+
|
|
151
|
+
# Explicit review subcommand
|
|
152
|
+
pr-review review
|
|
153
|
+
|
|
154
|
+
# Non-interactive — skip all prompts
|
|
155
|
+
pr-review review --provider=claude --model=claude-sonnet-4-5 --base=main
|
|
156
|
+
|
|
157
|
+
# Security-focused review of staged changes
|
|
158
|
+
pr-review review --staged --focus=security
|
|
159
|
+
|
|
160
|
+
# Use Gemini's most capable model
|
|
161
|
+
pr-review review --provider=gemini --model=pro
|
|
162
|
+
|
|
163
|
+
# Review with GitHub Copilot, custom output file
|
|
164
|
+
pr-review review --provider=copilot --output=review-$(date +%Y%m%d).md
|
|
165
|
+
|
|
166
|
+
# View and update config
|
|
167
|
+
pr-review config
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## PR Number Detection
|
|
173
|
+
|
|
174
|
+
If a GitHub PR is open for the current branch, `pr-review` automatically detects it via `gh pr view` and names the output file:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
pr-42-review.md
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Falls back to `pr-review-review.md` (from config) if no PR is found or `gh` is not installed.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Models
|
|
185
|
+
|
|
186
|
+
### Claude
|
|
187
|
+
| Model | Description |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `claude-sonnet-4-5` | Recommended (default) |
|
|
190
|
+
| `claude-opus-4-5` | Most capable |
|
|
191
|
+
| `claude-haiku-3-5` | Fastest |
|
|
192
|
+
| `claude-sonnet-4` | Previous Sonnet |
|
|
193
|
+
| `claude-opus-4` | Previous Opus |
|
|
194
|
+
|
|
195
|
+
### GitHub Copilot
|
|
196
|
+
| Model | Description |
|
|
197
|
+
|---|---|
|
|
198
|
+
| `default` | Your Copilot plan's default (recommended) |
|
|
199
|
+
| `gpt-4.1` | GPT-4.1 |
|
|
200
|
+
| `gpt-4o` | GPT-4o |
|
|
201
|
+
| `claude-sonnet-4-5` | Claude via Copilot |
|
|
202
|
+
| `o3` | Reasoning model |
|
|
203
|
+
| `o4-mini` | Fast reasoning |
|
|
204
|
+
|
|
205
|
+
### OpenAI Codex
|
|
206
|
+
| Model | Description |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `default` | Your Codex plan's default (recommended) |
|
|
209
|
+
| `gpt-4o` | GPT-4o |
|
|
210
|
+
| `o3` | Reasoning |
|
|
211
|
+
| `o4-mini` | Fast reasoning |
|
|
212
|
+
|
|
213
|
+
### Google Gemini
|
|
214
|
+
| Model alias | Resolves to | Description |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| `flash` | gemini-2.5-flash | Recommended (default) |
|
|
217
|
+
| `pro` | gemini-2.5-pro | Most capable |
|
|
218
|
+
| `flash-lite` | gemini-2.5-flash-lite | Fastest |
|
|
219
|
+
| `auto` | auto-select | Best available |
|
|
220
|
+
|
|
221
|
+
### Cursor
|
|
222
|
+
| Model | Description |
|
|
223
|
+
|---|---|
|
|
224
|
+
| `default` | Your Cursor plan's default (recommended) |
|
|
225
|
+
| `gpt-4o` | GPT-4o |
|
|
226
|
+
| `claude-sonnet-4-5` | Claude via Cursor |
|
|
227
|
+
| `gemini-2.5-pro` | Gemini via Cursor |
|
|
228
|
+
| `o3` | Reasoning |
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Global Config
|
|
233
|
+
|
|
234
|
+
Auto-created at `~/.pr-review/config.json` on first run.
|
|
235
|
+
|
|
236
|
+
**Default config:**
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"defaultProvider": "claude",
|
|
241
|
+
"defaultModels": {
|
|
242
|
+
"claude": "claude-sonnet-4-5",
|
|
243
|
+
"copilot": "default",
|
|
244
|
+
"codex": "default",
|
|
245
|
+
"gemini": "flash",
|
|
246
|
+
"cursor": "default"
|
|
247
|
+
},
|
|
248
|
+
"defaultBaseBranch": "main",
|
|
249
|
+
"maxDiffLength": 100000,
|
|
250
|
+
"ignoreFiles": ["package-lock.json", "yarn.lock"],
|
|
251
|
+
"outputFile": "pr-review-review.md",
|
|
252
|
+
"strictMode": true
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The last-used provider and model per provider are automatically remembered.
|
|
257
|
+
|
|
258
|
+
**Edit interactively:**
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
pr-review config
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Or edit directly:**
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
nano ~/.pr-review/config.json
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Config options
|
|
271
|
+
|
|
272
|
+
| Key | Description | Default |
|
|
273
|
+
|---|---|---|
|
|
274
|
+
| `defaultProvider` | AI provider to pre-select | `claude` |
|
|
275
|
+
| `defaultModels` | Last-used model per provider | see above |
|
|
276
|
+
| `defaultBaseBranch` | Branch to diff against | `main` |
|
|
277
|
+
| `maxDiffLength` | Max diff characters sent to AI | `100000` |
|
|
278
|
+
| `ignoreFiles` | Files excluded from diff | `[package-lock.json, yarn.lock]` |
|
|
279
|
+
| `outputFile` | Fallback report filename | `pr-review-review.md` |
|
|
280
|
+
| `strictMode` | Strict review mode in prompt | `true` |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Output format
|
|
285
|
+
|
|
286
|
+
Every generated report has this structure:
|
|
287
|
+
|
|
288
|
+
```markdown
|
|
289
|
+
---
|
|
290
|
+
<!-- Generated by pr-review on 2026-04-03T18:00:00.000Z -->
|
|
291
|
+
<!-- Provider: Claude | Model: claude-sonnet-4-5 | Diff: 4821 chars | feature/auth → main -->
|
|
292
|
+
<!-- PR: #42 -->
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
# PR Review: `feature/auth` → `main`
|
|
296
|
+
|
|
297
|
+
## Summary
|
|
298
|
+
## Critical Issues
|
|
299
|
+
## Security
|
|
300
|
+
## Performance
|
|
301
|
+
## Code Quality & Improvements
|
|
302
|
+
## Test Coverage
|
|
303
|
+
## Suggestions
|
|
304
|
+
## Final Verdict
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Project structure
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
pr-review/
|
|
313
|
+
├── src/
|
|
314
|
+
│ ├── cli.js Main entry point, argument parsing, user prompts
|
|
315
|
+
│ ├── ai.js Provider dispatcher
|
|
316
|
+
│ ├── config.js Config manager (~/.pr-review/config.json)
|
|
317
|
+
│ ├── git.js Git diff + PR number detection
|
|
318
|
+
│ ├── prompt.js PR review prompt builder + system prompt
|
|
319
|
+
│ ├── file.js Report file writer
|
|
320
|
+
│ └── providers/
|
|
321
|
+
│ ├── index.js Provider registry (models, labels, descriptions)
|
|
322
|
+
│ ├── claude.js claude CLI adapter
|
|
323
|
+
│ ├── copilot.js copilot CLI adapter
|
|
324
|
+
│ ├── codex.js codex CLI adapter
|
|
325
|
+
│ ├── gemini.js gemini CLI adapter
|
|
326
|
+
│ └── cursor.js agent CLI adapter (Cursor)
|
|
327
|
+
├── package.json
|
|
328
|
+
└── README.md
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## License
|
|
334
|
+
|
|
335
|
+
MIT
|
|
336
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pull-request-review",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered Pull Request reviews using Claude",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "Hardik <dhardik1430@gmail.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/hardik-143/pull-request-review.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/hardik-143/pull-request-review#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/hardik-143/pull-request-review/issues"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"bin": {
|
|
19
|
+
"pr-review": "./src/cli.js"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"start": "node src/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"chalk": "^5.3.0",
|
|
26
|
+
"ora": "^8.1.1"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"pr-review",
|
|
33
|
+
"code-review",
|
|
34
|
+
"claude",
|
|
35
|
+
"anthropic",
|
|
36
|
+
"ai",
|
|
37
|
+
"git",
|
|
38
|
+
"cli"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|
package/src/ai.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { callClaude } from './providers/claude.js';
|
|
2
|
+
import { callCopilot } from './providers/copilot.js';
|
|
3
|
+
import { callCodex } from './providers/codex.js';
|
|
4
|
+
import { callGemini } from './providers/gemini.js';
|
|
5
|
+
import { callCursor } from './providers/cursor.js';
|
|
6
|
+
|
|
7
|
+
const DISPATCH = {
|
|
8
|
+
claude: callClaude,
|
|
9
|
+
copilot: callCopilot,
|
|
10
|
+
codex: callCodex,
|
|
11
|
+
gemini: callGemini,
|
|
12
|
+
cursor: callCursor,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function callAI(prompt, provider, model) {
|
|
16
|
+
const fn = DISPATCH[provider];
|
|
17
|
+
if (!fn) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Unknown provider: "${provider}". Valid providers: ${Object.keys(DISPATCH).join(', ')}`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return fn(prompt, model);
|
|
23
|
+
}
|
package/src/claude.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { SYSTEM_PROMPT } from './prompt.js';
|
|
3
|
+
|
|
4
|
+
// Map friendly short names → claude CLI model aliases
|
|
5
|
+
const MODEL_ALIASES = {
|
|
6
|
+
'sonnet-4.5': 'claude-sonnet-4-5',
|
|
7
|
+
'sonnet-4': 'claude-sonnet-4',
|
|
8
|
+
'opus-4.5': 'claude-opus-4-5',
|
|
9
|
+
'opus-4': 'claude-opus-4',
|
|
10
|
+
'haiku-3.5': 'claude-haiku-3-5',
|
|
11
|
+
'haiku-3': 'claude-haiku-3',
|
|
12
|
+
// short aliases the claude CLI itself accepts
|
|
13
|
+
'sonnet': 'sonnet',
|
|
14
|
+
'opus': 'opus',
|
|
15
|
+
'haiku': 'haiku',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function resolveModel(model) {
|
|
19
|
+
return MODEL_ALIASES[model] ?? model;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function callClaude(prompt, model) {
|
|
23
|
+
const resolvedModel = resolveModel(model);
|
|
24
|
+
|
|
25
|
+
// Full prompt = system prompt + user prompt, joined clearly
|
|
26
|
+
const fullPrompt = `${SYSTEM_PROMPT}\n\n---\n\n${prompt}`;
|
|
27
|
+
|
|
28
|
+
let output;
|
|
29
|
+
try {
|
|
30
|
+
output = execFileSync(
|
|
31
|
+
'claude',
|
|
32
|
+
[
|
|
33
|
+
'--print', // non-interactive, print and exit
|
|
34
|
+
'--output-format', 'text', // plain text output
|
|
35
|
+
'--model', resolvedModel,
|
|
36
|
+
'--no-session-persistence', // don't pollute session history
|
|
37
|
+
fullPrompt,
|
|
38
|
+
],
|
|
39
|
+
{
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
maxBuffer: 50 * 1024 * 1024, // 50 MB
|
|
42
|
+
timeout: 120_000, // 2 min timeout
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.code === 'ENOENT') {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'`claude` command not found. Install Claude Code: https://claude.ai/code'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (err.signal === 'SIGTERM') {
|
|
52
|
+
throw new Error('Claude timed out after 2 minutes. Try reducing the diff size.');
|
|
53
|
+
}
|
|
54
|
+
const stderr = err.stderr?.trim();
|
|
55
|
+
throw new Error(`claude CLI error: ${stderr || err.message}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return output.trim();
|
|
59
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
import { loadConfig, saveConfig, showAndEditConfig } from './config.js';
|
|
8
|
+
import { isGitRepo, getCurrentBranch, getDiff, getStagedDiff, getPRNumber } from './git.js';
|
|
9
|
+
import { buildReviewPrompt } from './prompt.js';
|
|
10
|
+
import { callAI } from './ai.js';
|
|
11
|
+
import { saveReport, addMetadata } from './file.js';
|
|
12
|
+
import { PROVIDERS, PROVIDER_LIST } from './providers/index.js';
|
|
13
|
+
|
|
14
|
+
// ─── Argument parsing ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
|
|
18
|
+
const flags = {
|
|
19
|
+
staged: args.includes('--staged'),
|
|
20
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
21
|
+
version: args.includes('--version') || args.includes('-v'),
|
|
22
|
+
config: args[0] === 'config',
|
|
23
|
+
review: args[0] === 'review', // explicit subcommand (optional, same as default)
|
|
24
|
+
provider: args.find((a) => a.startsWith('--provider='))?.split('=')[1] ?? null,
|
|
25
|
+
model: args.find((a) => a.startsWith('--model='))?.split('=')[1] ?? null,
|
|
26
|
+
focus: args.find((a) => a.startsWith('--focus='))?.split('=')[1] ?? null,
|
|
27
|
+
output: args.find((a) => a.startsWith('--output='))?.split('=')[1] ?? null,
|
|
28
|
+
base: args.find((a) => a.startsWith('--base='))?.split('=')[1] ?? null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ─── Subcommand routing ───────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
if (flags.version) {
|
|
34
|
+
const { createRequire } = await import('module');
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const pkg = require('../package.json');
|
|
37
|
+
console.log(`pr-review v${pkg.version}`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (flags.help) {
|
|
42
|
+
printHelp();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (flags.config) {
|
|
47
|
+
await showAndEditConfig(chalk);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Main flow ────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
await main();
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
console.log(chalk.bold.cyan('\n┌─────────────────────────────────────┐'));
|
|
57
|
+
console.log(chalk.bold.cyan(' 🔍 pr-review — AI Code Review '));
|
|
58
|
+
console.log(chalk.bold.cyan('└─────────────────────────────────────┘\n'));
|
|
59
|
+
|
|
60
|
+
if (!isGitRepo()) {
|
|
61
|
+
fatal('Not inside a git repository. Run pr-review from your project root.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const config = loadConfig();
|
|
65
|
+
console.log(chalk.dim(' Config: ~/.pr-review/config.json\n'));
|
|
66
|
+
|
|
67
|
+
// ── Select AI provider ─────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const selectedProviderKey = flags.provider
|
|
70
|
+
? flags.provider
|
|
71
|
+
: await selectProvider(config.defaultProvider ?? 'claude');
|
|
72
|
+
|
|
73
|
+
const provider = PROVIDERS[selectedProviderKey];
|
|
74
|
+
if (!provider) fatal(`Unknown provider: "${selectedProviderKey}". Valid: ${Object.keys(PROVIDERS).join(', ')}`);
|
|
75
|
+
|
|
76
|
+
// ── Select model ───────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const defaultModelForProvider =
|
|
79
|
+
config.defaultModels?.[selectedProviderKey] ?? provider.defaultModel;
|
|
80
|
+
|
|
81
|
+
const selectedModel = flags.model
|
|
82
|
+
? flags.model
|
|
83
|
+
: await selectModel(provider, defaultModelForProvider);
|
|
84
|
+
|
|
85
|
+
// Persist last-used provider + model per provider
|
|
86
|
+
const updatedModels = { ...(config.defaultModels ?? {}), [selectedProviderKey]: selectedModel };
|
|
87
|
+
saveConfig({ ...config, defaultProvider: selectedProviderKey, defaultModels: updatedModels });
|
|
88
|
+
|
|
89
|
+
console.log('');
|
|
90
|
+
|
|
91
|
+
// ── Select branches ────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
let sourceBranch, destBranch;
|
|
94
|
+
const currentBranch = getCurrentBranch();
|
|
95
|
+
|
|
96
|
+
if (flags.staged) {
|
|
97
|
+
sourceBranch = '(staged changes)';
|
|
98
|
+
destBranch = '(index)';
|
|
99
|
+
} else {
|
|
100
|
+
sourceBranch = await prompt(
|
|
101
|
+
` Source branch ${chalk.dim(`(default: ${chalk.yellow(currentBranch)})`)}: `,
|
|
102
|
+
currentBranch
|
|
103
|
+
);
|
|
104
|
+
destBranch = flags.base ?? await prompt(
|
|
105
|
+
` Destination branch ${chalk.dim(`(default: ${chalk.yellow(config.defaultBaseBranch)})`)}: `,
|
|
106
|
+
config.defaultBaseBranch
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Detect PR number → determine output filename ───────────────────────────
|
|
111
|
+
|
|
112
|
+
const prNumber = getPRNumber();
|
|
113
|
+
const outputFile = flags.output
|
|
114
|
+
?? (prNumber ? `pr-${prNumber}-review.md` : config.outputFile);
|
|
115
|
+
|
|
116
|
+
if (prNumber) {
|
|
117
|
+
console.log(chalk.dim(`\n 🔗 PR #${prNumber} detected → output: ${chalk.yellow(outputFile)}`));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
// ── Fetch diff ─────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
let diff;
|
|
125
|
+
const spinner1 = ora({ text: 'Fetching git diff…', color: 'cyan' }).start();
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
diff = flags.staged
|
|
129
|
+
? getStagedDiff(config.ignoreFiles)
|
|
130
|
+
: getDiff(sourceBranch, destBranch, config.ignoreFiles);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
spinner1.fail(chalk.red(`Diff failed: ${err.message}`));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!diff || diff.trim().length === 0) {
|
|
137
|
+
spinner1.warn(chalk.yellow('No changes found between the selected branches.'));
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let truncated = false;
|
|
142
|
+
if (diff.length > config.maxDiffLength) {
|
|
143
|
+
diff = diff.slice(0, config.maxDiffLength) + '\n\n[DIFF TRUNCATED — exceeded maxDiffLength]\n';
|
|
144
|
+
truncated = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
spinner1.succeed(
|
|
148
|
+
chalk.green('Diff fetched') +
|
|
149
|
+
chalk.dim(` — ${diff.length.toLocaleString()} chars${truncated ? ' (truncated)' : ''}`)
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// ── Build prompt ───────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
const reviewPrompt = buildReviewPrompt(sourceBranch, destBranch, diff, flags.focus);
|
|
155
|
+
|
|
156
|
+
// ── Call AI ────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
const spinner2 = ora({
|
|
159
|
+
text: `Calling ${provider.name} (${selectedModel})…`,
|
|
160
|
+
color: 'cyan',
|
|
161
|
+
}).start();
|
|
162
|
+
|
|
163
|
+
let reviewText;
|
|
164
|
+
try {
|
|
165
|
+
reviewText = await callAI(reviewPrompt, selectedProviderKey, selectedModel);
|
|
166
|
+
spinner2.succeed(chalk.green(`${provider.name} review received`));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
spinner2.fail(chalk.red(`${provider.name} error: ${err.message}`));
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Write report ───────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
const spinner3 = ora({ text: 'Writing report…', color: 'cyan' }).start();
|
|
175
|
+
let outputPath;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const finalContent = addMetadata(reviewText, {
|
|
179
|
+
sourceBranch,
|
|
180
|
+
destBranch,
|
|
181
|
+
provider: provider.name,
|
|
182
|
+
model: selectedModel,
|
|
183
|
+
prNumber,
|
|
184
|
+
diffLength: diff.length,
|
|
185
|
+
generatedAt: new Date().toISOString(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
outputPath = saveReport(finalContent, outputFile);
|
|
189
|
+
spinner3.succeed(chalk.green('Report saved') + chalk.dim(` → ${outputPath}`));
|
|
190
|
+
} catch (err) {
|
|
191
|
+
spinner3.fail(chalk.red(`Failed to write report: ${err.message}`));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Done ───────────────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(chalk.bold.green('✅ PR Review complete!'));
|
|
199
|
+
if (prNumber) {
|
|
200
|
+
console.log(chalk.dim(` PR #${prNumber} · ${provider.name} · ${selectedModel}`));
|
|
201
|
+
}
|
|
202
|
+
console.log(chalk.dim(` Open ${outputPath} to read the review.\n`));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Provider selection ───────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
async function selectProvider(defaultProvider) {
|
|
208
|
+
const defaultIndex = PROVIDER_LIST.findIndex((p) => p.key === defaultProvider);
|
|
209
|
+
const safeDefault = defaultIndex >= 0 ? defaultIndex : 0;
|
|
210
|
+
|
|
211
|
+
const items = PROVIDER_LIST.map((p) => ({ label: p.label, description: p.description }));
|
|
212
|
+
const idx = await arrowSelect('Select AI provider:', items, safeDefault);
|
|
213
|
+
|
|
214
|
+
const chosen = PROVIDER_LIST[idx];
|
|
215
|
+
console.log(chalk.dim(` → ${chosen.label}\n`));
|
|
216
|
+
return chosen.key;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── Model selection ──────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
async function selectModel(provider, defaultModel) {
|
|
222
|
+
const defaultIndex = provider.models.findIndex((m) => m.id === defaultModel);
|
|
223
|
+
const safeDefault = defaultIndex >= 0 ? defaultIndex : 0;
|
|
224
|
+
|
|
225
|
+
const items = provider.models.map((m) => ({ label: m.label }));
|
|
226
|
+
const idx = await arrowSelect(`Select ${provider.name} model:`, items, safeDefault);
|
|
227
|
+
|
|
228
|
+
const chosen = provider.models[idx];
|
|
229
|
+
console.log(chalk.dim(` → ${chosen.id}\n`));
|
|
230
|
+
return chosen.id;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── Arrow-key list selector ──────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
function arrowSelect(title, items, defaultIdx = 0) {
|
|
236
|
+
console.log(chalk.bold(` ${title}\n`));
|
|
237
|
+
|
|
238
|
+
// Fallback to plain prompt when stdin is not a TTY (e.g. piped)
|
|
239
|
+
if (!process.stdin.isTTY) {
|
|
240
|
+
items.forEach((item, i) => {
|
|
241
|
+
const marker = i === defaultIdx ? chalk.cyan('❯') : ' ';
|
|
242
|
+
const label = chalk.dim(item.label) + (item.description ? chalk.dim(` — ${item.description}`) : '');
|
|
243
|
+
console.log(` ${marker} ${label}`);
|
|
244
|
+
});
|
|
245
|
+
return Promise.resolve(defaultIdx);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let idx = defaultIdx;
|
|
249
|
+
|
|
250
|
+
const render = (first) => {
|
|
251
|
+
if (!first) process.stdout.write(`\x1b[${items.length}A`);
|
|
252
|
+
for (let i = 0; i < items.length; i++) {
|
|
253
|
+
const active = i === idx;
|
|
254
|
+
const cursor = active ? chalk.cyan('❯') : ' ';
|
|
255
|
+
const label = active ? chalk.bold.white(items[i].label) : chalk.dim(items[i].label);
|
|
256
|
+
const desc = items[i].description ? chalk.dim(` — ${items[i].description}`) : '';
|
|
257
|
+
process.stdout.write(` ${cursor} ${label}${desc}\x1b[K\n`);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
render(true);
|
|
262
|
+
|
|
263
|
+
return new Promise((resolve) => {
|
|
264
|
+
process.stdin.setRawMode(true);
|
|
265
|
+
process.stdin.resume();
|
|
266
|
+
process.stdin.setEncoding('utf8');
|
|
267
|
+
|
|
268
|
+
const onData = (key) => {
|
|
269
|
+
if (key === '\u0003') { // Ctrl-C
|
|
270
|
+
process.stdin.setRawMode(false);
|
|
271
|
+
process.exit(0);
|
|
272
|
+
} else if (key === '\u001b[A' || key === 'k') { // Up / k
|
|
273
|
+
idx = (idx - 1 + items.length) % items.length;
|
|
274
|
+
render(false);
|
|
275
|
+
} else if (key === '\u001b[B' || key === 'j') { // Down / j
|
|
276
|
+
idx = (idx + 1) % items.length;
|
|
277
|
+
render(false);
|
|
278
|
+
} else if (key === '\r' || key === '\n') { // Enter
|
|
279
|
+
process.stdin.setRawMode(false);
|
|
280
|
+
process.stdin.removeListener('data', onData);
|
|
281
|
+
process.stdin.pause();
|
|
282
|
+
process.stdout.write('\n');
|
|
283
|
+
resolve(idx);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
process.stdin.on('data', onData);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function prompt(question, defaultValue = '') {
|
|
294
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
295
|
+
return new Promise((resolve) => {
|
|
296
|
+
rl.question(question, (answer) => {
|
|
297
|
+
rl.close();
|
|
298
|
+
resolve(answer.trim() || defaultValue);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function fatal(message) {
|
|
304
|
+
console.error('\n' + chalk.red('✖ ') + message + '\n');
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function printHelp() {
|
|
309
|
+
console.log(`
|
|
310
|
+
${chalk.bold.cyan('pr-review')} — AI-powered Pull Request reviews
|
|
311
|
+
|
|
312
|
+
${chalk.bold('USAGE')}
|
|
313
|
+
${chalk.cyan('pr-review')} Interactive PR review
|
|
314
|
+
${chalk.cyan('pr-review review')} Explicit review subcommand (same as above)
|
|
315
|
+
${chalk.cyan('pr-review config')} View and edit global config
|
|
316
|
+
${chalk.cyan('pr-review review --staged')} Review staged (uncommitted) changes
|
|
317
|
+
${chalk.cyan('pr-review review --provider=<provider>')} Skip provider prompt
|
|
318
|
+
${chalk.cyan('pr-review review --model=<model>')} Skip model prompt
|
|
319
|
+
${chalk.cyan('pr-review review --base=<branch>')} Override destination/base branch
|
|
320
|
+
${chalk.cyan('pr-review review --focus=<area>')} Focus: security, performance, etc.
|
|
321
|
+
${chalk.cyan('pr-review review --output=<file>')} Override output file name
|
|
322
|
+
${chalk.cyan('pr-review --help')} Show this help
|
|
323
|
+
${chalk.cyan('pr-review --version')} Show version
|
|
324
|
+
|
|
325
|
+
${chalk.bold('PROVIDERS')}
|
|
326
|
+
${chalk.cyan('claude')} claude CLI — claude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5
|
|
327
|
+
${chalk.cyan('copilot')} copilot CLI — claude-sonnet-4.6, gpt-5.4, claude-opus-4.6, gpt-5.4-mini
|
|
328
|
+
${chalk.cyan('codex')} codex CLI — gpt-5.4, o3, o4-mini, gpt-4.1
|
|
329
|
+
${chalk.cyan('gemini')} gemini CLI — flash, pro, flash-lite
|
|
330
|
+
${chalk.cyan('cursor')} agent CLI — install Cursor + add \`agent\` to PATH
|
|
331
|
+
|
|
332
|
+
${chalk.bold('PR NUMBER')}
|
|
333
|
+
If a GitHub PR is open for the current branch (\`gh pr view\`),
|
|
334
|
+
the output file is automatically named ${chalk.yellow('pr-<number>-review.md')}.
|
|
335
|
+
|
|
336
|
+
${chalk.bold('CONFIG')}
|
|
337
|
+
${chalk.dim('~/.pr-review/config.json')} Auto-created on first run
|
|
338
|
+
Last-used provider and model per provider are remembered.
|
|
339
|
+
|
|
340
|
+
${chalk.bold('EXAMPLES')}
|
|
341
|
+
${chalk.dim('# Standard interactive review')}
|
|
342
|
+
pr-review
|
|
343
|
+
|
|
344
|
+
${chalk.dim('# Explicit review subcommand')}
|
|
345
|
+
pr-review review
|
|
346
|
+
|
|
347
|
+
${chalk.dim('# Skip all prompts')}
|
|
348
|
+
pr-review review --provider=copilot --model=gpt-5.4 --base=develop
|
|
349
|
+
|
|
350
|
+
${chalk.dim('# Security-focused review of staged changes')}
|
|
351
|
+
pr-review review --staged --focus=security
|
|
352
|
+
|
|
353
|
+
${chalk.dim('# Most capable model with custom output file')}
|
|
354
|
+
pr-review review --provider=claude --model=claude-opus-4-6 --output=deep-review.md
|
|
355
|
+
`);
|
|
356
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
|
|
6
|
+
export const CONFIG_DIR = path.join(os.homedir(), '.pr-review');
|
|
7
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_CONFIG = {
|
|
10
|
+
defaultProvider: 'claude',
|
|
11
|
+
defaultModels: {
|
|
12
|
+
claude: 'claude-sonnet-4-5',
|
|
13
|
+
copilot: 'default',
|
|
14
|
+
codex: 'default',
|
|
15
|
+
gemini: 'flash',
|
|
16
|
+
cursor: 'default',
|
|
17
|
+
},
|
|
18
|
+
defaultBaseBranch: 'main',
|
|
19
|
+
maxDiffLength: 100000,
|
|
20
|
+
ignoreFiles: ['package-lock.json', 'yarn.lock'],
|
|
21
|
+
outputFile: 'pr-review-review.md',
|
|
22
|
+
strictMode: true,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function ensureConfigDir() {
|
|
26
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
27
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function loadConfig() {
|
|
32
|
+
ensureConfigDir();
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
35
|
+
saveConfig(DEFAULT_CONFIG);
|
|
36
|
+
return { ...DEFAULT_CONFIG };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
// Merge with defaults so new keys are always present
|
|
43
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
44
|
+
} catch {
|
|
45
|
+
return { ...DEFAULT_CONFIG };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function saveConfig(config) {
|
|
50
|
+
ensureConfigDir();
|
|
51
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getConfigPath() {
|
|
55
|
+
return CONFIG_FILE;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ask(rl, question, defaultValue) {
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
rl.question(` ${question} [${defaultValue}]: `, (answer) => {
|
|
61
|
+
const trimmed = answer.trim();
|
|
62
|
+
resolve(trimmed === '' ? String(defaultValue) : trimmed);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function showAndEditConfig(chalk) {
|
|
68
|
+
const config = loadConfig();
|
|
69
|
+
|
|
70
|
+
console.log(chalk.bold('\nCurrent configuration:'));
|
|
71
|
+
console.log(chalk.dim(` File: ${CONFIG_FILE}\n`));
|
|
72
|
+
console.log(JSON.stringify(config, null, 2));
|
|
73
|
+
console.log('');
|
|
74
|
+
|
|
75
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
76
|
+
|
|
77
|
+
console.log(chalk.bold('Update values') + chalk.dim(' (press Enter to keep current):\n'));
|
|
78
|
+
|
|
79
|
+
const newModel = await ask(rl, 'Default model ', config.defaultModel);
|
|
80
|
+
const newBranch = await ask(rl, 'Default base branch', config.defaultBaseBranch);
|
|
81
|
+
const newMaxDiff = await ask(rl, 'Max diff length ', config.maxDiffLength);
|
|
82
|
+
const newOutputFile = await ask(rl, 'Output file ', config.outputFile);
|
|
83
|
+
const newStrictMode = await ask(rl, 'Strict mode ', config.strictMode);
|
|
84
|
+
const newIgnore = await ask(rl, 'Ignore files (csv) ', config.ignoreFiles.join(','));
|
|
85
|
+
|
|
86
|
+
rl.close();
|
|
87
|
+
|
|
88
|
+
const updated = {
|
|
89
|
+
...config,
|
|
90
|
+
defaultModel: newModel,
|
|
91
|
+
defaultBaseBranch: newBranch,
|
|
92
|
+
maxDiffLength: parseInt(newMaxDiff, 10) || config.maxDiffLength,
|
|
93
|
+
outputFile: newOutputFile,
|
|
94
|
+
strictMode: newStrictMode === 'true',
|
|
95
|
+
ignoreFiles: newIgnore.split(',').map((f) => f.trim()).filter(Boolean),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
saveConfig(updated);
|
|
99
|
+
console.log('\n' + chalk.green('✔ Config saved.\n'));
|
|
100
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
101
|
+
console.log('');
|
|
102
|
+
}
|
package/src/file.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export function saveReport(content, outputFile) {
|
|
5
|
+
const outputPath = path.resolve(process.cwd(), outputFile);
|
|
6
|
+
fs.writeFileSync(outputPath, content, 'utf8');
|
|
7
|
+
return outputPath;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function addMetadata(reviewText, meta) {
|
|
11
|
+
const { sourceBranch, destBranch, provider, model, prNumber, diffLength, generatedAt } = meta;
|
|
12
|
+
|
|
13
|
+
const lines = [
|
|
14
|
+
'---',
|
|
15
|
+
`<!-- Generated by pr-review on ${generatedAt} -->`,
|
|
16
|
+
`<!-- Provider: ${provider} | Model: ${model} | Diff: ${diffLength} chars | ${sourceBranch} → ${destBranch} -->`,
|
|
17
|
+
];
|
|
18
|
+
if (prNumber) lines.push(`<!-- PR: #${prNumber} -->`);
|
|
19
|
+
lines.push('---', '');
|
|
20
|
+
|
|
21
|
+
return lines.join('\n') + reviewText;
|
|
22
|
+
}
|
package/src/git.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export function getPRNumber() {
|
|
4
|
+
try {
|
|
5
|
+
const result = execSync('gh pr view --json number --jq .number', {
|
|
6
|
+
encoding: 'utf8',
|
|
7
|
+
stdio: 'pipe',
|
|
8
|
+
}).trim();
|
|
9
|
+
const num = parseInt(result, 10);
|
|
10
|
+
return isNaN(num) ? null : num;
|
|
11
|
+
} catch {
|
|
12
|
+
return null; // No open PR, gh not installed, or not authenticated
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isGitRepo() {
|
|
17
|
+
try {
|
|
18
|
+
execSync('git rev-parse --is-inside-work-tree', { encoding: 'utf8', stdio: 'pipe' });
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getCurrentBranch() {
|
|
26
|
+
try {
|
|
27
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error('Unable to detect current branch. Is this a git repository?');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function branchExists(branch) {
|
|
34
|
+
try {
|
|
35
|
+
execSync(`git rev-parse --verify "${branch}"`, { encoding: 'utf8', stdio: 'pipe' });
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getDiff(sourceBranch, destBranch, ignoreFiles = []) {
|
|
43
|
+
if (!branchExists(sourceBranch)) {
|
|
44
|
+
throw new Error(`Branch not found: "${sourceBranch}"`);
|
|
45
|
+
}
|
|
46
|
+
if (!branchExists(destBranch)) {
|
|
47
|
+
throw new Error(`Branch not found: "${destBranch}"`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
let cmd = `git diff "${destBranch}...${sourceBranch}"`;
|
|
52
|
+
|
|
53
|
+
if (ignoreFiles.length > 0) {
|
|
54
|
+
const exclusions = ignoreFiles.map((f) => `':(exclude)${f}'`).join(' ');
|
|
55
|
+
cmd += ` -- . ${exclusions}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return execSync(cmd, {
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
throw new Error(`git diff failed: ${err.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getStagedDiff(ignoreFiles = []) {
|
|
68
|
+
try {
|
|
69
|
+
let cmd = 'git diff --staged';
|
|
70
|
+
|
|
71
|
+
if (ignoreFiles.length > 0) {
|
|
72
|
+
const exclusions = ignoreFiles.map((f) => `':(exclude)${f}'`).join(' ');
|
|
73
|
+
cmd += ` -- . ${exclusions}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return execSync(cmd, {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
79
|
+
});
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new Error(`git diff --staged failed: ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const SYSTEM_PROMPT = `You are a principal software engineer performing a rigorous pull request review.
|
|
2
|
+
|
|
3
|
+
Your reviews are:
|
|
4
|
+
- Precise, technical, and evidence-based — cite specific file paths and line context from the diff
|
|
5
|
+
- Comprehensive — cover correctness, security, performance, maintainability, and test coverage
|
|
6
|
+
- Actionable — every issue must include a clear recommendation
|
|
7
|
+
- Honest — if code is well-written, say so; do not invent issues
|
|
8
|
+
|
|
9
|
+
You output reviews in strict Markdown with exactly the sections specified. No preamble, no postamble.`;
|
|
10
|
+
|
|
11
|
+
export function buildReviewPrompt(sourceBranch, destBranch, diff, focus = null) {
|
|
12
|
+
const focusInstruction = focus
|
|
13
|
+
? `\n> **Special focus requested:** Prioritize **${focus}**-related issues throughout the review.\n`
|
|
14
|
+
: '';
|
|
15
|
+
|
|
16
|
+
return `You are reviewing a Pull Request merging \`${sourceBranch}\` into \`${destBranch}\`.
|
|
17
|
+
${focusInstruction}
|
|
18
|
+
Analyze the git diff below and produce a structured code review in the exact Markdown format specified.
|
|
19
|
+
|
|
20
|
+
<git_diff>
|
|
21
|
+
\`\`\`diff
|
|
22
|
+
${diff}
|
|
23
|
+
\`\`\`
|
|
24
|
+
</git_diff>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
Respond using **exactly** this structure:
|
|
29
|
+
|
|
30
|
+
# PR Review: \`${sourceBranch}\` → \`${destBranch}\`
|
|
31
|
+
|
|
32
|
+
## Summary
|
|
33
|
+
_2–4 sentences describing what this PR does, its scope, and overall quality._
|
|
34
|
+
|
|
35
|
+
## Critical Issues
|
|
36
|
+
_Bugs, breaking changes, data-loss risks, or anything that MUST be fixed before merge._
|
|
37
|
+
_Format each item as:_ **[CRITICAL]** \`path/to/file.ext\` — Description and fix recommendation.
|
|
38
|
+
_If none: "No critical issues found."_
|
|
39
|
+
|
|
40
|
+
## Security
|
|
41
|
+
_Auth bypasses, injection risks, secret exposure, insecure defaults, etc._
|
|
42
|
+
_Format each item as:_ **[HIGH|MEDIUM|LOW]** \`path/to/file.ext\` — Description and fix recommendation.
|
|
43
|
+
_If none: "No security issues found."_
|
|
44
|
+
|
|
45
|
+
## Performance
|
|
46
|
+
_Algorithmic complexity, unnecessary re-renders, N+1 queries, blocking I/O, memory leaks._
|
|
47
|
+
_Format each item as:_ **[HIGH|MEDIUM|LOW]** \`path/to/file.ext\` — Description and fix recommendation.
|
|
48
|
+
_If none: "No performance issues found."_
|
|
49
|
+
|
|
50
|
+
## Code Quality & Improvements
|
|
51
|
+
_Anti-patterns, duplication, poor naming, overly complex logic, missing error handling._
|
|
52
|
+
- Description with specific recommendation.
|
|
53
|
+
_If none: "No code quality issues found."_
|
|
54
|
+
|
|
55
|
+
## Test Coverage
|
|
56
|
+
_Missing unit tests, edge cases not covered, untested error paths._
|
|
57
|
+
- Description of what should be tested.
|
|
58
|
+
_If none: "Test coverage appears adequate."_
|
|
59
|
+
|
|
60
|
+
## Suggestions
|
|
61
|
+
_Non-blocking improvements, best practices, minor style notes._
|
|
62
|
+
- Suggestion.
|
|
63
|
+
|
|
64
|
+
## Final Verdict
|
|
65
|
+
**[APPROVE | REQUEST CHANGES | NEEDS DISCUSSION]**
|
|
66
|
+
|
|
67
|
+
_2–3 sentence justification for your verdict._`;
|
|
68
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { SYSTEM_PROMPT } from '../prompt.js';
|
|
3
|
+
|
|
4
|
+
export async function callClaude(prompt, model) {
|
|
5
|
+
const fullPrompt = `${SYSTEM_PROMPT}\n\n---\n\n${prompt}`;
|
|
6
|
+
let output;
|
|
7
|
+
try {
|
|
8
|
+
output = execFileSync(
|
|
9
|
+
'claude',
|
|
10
|
+
['--print', '--output-format', 'text', '--model', model, '--no-session-persistence', fullPrompt],
|
|
11
|
+
{ encoding: 'utf8', maxBuffer: 50 * 1024 * 1024, timeout: 120_000 }
|
|
12
|
+
);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (err.code === 'ENOENT') throw new Error('`claude` not found. Install Claude Code: https://claude.ai/code');
|
|
15
|
+
if (err.signal === 'SIGTERM') throw new Error('Claude timed out after 2 minutes. Try a smaller diff.');
|
|
16
|
+
const detail = err.stderr?.trim() || err.stdout?.trim() || '';
|
|
17
|
+
if (/auth|token|expired|401|login/i.test(detail)) {
|
|
18
|
+
throw new Error('Claude authentication expired. Run `claude` to re-authenticate, then try again.');
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`claude CLI error: ${detail || 'unknown error (run `claude` to check status)'}`);
|
|
21
|
+
}
|
|
22
|
+
return output.trim();
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { SYSTEM_PROMPT } from '../prompt.js';
|
|
3
|
+
|
|
4
|
+
export async function callCodex(prompt, model) {
|
|
5
|
+
const fullPrompt = `${SYSTEM_PROMPT}\n\n---\n\n${prompt}`;
|
|
6
|
+
|
|
7
|
+
// -q quiet / non-interactive mode (no TUI, outputs response to stdout)
|
|
8
|
+
// --approval-mode suggest read-only agent: proposes but never auto-executes
|
|
9
|
+
const cliArgs = ['-q', '--approval-mode', 'suggest'];
|
|
10
|
+
|
|
11
|
+
if (model && model !== 'default') {
|
|
12
|
+
cliArgs.push('--model', model);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
cliArgs.push(fullPrompt);
|
|
16
|
+
|
|
17
|
+
let output;
|
|
18
|
+
try {
|
|
19
|
+
output = execFileSync('codex', cliArgs, {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
22
|
+
timeout: 180_000, // 3 min — codex reasoning models can be slow
|
|
23
|
+
});
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err.code === 'ENOENT') {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'`codex` not found. Install: npm install -g @openai/codex\n' +
|
|
28
|
+
' Then run: codex (and sign in on first launch)'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (err.signal === 'SIGTERM') {
|
|
32
|
+
throw new Error('Codex timed out after 3 minutes. Try reducing the diff size.');
|
|
33
|
+
}
|
|
34
|
+
const stderr = err.stderr?.trim();
|
|
35
|
+
throw new Error(`codex CLI error: ${stderr || err.message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return output.trim();
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { SYSTEM_PROMPT } from '../prompt.js';
|
|
3
|
+
|
|
4
|
+
export async function callCopilot(prompt, model) {
|
|
5
|
+
const fullPrompt = `${SYSTEM_PROMPT}\n\n---\n\n${prompt}`;
|
|
6
|
+
|
|
7
|
+
// -p non-interactive (exits after response)
|
|
8
|
+
// -s output only the agent response (no stats/spinners) — perfect for scripting
|
|
9
|
+
const cliArgs = ['-p', fullPrompt, '-s'];
|
|
10
|
+
|
|
11
|
+
if (model && model !== 'default') {
|
|
12
|
+
cliArgs.push('--model', model);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let output;
|
|
16
|
+
try {
|
|
17
|
+
output = execFileSync('copilot', cliArgs, {
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
20
|
+
timeout: 120_000,
|
|
21
|
+
});
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (err.code === 'ENOENT') {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'`copilot` not found. Install: npm install -g @github/copilot\n' +
|
|
26
|
+
' Then run: copilot /login'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (err.signal === 'SIGTERM') {
|
|
30
|
+
throw new Error('Copilot CLI timed out after 2 minutes. Try reducing the diff size.');
|
|
31
|
+
}
|
|
32
|
+
const stderr = err.stderr?.trim();
|
|
33
|
+
throw new Error(`copilot CLI error: ${stderr || err.message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return output.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { SYSTEM_PROMPT } from '../prompt.js';
|
|
3
|
+
|
|
4
|
+
export async function callCursor(prompt, model) {
|
|
5
|
+
const fullPrompt = `${SYSTEM_PROMPT}\n\n---\n\n${prompt}`;
|
|
6
|
+
|
|
7
|
+
// -p / --print non-interactive, prints response to stdout
|
|
8
|
+
// --mode=ask read-only exploration — agent won't edit any files
|
|
9
|
+
// --output-format text plain text (not JSON)
|
|
10
|
+
const cliArgs = ['-p', fullPrompt, '--mode=ask', '--output-format', 'text'];
|
|
11
|
+
|
|
12
|
+
if (model && model !== 'default') {
|
|
13
|
+
cliArgs.push('--model', model);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let output;
|
|
17
|
+
try {
|
|
18
|
+
output = execFileSync('agent', cliArgs, {
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
21
|
+
timeout: 120_000,
|
|
22
|
+
});
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err.code === 'ENOENT') {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'`agent` not found. Install Cursor and ensure the CLI is in your PATH.\n' +
|
|
27
|
+
' macOS: open Cursor → Cmd+Shift+P → "Install cursor/agent in PATH"'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (err.signal === 'SIGTERM') {
|
|
31
|
+
throw new Error('Cursor agent timed out after 2 minutes. Try reducing the diff size.');
|
|
32
|
+
}
|
|
33
|
+
const stderr = err.stderr?.trim();
|
|
34
|
+
throw new Error(`agent CLI error: ${stderr || err.message}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return output.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { SYSTEM_PROMPT } from '../prompt.js';
|
|
3
|
+
|
|
4
|
+
export async function callGemini(prompt, model) {
|
|
5
|
+
const fullPrompt = `${SYSTEM_PROMPT}\n\n---\n\n${prompt}`;
|
|
6
|
+
|
|
7
|
+
// -p non-interactive (exits after response)
|
|
8
|
+
// -o text plain text output (default, but explicit is safer)
|
|
9
|
+
const cliArgs = ['-p', fullPrompt, '-o', 'text'];
|
|
10
|
+
|
|
11
|
+
if (model && model !== 'default') {
|
|
12
|
+
cliArgs.push('-m', model);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let output;
|
|
16
|
+
try {
|
|
17
|
+
output = execFileSync('gemini', cliArgs, {
|
|
18
|
+
encoding: 'utf8',
|
|
19
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
20
|
+
timeout: 120_000,
|
|
21
|
+
});
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (err.code === 'ENOENT') {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'`gemini` not found. Install: npm install -g @google/gemini-cli\n' +
|
|
26
|
+
' Then run: gemini (and authenticate on first launch)'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (err.signal === 'SIGTERM') {
|
|
30
|
+
throw new Error('Gemini CLI timed out after 2 minutes. Try reducing the diff size.');
|
|
31
|
+
}
|
|
32
|
+
const stderr = err.stderr?.trim();
|
|
33
|
+
throw new Error(`gemini CLI error: ${stderr || err.message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return output.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const PROVIDERS = {
|
|
2
|
+
// claude CLI: model IDs use dashes (e.g. claude-sonnet-4-6)
|
|
3
|
+
claude: {
|
|
4
|
+
key: 'claude',
|
|
5
|
+
name: 'Claude',
|
|
6
|
+
label: 'Claude (Anthropic)',
|
|
7
|
+
description: 'Uses local claude CLI — no API key needed',
|
|
8
|
+
defaultModel: 'claude-sonnet-4-6',
|
|
9
|
+
models: [
|
|
10
|
+
{ id: 'claude-sonnet-4-6', label: 'claude-sonnet-4-6 · Recommended' },
|
|
11
|
+
{ id: 'claude-opus-4-6', label: 'claude-opus-4-6 · Most capable' },
|
|
12
|
+
{ id: 'claude-haiku-4-5', label: 'claude-haiku-4-5 · Fastest' },
|
|
13
|
+
{ id: 'claude-sonnet-4-5', label: 'claude-sonnet-4-5' },
|
|
14
|
+
{ id: 'claude-sonnet-4', label: 'claude-sonnet-4' },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
// copilot CLI: model IDs use periods (e.g. claude-sonnet-4.6, gpt-5.4)
|
|
18
|
+
copilot: {
|
|
19
|
+
key: 'copilot',
|
|
20
|
+
name: 'GitHub Copilot',
|
|
21
|
+
label: 'GitHub Copilot',
|
|
22
|
+
description: 'Uses copilot CLI — run `copilot /login` first',
|
|
23
|
+
defaultModel: 'claude-sonnet-4.6',
|
|
24
|
+
models: [
|
|
25
|
+
{ id: 'claude-sonnet-4.6', label: 'claude-sonnet-4.6 · Recommended' },
|
|
26
|
+
{ id: 'claude-opus-4.6', label: 'claude-opus-4.6 · Most capable' },
|
|
27
|
+
{ id: 'gpt-5.4', label: 'gpt-5.4 · Standard' },
|
|
28
|
+
{ id: 'gpt-5.2', label: 'gpt-5.2 · Standard' },
|
|
29
|
+
{ id: 'claude-haiku-4.5', label: 'claude-haiku-4.5 · Fast/cheap' },
|
|
30
|
+
{ id: 'gpt-5.4-mini', label: 'gpt-5.4-mini · Fast/cheap' },
|
|
31
|
+
{ id: 'gpt-4.1', label: 'gpt-4.1 · Fast/cheap' },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
// codex CLI: model IDs from ~/.codex/config.toml and codex help examples
|
|
35
|
+
codex: {
|
|
36
|
+
key: 'codex',
|
|
37
|
+
name: 'Codex',
|
|
38
|
+
label: 'OpenAI Codex',
|
|
39
|
+
description: 'Uses codex CLI — run `codex` once to sign in',
|
|
40
|
+
defaultModel: 'gpt-5.4',
|
|
41
|
+
models: [
|
|
42
|
+
{ id: 'gpt-5.4', label: 'gpt-5.4 · Recommended' },
|
|
43
|
+
{ id: 'o3', label: 'o3 · Reasoning' },
|
|
44
|
+
{ id: 'o4-mini', label: 'o4-mini · Fast reasoning' },
|
|
45
|
+
{ id: 'gpt-4.1', label: 'gpt-4.1 · Cheaper' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
// gemini CLI: uses short aliases (flash, pro, flash-lite, auto)
|
|
49
|
+
gemini: {
|
|
50
|
+
key: 'gemini',
|
|
51
|
+
name: 'Gemini',
|
|
52
|
+
label: 'Google Gemini',
|
|
53
|
+
description: 'Uses gemini CLI — run `gemini` once to authenticate',
|
|
54
|
+
defaultModel: 'flash',
|
|
55
|
+
models: [
|
|
56
|
+
{ id: 'flash', label: 'flash · gemini-2.5-flash (Recommended)' },
|
|
57
|
+
{ id: 'pro', label: 'pro · gemini-2.5-pro (Most capable)' },
|
|
58
|
+
{ id: 'flash-lite', label: 'flash-lite · gemini-2.5-flash-lite (Fastest)' },
|
|
59
|
+
{ id: 'auto', label: 'auto · Auto-select best available' },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
// cursor agent CLI: model IDs (install Cursor + add agent binary to PATH)
|
|
63
|
+
cursor: {
|
|
64
|
+
key: 'cursor',
|
|
65
|
+
name: 'Cursor',
|
|
66
|
+
label: 'Cursor',
|
|
67
|
+
description: 'Uses agent CLI — install Cursor + add agent to PATH',
|
|
68
|
+
defaultModel: 'default',
|
|
69
|
+
models: [
|
|
70
|
+
{ id: 'default', label: 'Default (your Cursor plan model)' },
|
|
71
|
+
{ id: 'claude-sonnet-4-6', label: 'claude-sonnet-4-6' },
|
|
72
|
+
{ id: 'gpt-5.4', label: 'gpt-5.4' },
|
|
73
|
+
{ id: 'gemini-2.5-pro', label: 'gemini-2.5-pro' },
|
|
74
|
+
{ id: 'o3', label: 'o3 · Reasoning' },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const PROVIDER_LIST = Object.values(PROVIDERS);
|
|
80
|
+
|