diff-hound 1.0.2
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 +221 -0
- package/bin/diff-hound.js +4 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +56 -0
- package/dist/config/index.d.ts +18 -0
- package/dist/config/index.js +102 -0
- package/dist/core/parseUnifiedDiff.d.ts +2 -0
- package/dist/core/parseUnifiedDiff.js +38 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +110 -0
- package/dist/models/index.d.ts +9 -0
- package/dist/models/index.js +19 -0
- package/dist/models/openai.d.ts +30 -0
- package/dist/models/openai.js +162 -0
- package/dist/platforms/github.d.ts +43 -0
- package/dist/platforms/github.js +157 -0
- package/dist/platforms/index.d.ts +7 -0
- package/dist/platforms/index.js +17 -0
- package/dist/types/index.d.ts +64 -0
- package/dist/types/index.js +5 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Diff Hound
|
|
2
|
+
|
|
3
|
+
Diff Hound is an automated AI-powered code review tool that posts intelligent, contextual comments directly on pull requests across supported platforms.
|
|
4
|
+
|
|
5
|
+
Supports GitHub today. GitLab and Bitbucket support are planned.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- 🧠 Automated code review using OpenAI (Upcoming: Claude, DeepSeek, CodeLlama)
|
|
12
|
+
- 💬 Posts inline or summary comments on pull requests
|
|
13
|
+
- 🔌 Plug-and-play architecture for models and platforms
|
|
14
|
+
- ⚙️ Configurable with JSON/YAML config files and CLI overrides
|
|
15
|
+
- 🛠️ Designed for CI/CD pipelines and local runs
|
|
16
|
+
- 🧐 Tracks last reviewed commit to avoid duplicate reviews
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🛠️ Installation
|
|
21
|
+
|
|
22
|
+
### Option 1: Install via npm
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g diff-hound
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Option 2: Install from source
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/runtimebug/diff-hound.git
|
|
32
|
+
cd diff-hound
|
|
33
|
+
npm install
|
|
34
|
+
npm run build
|
|
35
|
+
npm link
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 🚀 How to Use
|
|
41
|
+
|
|
42
|
+
### Step 1: Setup Environment Variables
|
|
43
|
+
|
|
44
|
+
Copy the provided `.env.example` to `.env` and fill in your credentials:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cp .env.example .env
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then modify with your keys / tokens:
|
|
51
|
+
|
|
52
|
+
```env
|
|
53
|
+
# Platform tokens
|
|
54
|
+
GITHUB_TOKEN=your_github_token # Requires 'repo' scope
|
|
55
|
+
|
|
56
|
+
# AI Model API keys
|
|
57
|
+
OPENAI_API_KEY=your_openai_key
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
> 🔐 `GITHUB_TOKEN` is used to fetch PRs and post comments – [get it here](https://github.com/settings/personal-access-tokens)
|
|
61
|
+
> 🔐 `OPENAI_API_KEY` is used to generate code reviews via GPT – [get it here](https://platform.openai.com/api-keys)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### Step 2: Create a Config File
|
|
66
|
+
|
|
67
|
+
You can define your config in `.aicodeconfig.json` or `.aicode.yml`:
|
|
68
|
+
|
|
69
|
+
#### JSON Example (`.aicodeconfig.json`)
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"provider": "openai",
|
|
74
|
+
"model": "gpt-4o", // Or any other openai model
|
|
75
|
+
"endpoint": "", // Optional: custom endpoint
|
|
76
|
+
"gitProvider": "github",
|
|
77
|
+
"repo": "your-username/your-repo",
|
|
78
|
+
"dryRun": false,
|
|
79
|
+
"verbose": false,
|
|
80
|
+
"rules": [
|
|
81
|
+
"Prefer const over let when variables are not reassigned",
|
|
82
|
+
"Avoid reassigning const variables",
|
|
83
|
+
"Add descriptive comments for complex logic",
|
|
84
|
+
"Remove unnecessary comments",
|
|
85
|
+
"Follow the DRY (Don't Repeat Yourself) principle",
|
|
86
|
+
"Use descriptive variable and function names",
|
|
87
|
+
"Handle errors appropriately",
|
|
88
|
+
"Add type annotations where necessary"
|
|
89
|
+
],
|
|
90
|
+
"ignoreFiles": ["*.md", "package-lock.json", "yarn.lock", "LICENSE", "*.log"],
|
|
91
|
+
"commentStyle": "inline",
|
|
92
|
+
"severity": "suggestion"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### YAML Example (`.aicode.yml`)
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
provider: openai
|
|
100
|
+
model: gpt-4o # Or any other openai model
|
|
101
|
+
endpoint: "" # Optional: custom endpoint
|
|
102
|
+
gitProvider: github
|
|
103
|
+
repo: your-username/your-repo
|
|
104
|
+
dryRun: false
|
|
105
|
+
verbose: false
|
|
106
|
+
commentStyle: inline
|
|
107
|
+
severity: suggestion
|
|
108
|
+
ignoreFiles:
|
|
109
|
+
- "*.md"
|
|
110
|
+
- package-lock.json
|
|
111
|
+
- yarn.lock
|
|
112
|
+
- LICENSE
|
|
113
|
+
- "*.log"
|
|
114
|
+
rules:
|
|
115
|
+
- Prefer const over let when variables are not reassigned
|
|
116
|
+
- Avoid reassigning const variables
|
|
117
|
+
- Add descriptive comments for complex logic
|
|
118
|
+
- Remove unnecessary comments
|
|
119
|
+
- Follow the DRY (Don't Repeat Yourself) principle
|
|
120
|
+
- Use descriptive variable and function names
|
|
121
|
+
- Handle errors appropriately
|
|
122
|
+
- Add type annotations where necessary
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### Step 3: Run It
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
diff-hound
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Or override config values via CLI:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
diff-hound --repo=owner/repo --provider=openai --model=gpt-4o --dry-run
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
> Add `--dry-run` to **print comments to console** instead of posting them.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### Output Example (Dry Run)
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
== Comments for PR #42: Fix input validation ==
|
|
147
|
+
|
|
148
|
+
src/index.ts:17 —
|
|
149
|
+
Prefer `const` over `let` since `userId` is not reassigned.
|
|
150
|
+
|
|
151
|
+
src/utils/parse.ts:45 —
|
|
152
|
+
Consider refactoring to reduce nesting.
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### Optional CLI Flags
|
|
158
|
+
|
|
159
|
+
| Flag | Short | Description |
|
|
160
|
+
| ------------------ | ----- | --------------------------------------- |
|
|
161
|
+
| `--provider` | `-p` | AI model provider (e.g. `openai`) |
|
|
162
|
+
| `--model` | `-m` | AI model (e.g. `gpt-4o`, `gpt-4`, etc.) |
|
|
163
|
+
| `--model-endpoint` | `-e` | Custom API endpoint for the model |
|
|
164
|
+
| `--git-provider` | `-g` | Repo platform (default: `github`) |
|
|
165
|
+
| `--repo` | `-r` | GitHub repo in format `owner/repo` |
|
|
166
|
+
| `--comment-style` | `-s` | `inline` or `summary` |
|
|
167
|
+
| `--dry-run` | `-d` | Don’t post comments, only print |
|
|
168
|
+
| `--verbose` | `-v` | Enable debug logs |
|
|
169
|
+
| `--config-path` | `-c` | Custom config file path |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 🛠️ Development
|
|
174
|
+
|
|
175
|
+
### Project Structure
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
diff-hound/
|
|
179
|
+
├── bin/ # CLI entrypoint
|
|
180
|
+
├── src/
|
|
181
|
+
│ ├── cli/ # CLI argument parsing
|
|
182
|
+
│ ├── config/ # JSON/YAML config handling
|
|
183
|
+
│ ├── core/ # Diff parsing, formatting
|
|
184
|
+
│ ├── models/ # AI model adapters
|
|
185
|
+
│ ├── platforms/ # GitHub, GitLab, etc.
|
|
186
|
+
│ └── types/ # TypeScript types
|
|
187
|
+
├── .env
|
|
188
|
+
├── README.md
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### Add Support for New AI Models
|
|
194
|
+
|
|
195
|
+
Create a new class in `src/models/` that implements the `CodeReviewModel` interface.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### Add Support for New Platforms
|
|
200
|
+
|
|
201
|
+
Create a new class in `src/platforms/` that implements the `CodeReviewPlatform` interface.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## ✅ Next Steps
|
|
206
|
+
|
|
207
|
+
🔧 Add Winston for production-grade logging
|
|
208
|
+
🌐 Implement GitLab and Bitbucket platform adapters
|
|
209
|
+
🌍 Add support for other AI model providers (e.g. Anthropic, DeepSeek...)
|
|
210
|
+
💻 Add support for running local models (e.g. Ollama, Llama.cpp, Hugging Face transformers)
|
|
211
|
+
📤 Add support for webhook triggers (e.g., GitHub Actions, GitLab CI)
|
|
212
|
+
🧪 Add unit and integration test suites (Jest or Vitest)
|
|
213
|
+
📦 Publish Docker image for CI/CD use
|
|
214
|
+
🧩 Enable plugin hooks for custom rule logic
|
|
215
|
+
🗂 Add support for reviewing diffs from local branches or patch files
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 📜 License
|
|
220
|
+
|
|
221
|
+
MIT – Use freely, contribute openly.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ReviewConfig } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Parse and validate CLI arguments
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseCli(): Partial<ReviewConfig>;
|
|
6
|
+
/**
|
|
7
|
+
* Log message if verbose mode is enabled
|
|
8
|
+
*/
|
|
9
|
+
export declare function verboseLog(options: Partial<ReviewConfig>, message: string): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseCli = parseCli;
|
|
7
|
+
exports.verboseLog = verboseLog;
|
|
8
|
+
const commander_1 = require("commander");
|
|
9
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
10
|
+
const package_json_1 = require("../../package.json");
|
|
11
|
+
const config_1 = require("../config");
|
|
12
|
+
// Load environment variables
|
|
13
|
+
dotenv_1.default.config();
|
|
14
|
+
/**
|
|
15
|
+
* Parse and validate CLI arguments
|
|
16
|
+
*/
|
|
17
|
+
function parseCli() {
|
|
18
|
+
const program = new commander_1.Command();
|
|
19
|
+
program
|
|
20
|
+
.name("diff-hound")
|
|
21
|
+
.description("AI-powered code review for GitHub, GitLab, and Bitbucket")
|
|
22
|
+
.version(package_json_1.version)
|
|
23
|
+
.option("-p, --provider <provider>", "The provider of the AI model (openai, anthropic, deepseek, groq, gemini)", config_1.DEFAULT_CONFIG.provider)
|
|
24
|
+
.option("-m, --model <model>", "The AI model (gpt-4o, claude-3-5-sonnet, deepseek, llama3, gemini-2.0-flash)", config_1.DEFAULT_CONFIG.model)
|
|
25
|
+
.option("-e, --model-endpoint <endpoint>", "The endpoint for the AI model")
|
|
26
|
+
.option("-g, --git-platform <platform>", "Platform to use (github, gitlab, bitbucket)", config_1.DEFAULT_CONFIG.gitPlatform)
|
|
27
|
+
.option("-r, --repo <owner/repo>", "Repository to review")
|
|
28
|
+
.option("-s, --comment-style <commentStyle>", "Comment style (inline, summary)", config_1.DEFAULT_CONFIG.commentStyle)
|
|
29
|
+
.option("-d, --dry-run", "Do not post comments, just print them", config_1.DEFAULT_CONFIG.dryRun)
|
|
30
|
+
.option("-v, --verbose", "Enable verbose logging", config_1.DEFAULT_CONFIG.verbose)
|
|
31
|
+
.option("-c, --config-path <path>", "Path to config file (default: .aicodeconfig.json or .aicode.yml)")
|
|
32
|
+
.parse(process.argv);
|
|
33
|
+
const options = program.opts();
|
|
34
|
+
return sanitizeCliOptions({
|
|
35
|
+
provider: options.provider,
|
|
36
|
+
model: options.model,
|
|
37
|
+
gitPlatform: options.gitPlatform,
|
|
38
|
+
repo: options.repo,
|
|
39
|
+
commentStyle: options.commentStyle,
|
|
40
|
+
dryRun: options.dryRun,
|
|
41
|
+
verbose: options.verbose,
|
|
42
|
+
endpoint: options.modelEndpoint,
|
|
43
|
+
configPath: options.configPath,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Log message if verbose mode is enabled
|
|
48
|
+
*/
|
|
49
|
+
function verboseLog(options, message) {
|
|
50
|
+
if (options.verbose) {
|
|
51
|
+
console.log(`[DEBUG] ${message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function sanitizeCliOptions(cli) {
|
|
55
|
+
return Object.fromEntries(Object.entries(cli).filter(([_, v]) => v !== undefined));
|
|
56
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReviewConfig } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Default configuration
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFAULT_CONFIG: ReviewConfig;
|
|
6
|
+
/**
|
|
7
|
+
* Load configuration from file
|
|
8
|
+
* @param configPath Optional path to config file
|
|
9
|
+
* @returns Review configuration
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadConfig(configPath?: string): Promise<ReviewConfig>;
|
|
12
|
+
/**
|
|
13
|
+
* Validates the configuration : CLI options && config file
|
|
14
|
+
* @param cliOptions CLI options from command line
|
|
15
|
+
* @param config Configuration from file
|
|
16
|
+
* @returns Updated configuration
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateConfig(cliOptions: Partial<ReviewConfig>, config: ReviewConfig): ReviewConfig;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_CONFIG = void 0;
|
|
7
|
+
exports.loadConfig = loadConfig;
|
|
8
|
+
exports.validateConfig = validateConfig;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
12
|
+
/**
|
|
13
|
+
* Default configuration
|
|
14
|
+
*/
|
|
15
|
+
exports.DEFAULT_CONFIG = {
|
|
16
|
+
provider: "openai",
|
|
17
|
+
model: "gpt-4o",
|
|
18
|
+
gitPlatform: "github",
|
|
19
|
+
commentStyle: "inline",
|
|
20
|
+
dryRun: false,
|
|
21
|
+
verbose: false,
|
|
22
|
+
severity: "suggestion",
|
|
23
|
+
ignoreFiles: [],
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Load configuration from file
|
|
27
|
+
* @param configPath Optional path to config file
|
|
28
|
+
* @returns Review configuration
|
|
29
|
+
*/
|
|
30
|
+
async function loadConfig(configPath) {
|
|
31
|
+
// If config path is specified, use it
|
|
32
|
+
if (configPath && fs_1.default.existsSync(configPath)) {
|
|
33
|
+
return loadConfigFromFile(configPath);
|
|
34
|
+
}
|
|
35
|
+
// Otherwise, look for default config files
|
|
36
|
+
const jsonConfigPath = path_1.default.resolve(process.cwd(), ".aicodeconfig.json");
|
|
37
|
+
const yamlConfigPath = path_1.default.resolve(process.cwd(), ".aicode.yml");
|
|
38
|
+
if (fs_1.default.existsSync(jsonConfigPath)) {
|
|
39
|
+
return loadConfigFromFile(jsonConfigPath);
|
|
40
|
+
}
|
|
41
|
+
if (fs_1.default.existsSync(yamlConfigPath)) {
|
|
42
|
+
return loadConfigFromFile(yamlConfigPath);
|
|
43
|
+
}
|
|
44
|
+
// Return default config if no config file is found
|
|
45
|
+
return { ...exports.DEFAULT_CONFIG };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Load configuration from a specific file
|
|
49
|
+
* @param filePath Path to config file
|
|
50
|
+
* @returns Review configuration
|
|
51
|
+
*/
|
|
52
|
+
function loadConfigFromFile(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
const fileContent = fs_1.default.readFileSync(filePath, "utf8");
|
|
55
|
+
let config;
|
|
56
|
+
if (filePath.endsWith(".json")) {
|
|
57
|
+
config = JSON.parse(fileContent);
|
|
58
|
+
}
|
|
59
|
+
else if (filePath.endsWith(".yml") || filePath.endsWith(".yaml")) {
|
|
60
|
+
config = js_yaml_1.default.load(fileContent);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
throw new Error(`Unsupported config file format: ${filePath}`);
|
|
64
|
+
}
|
|
65
|
+
// Merge with default config
|
|
66
|
+
return { ...exports.DEFAULT_CONFIG, ...config };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error(`Error loading config from ${filePath}: ${error}`);
|
|
70
|
+
return { ...exports.DEFAULT_CONFIG };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Validates the configuration : CLI options && config file
|
|
75
|
+
* @param cliOptions CLI options from command line
|
|
76
|
+
* @param config Configuration from file
|
|
77
|
+
* @returns Updated configuration
|
|
78
|
+
*/
|
|
79
|
+
function validateConfig(cliOptions, config) {
|
|
80
|
+
let finalConfig = { ...config, ...cliOptions };
|
|
81
|
+
// Validate provider
|
|
82
|
+
// Todo: Add more providers as needed ("anthropic", "deepseek", "groq", "gemini")
|
|
83
|
+
const validProviders = ["openai"];
|
|
84
|
+
if (!validProviders.includes(finalConfig.provider)) {
|
|
85
|
+
console.error(`Error: Invalid provider '${finalConfig.provider}'. Using default: ${exports.DEFAULT_CONFIG.provider}`);
|
|
86
|
+
finalConfig.provider = exports.DEFAULT_CONFIG.provider;
|
|
87
|
+
}
|
|
88
|
+
// Validate platform
|
|
89
|
+
// Todo: Add more platforms as needed ("gitlab", "bitbucket")
|
|
90
|
+
const validPlatforms = ["github"];
|
|
91
|
+
if (!validPlatforms.includes(finalConfig.gitPlatform)) {
|
|
92
|
+
console.error(`Error: Invalid platform '${finalConfig.gitPlatform}'. Using default: ${exports.DEFAULT_CONFIG.gitPlatform}`);
|
|
93
|
+
finalConfig.gitPlatform = exports.DEFAULT_CONFIG.gitPlatform;
|
|
94
|
+
}
|
|
95
|
+
// Validate severity
|
|
96
|
+
if (finalConfig.severity &&
|
|
97
|
+
!["suggestion", "warning", "error"].includes(finalConfig.severity)) {
|
|
98
|
+
console.warn(`Warning: Invalid severity '${finalConfig.severity}' in config file. Using default: ${exports.DEFAULT_CONFIG.severity}`);
|
|
99
|
+
finalConfig.severity = exports.DEFAULT_CONFIG.severity;
|
|
100
|
+
}
|
|
101
|
+
return finalConfig;
|
|
102
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseUnifiedDiff = parseUnifiedDiff;
|
|
4
|
+
function parseUnifiedDiff(files) {
|
|
5
|
+
return files.map((file) => {
|
|
6
|
+
if (!file.patch || file.status === "deleted")
|
|
7
|
+
return file;
|
|
8
|
+
const lines = file.patch.split("\n");
|
|
9
|
+
const updatedLines = [];
|
|
10
|
+
let newLineNum = 0;
|
|
11
|
+
let lineOffset = 0;
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
14
|
+
if (hunkMatch) {
|
|
15
|
+
newLineNum = parseInt(hunkMatch[1], 10);
|
|
16
|
+
lineOffset = 0;
|
|
17
|
+
updatedLines.push(line); // keep hunk line
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
21
|
+
const actualLineNumber = newLineNum + lineOffset;
|
|
22
|
+
updatedLines.push(`${line} // LINE_NUMBER: ${actualLineNumber}`);
|
|
23
|
+
lineOffset++;
|
|
24
|
+
}
|
|
25
|
+
else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
26
|
+
updatedLines.push(line); // deleted line
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
updatedLines.push(line); // context line
|
|
30
|
+
lineOffset++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
...file,
|
|
35
|
+
patch: updatedLines.join("\n"),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const cli_1 = require("./cli");
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const platforms_1 = require("./platforms");
|
|
6
|
+
const models_1 = require("./models");
|
|
7
|
+
const parseUnifiedDiff_1 = require("./core/parseUnifiedDiff");
|
|
8
|
+
async function main() {
|
|
9
|
+
try {
|
|
10
|
+
// Parse CLI options
|
|
11
|
+
const cliOptions = (0, cli_1.parseCli)();
|
|
12
|
+
(0, cli_1.verboseLog)(cliOptions, "CLI options parsed");
|
|
13
|
+
// Load configuration
|
|
14
|
+
const fileConfig = await (0, config_1.loadConfig)(cliOptions.configPath);
|
|
15
|
+
(0, cli_1.verboseLog)(cliOptions, `Configuration loaded from ${cliOptions.configPath || "default"}`);
|
|
16
|
+
// Merge CLI options with config
|
|
17
|
+
const config = (0, config_1.validateConfig)(cliOptions, fileConfig);
|
|
18
|
+
(0, cli_1.verboseLog)(config, `Bot configuration: ${JSON.stringify(config, null, 2)}`);
|
|
19
|
+
// Get platform adapter
|
|
20
|
+
const platform = await (0, platforms_1.getPlatform)(config.gitPlatform);
|
|
21
|
+
(0, cli_1.verboseLog)(config, `Using platform: ${config.gitPlatform}`);
|
|
22
|
+
// Get model adapter
|
|
23
|
+
const model = (0, models_1.getModel)(config.provider, config.model, config.endpoint);
|
|
24
|
+
(0, cli_1.verboseLog)(config, `Using model: ${config.model}`);
|
|
25
|
+
// Ensure repository is specified
|
|
26
|
+
if (!config.repo) {
|
|
27
|
+
console.error("Error: Repository is not specified. Please add it to the config file or use the --repo CLI option.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
// Get pull requests that need review
|
|
31
|
+
const pullRequests = await platform.getPullRequests(config.repo);
|
|
32
|
+
(0, cli_1.verboseLog)(config, `Found ${pullRequests.length} PRs`);
|
|
33
|
+
if (pullRequests.length === 0) {
|
|
34
|
+
console.log("No pull requests found that need review");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Process each pull request
|
|
38
|
+
const results = [];
|
|
39
|
+
for (const pr of pullRequests) {
|
|
40
|
+
(0, cli_1.verboseLog)(config, `Processing PR #${pr.number}: ${pr.title}`);
|
|
41
|
+
// Check if AI has already commented since the last update
|
|
42
|
+
const hasCommented = await platform.hasAICommented(config.repo, pr.id);
|
|
43
|
+
if (hasCommented) {
|
|
44
|
+
(0, cli_1.verboseLog)(config, `Skipping PR #${pr.number} - already reviewed since last update`);
|
|
45
|
+
results.push({
|
|
46
|
+
prId: pr.id,
|
|
47
|
+
commentsPosted: 0,
|
|
48
|
+
status: "skipped",
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
// Get PR diff
|
|
54
|
+
const diff = await platform.getPullRequestDiff(config.repo, pr.id);
|
|
55
|
+
(0, cli_1.verboseLog)(config, `Got diff for PR #${pr.number} with ${diff.length} changed files`);
|
|
56
|
+
const parsedDiff = (0, parseUnifiedDiff_1.parseUnifiedDiff)(diff);
|
|
57
|
+
(0, cli_1.verboseLog)(config, `Parsed diff for PR #${pr.number} with ${diff.length} changed files`);
|
|
58
|
+
// Get AI review
|
|
59
|
+
const comments = await model.review(parsedDiff, config);
|
|
60
|
+
(0, cli_1.verboseLog)(config, `Generated ${comments.length} comments for PR #${pr.number}`);
|
|
61
|
+
if (cliOptions.dryRun) {
|
|
62
|
+
// Just print comments in dry run mode
|
|
63
|
+
console.log(`\n== Comments for PR #${pr.number}: ${pr.title} ==`);
|
|
64
|
+
comments.forEach((comment) => {
|
|
65
|
+
console.log(`\n${comment.type === "inline"
|
|
66
|
+
? `${comment.path}:${comment.line}`
|
|
67
|
+
: "Summary comment"}:`);
|
|
68
|
+
console.log(comment.content);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Post comments to PR
|
|
73
|
+
for (const comment of comments) {
|
|
74
|
+
await platform.postComment(config.repo, pr.id, comment);
|
|
75
|
+
}
|
|
76
|
+
console.log(`Posted ${comments.length} comments to PR #${pr.number}`);
|
|
77
|
+
}
|
|
78
|
+
results.push({
|
|
79
|
+
prId: pr.id,
|
|
80
|
+
commentsPosted: comments.length,
|
|
81
|
+
status: "success",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error(`Error processing PR #${pr.number}: ${error}`);
|
|
86
|
+
results.push({
|
|
87
|
+
prId: pr.id,
|
|
88
|
+
commentsPosted: 0,
|
|
89
|
+
status: "failure",
|
|
90
|
+
error: error instanceof Error ? error.message : String(error),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Print summary
|
|
95
|
+
console.log("\n== Review Summary ==");
|
|
96
|
+
console.log(`Total PRs: ${pullRequests.length}`);
|
|
97
|
+
console.log(`Reviewed: ${results.filter((r) => r.status === "success").length}`);
|
|
98
|
+
console.log(`Skipped: ${results.filter((r) => r.status === "skipped").length}`);
|
|
99
|
+
console.log(`Failed: ${results.filter((r) => r.status === "failure").length}`);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.error(`Error: ${error}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Run the application
|
|
107
|
+
main().catch((error) => {
|
|
108
|
+
console.error(`Unhandled error: ${error}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CodeReviewModel } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Get model adapter for the specified AI model
|
|
4
|
+
* @param provider The provider of the AI model (e.g., openai, anthropic, deepseek)
|
|
5
|
+
* @param model The specific model to use (e.g., gpt-3.5-turbo, gpt-4)
|
|
6
|
+
* @param endpoint Optional custom endpoint URL
|
|
7
|
+
* @returns Model adapter instance
|
|
8
|
+
*/
|
|
9
|
+
export declare function getModel(provider: string, model: string, endpoint?: string): CodeReviewModel;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getModel = getModel;
|
|
4
|
+
const openai_1 = require("./openai");
|
|
5
|
+
/**
|
|
6
|
+
* Get model adapter for the specified AI model
|
|
7
|
+
* @param provider The provider of the AI model (e.g., openai, anthropic, deepseek)
|
|
8
|
+
* @param model The specific model to use (e.g., gpt-3.5-turbo, gpt-4)
|
|
9
|
+
* @param endpoint Optional custom endpoint URL
|
|
10
|
+
* @returns Model adapter instance
|
|
11
|
+
*/
|
|
12
|
+
function getModel(provider, model, endpoint) {
|
|
13
|
+
switch (provider) {
|
|
14
|
+
case "openai":
|
|
15
|
+
return new openai_1.OpenAIModel(model, endpoint);
|
|
16
|
+
default:
|
|
17
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CodeReviewModel, FileChange, AIComment, ReviewConfig } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI model adapter for code review
|
|
4
|
+
*/
|
|
5
|
+
export declare class OpenAIModel implements CodeReviewModel {
|
|
6
|
+
private client;
|
|
7
|
+
private model;
|
|
8
|
+
constructor(model: string, endpoint?: string);
|
|
9
|
+
/**
|
|
10
|
+
* Generate a code review prompt for the given diff
|
|
11
|
+
* @param diff File changes to review
|
|
12
|
+
* @param config Review configuration
|
|
13
|
+
* @returns Prompt for the AI
|
|
14
|
+
*/
|
|
15
|
+
private generatePrompt;
|
|
16
|
+
/**
|
|
17
|
+
* Parse the AI response into comments
|
|
18
|
+
* @param response AI generated response
|
|
19
|
+
* @param config Review configuration
|
|
20
|
+
* @returns List of comments
|
|
21
|
+
*/
|
|
22
|
+
private parseResponse;
|
|
23
|
+
/**
|
|
24
|
+
* Review code changes and generate comments
|
|
25
|
+
* @param diff File changes to review
|
|
26
|
+
* @param config Review configuration
|
|
27
|
+
* @returns List of AI comments
|
|
28
|
+
*/
|
|
29
|
+
review(diff: FileChange[], config: ReviewConfig): Promise<AIComment[]>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OpenAIModel = void 0;
|
|
7
|
+
const openai_1 = __importDefault(require("openai"));
|
|
8
|
+
/**
|
|
9
|
+
* OpenAI model adapter for code review
|
|
10
|
+
*/
|
|
11
|
+
class OpenAIModel {
|
|
12
|
+
constructor(model, endpoint) {
|
|
13
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
throw new Error("OPENAI_API_KEY environment variable is required");
|
|
16
|
+
}
|
|
17
|
+
if (!model) {
|
|
18
|
+
throw new Error("Model is required");
|
|
19
|
+
}
|
|
20
|
+
this.model = model;
|
|
21
|
+
this.client = new openai_1.default({
|
|
22
|
+
apiKey,
|
|
23
|
+
baseURL: endpoint,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generate a code review prompt for the given diff
|
|
28
|
+
* @param diff File changes to review
|
|
29
|
+
* @param config Review configuration
|
|
30
|
+
* @returns Prompt for the AI
|
|
31
|
+
*/
|
|
32
|
+
generatePrompt(diff, config) {
|
|
33
|
+
const rules = config.rules && config.rules.length > 0
|
|
34
|
+
? `\nApply these specific rules:\n${config.rules
|
|
35
|
+
.map((rule) => `- ${rule}`)
|
|
36
|
+
.join("\n")}`
|
|
37
|
+
: "";
|
|
38
|
+
const diffText = diff
|
|
39
|
+
.map((file) => {
|
|
40
|
+
return `File: ${file.filename} (${file.status})
|
|
41
|
+
${file.patch || "No changes"}
|
|
42
|
+
`;
|
|
43
|
+
})
|
|
44
|
+
.join("\n\n");
|
|
45
|
+
return `
|
|
46
|
+
Only provide brief, actionable, file-specific feedback related to the actual code diff.
|
|
47
|
+
Do not include general advice, documentation-style summaries, or best practices unless they directly relate to the diff.
|
|
48
|
+
Use a direct tone. No greetings. No summaries. No repeated advice.
|
|
49
|
+
|
|
50
|
+
Important formatting rules:
|
|
51
|
+
- Do not comment on lines that are unchanged or just context unless it's directly impacted by a change.
|
|
52
|
+
${config.commentStyle === "inline"
|
|
53
|
+
? "- Output only inline comments, Use this format: 'filename.py:<line number in the new file> — comment'"
|
|
54
|
+
: "- Provide a single short summary of your review."}
|
|
55
|
+
|
|
56
|
+
${rules}
|
|
57
|
+
|
|
58
|
+
Here are the changes to review:
|
|
59
|
+
|
|
60
|
+
${diffText}
|
|
61
|
+
|
|
62
|
+
${config.customPrompt || ""}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse the AI response into comments
|
|
66
|
+
* @param response AI generated response
|
|
67
|
+
* @param config Review configuration
|
|
68
|
+
* @returns List of comments
|
|
69
|
+
*/
|
|
70
|
+
parseResponse(response, config) {
|
|
71
|
+
const comments = [];
|
|
72
|
+
if (config.commentStyle === "summary") {
|
|
73
|
+
// Generate a single summary comment
|
|
74
|
+
comments.push({
|
|
75
|
+
type: "summary",
|
|
76
|
+
content: response.trim(),
|
|
77
|
+
severity: config.severity,
|
|
78
|
+
});
|
|
79
|
+
return comments;
|
|
80
|
+
}
|
|
81
|
+
// Look for patterns like "filename.ext:123 — comment text"
|
|
82
|
+
const inlineCommentRegex = /([\w/.-]+):(\d+)\s*[—–-]\s*(.*?)(?=\s+[\w/.-]+:\d+\s*[—–-]|$)/gs;
|
|
83
|
+
let match;
|
|
84
|
+
while ((match = inlineCommentRegex.exec(response + "\n\n")) !== null) {
|
|
85
|
+
const [, path, lineStr, content] = match;
|
|
86
|
+
const line = parseInt(lineStr, 10);
|
|
87
|
+
comments.push({
|
|
88
|
+
type: "inline",
|
|
89
|
+
path,
|
|
90
|
+
line,
|
|
91
|
+
content: content.trim(),
|
|
92
|
+
severity: config.severity,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// If no inline comments were parsed, create a summary comment
|
|
96
|
+
if (comments.length === 0) {
|
|
97
|
+
comments.push({
|
|
98
|
+
type: "summary",
|
|
99
|
+
content: response.trim(),
|
|
100
|
+
severity: config.severity,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return comments;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Review code changes and generate comments
|
|
107
|
+
* @param diff File changes to review
|
|
108
|
+
* @param config Review configuration
|
|
109
|
+
* @returns List of AI comments
|
|
110
|
+
*/
|
|
111
|
+
async review(diff, config) {
|
|
112
|
+
// Filter out ignored files
|
|
113
|
+
if (config.ignoreFiles && config.ignoreFiles.length > 0) {
|
|
114
|
+
diff = diff.filter((file) => {
|
|
115
|
+
return !config.ignoreFiles?.some((pattern) => {
|
|
116
|
+
// Basic glob pattern matching for *.ext
|
|
117
|
+
if (pattern.startsWith("*") && pattern.indexOf(".") > 0) {
|
|
118
|
+
const ext = pattern.substring(1);
|
|
119
|
+
return file.filename.endsWith(ext);
|
|
120
|
+
}
|
|
121
|
+
return file.filename === pattern;
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Skip if no files to review
|
|
126
|
+
if (diff.length === 0) {
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
type: "summary",
|
|
130
|
+
content: "No files to review after applying ignore patterns.",
|
|
131
|
+
severity: "suggestion",
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
const prompt = this.generatePrompt(diff, config);
|
|
136
|
+
try {
|
|
137
|
+
const response = await this.client.chat.completions.create({
|
|
138
|
+
model: this.model,
|
|
139
|
+
messages: [
|
|
140
|
+
{
|
|
141
|
+
role: "system",
|
|
142
|
+
content: "You are a senior software engineer doing a peer code review. Your job is to spot all logic, syntax, and semantic issues in a code diff.",
|
|
143
|
+
},
|
|
144
|
+
{ role: "user", content: prompt },
|
|
145
|
+
],
|
|
146
|
+
temperature: 0.1,
|
|
147
|
+
max_tokens: 4000,
|
|
148
|
+
store: true,
|
|
149
|
+
});
|
|
150
|
+
const content = response.choices[0]?.message.content;
|
|
151
|
+
if (!content) {
|
|
152
|
+
throw new Error("No response from OpenAI");
|
|
153
|
+
}
|
|
154
|
+
return this.parseResponse(content, config);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error("Error generating review with OpenAI:", error);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exports.OpenAIModel = OpenAIModel;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CodeReviewPlatform, PullRequest, FileChange, AIComment } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* GitHub platform adapter
|
|
4
|
+
*/
|
|
5
|
+
export declare class GithubPlatform implements CodeReviewPlatform {
|
|
6
|
+
private client;
|
|
7
|
+
private commentSignature;
|
|
8
|
+
private constructor();
|
|
9
|
+
static init(): Promise<GithubPlatform>;
|
|
10
|
+
/**
|
|
11
|
+
* Extract owner and repo from repo string
|
|
12
|
+
* @param repo Repository in format "owner/repo"
|
|
13
|
+
* @returns Object with owner and repo properties
|
|
14
|
+
*/
|
|
15
|
+
private parseRepo;
|
|
16
|
+
/**
|
|
17
|
+
* Get all open pull requests for a repository
|
|
18
|
+
* @param repo Repository in format "owner/repo"
|
|
19
|
+
* @returns List of pull requests
|
|
20
|
+
*/
|
|
21
|
+
getPullRequests(repo: string): Promise<PullRequest[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Get the diff for a pull request
|
|
24
|
+
* @param repo Repository in format "owner/repo"
|
|
25
|
+
* @param prId Pull request ID
|
|
26
|
+
* @returns List of file changes
|
|
27
|
+
*/
|
|
28
|
+
getPullRequestDiff(repo: string, prId: string | number): Promise<FileChange[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Post a comment on a pull request
|
|
31
|
+
* @param repo Repository in format "owner/repo"
|
|
32
|
+
* @param prId Pull request ID
|
|
33
|
+
* @param comment Comment to post
|
|
34
|
+
*/
|
|
35
|
+
postComment(repo: string, prId: string | number, comment: AIComment): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Checks if the AI has already commented on the latest commit of a pull request
|
|
38
|
+
* @param repo Repository in format "owner/repo"
|
|
39
|
+
* @param prId Pull request ID
|
|
40
|
+
* @returns True if the AI has commented, false otherwise
|
|
41
|
+
*/
|
|
42
|
+
hasAICommented(repo: string, prId: string | number): Promise<boolean>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GithubPlatform = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* GitHub platform adapter
|
|
6
|
+
*/
|
|
7
|
+
class GithubPlatform {
|
|
8
|
+
constructor(client) {
|
|
9
|
+
this.commentSignature = "<!-- DIFF-HOUND-BOT -->";
|
|
10
|
+
this.client = client;
|
|
11
|
+
}
|
|
12
|
+
static async init() {
|
|
13
|
+
const { Octokit } = await import("@octokit/rest");
|
|
14
|
+
const token = process.env.GITHUB_TOKEN;
|
|
15
|
+
if (!token) {
|
|
16
|
+
throw new Error("GITHUB_TOKEN environment variable is required");
|
|
17
|
+
}
|
|
18
|
+
const client = new Octokit({ auth: token });
|
|
19
|
+
return new GithubPlatform(client);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Extract owner and repo from repo string
|
|
23
|
+
* @param repo Repository in format "owner/repo"
|
|
24
|
+
* @returns Object with owner and repo properties
|
|
25
|
+
*/
|
|
26
|
+
parseRepo(repo) {
|
|
27
|
+
const [owner, repoName] = repo.split("/");
|
|
28
|
+
if (!owner || !repoName) {
|
|
29
|
+
throw new Error(`Invalid repository format: ${repo}. Expected format: owner/repo`);
|
|
30
|
+
}
|
|
31
|
+
return { owner, repo: repoName };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get all open pull requests for a repository
|
|
35
|
+
* @param repo Repository in format "owner/repo"
|
|
36
|
+
* @returns List of pull requests
|
|
37
|
+
*/
|
|
38
|
+
async getPullRequests(repo) {
|
|
39
|
+
const { owner, repo: repoName } = this.parseRepo(repo);
|
|
40
|
+
const { data: pulls } = await this.client.pulls.list({
|
|
41
|
+
owner,
|
|
42
|
+
repo: repoName,
|
|
43
|
+
state: "open",
|
|
44
|
+
sort: "updated",
|
|
45
|
+
direction: "desc",
|
|
46
|
+
});
|
|
47
|
+
return pulls.map((pull) => ({
|
|
48
|
+
id: pull.number,
|
|
49
|
+
number: pull.number,
|
|
50
|
+
title: pull.title,
|
|
51
|
+
description: pull.body || undefined,
|
|
52
|
+
author: pull.user?.login || "unknown",
|
|
53
|
+
branch: pull.head.ref,
|
|
54
|
+
baseBranch: pull.base.ref,
|
|
55
|
+
updatedAt: new Date(pull.updated_at),
|
|
56
|
+
url: pull.html_url,
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get the diff for a pull request
|
|
61
|
+
* @param repo Repository in format "owner/repo"
|
|
62
|
+
* @param prId Pull request ID
|
|
63
|
+
* @returns List of file changes
|
|
64
|
+
*/
|
|
65
|
+
async getPullRequestDiff(repo, prId) {
|
|
66
|
+
const { owner, repo: repoName } = this.parseRepo(repo);
|
|
67
|
+
const { data: files } = await this.client.pulls.listFiles({
|
|
68
|
+
owner,
|
|
69
|
+
repo: repoName,
|
|
70
|
+
pull_number: Number(prId),
|
|
71
|
+
});
|
|
72
|
+
return files.map((file) => ({
|
|
73
|
+
filename: file.filename,
|
|
74
|
+
status: file.status,
|
|
75
|
+
additions: file.additions,
|
|
76
|
+
deletions: file.deletions,
|
|
77
|
+
patch: file.patch || undefined,
|
|
78
|
+
previousFilename: file.previous_filename,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Post a comment on a pull request
|
|
83
|
+
* @param repo Repository in format "owner/repo"
|
|
84
|
+
* @param prId Pull request ID
|
|
85
|
+
* @param comment Comment to post
|
|
86
|
+
*/
|
|
87
|
+
async postComment(repo, prId, comment) {
|
|
88
|
+
const { owner, repo: repoName } = this.parseRepo(repo);
|
|
89
|
+
const pullNumber = Number(prId);
|
|
90
|
+
const { data: pull } = await this.client.pulls.get({
|
|
91
|
+
owner,
|
|
92
|
+
repo: repoName,
|
|
93
|
+
pull_number: pullNumber,
|
|
94
|
+
});
|
|
95
|
+
if (comment.type === "inline" && comment.path && comment.line) {
|
|
96
|
+
// Post inline comment
|
|
97
|
+
await this.client.pulls.createReviewComment({
|
|
98
|
+
owner,
|
|
99
|
+
repo: repoName,
|
|
100
|
+
pull_number: pullNumber,
|
|
101
|
+
body: `${comment.content}\n\n${this.commentSignature}\n<!-- SHA: ${pull.head.sha} -->`,
|
|
102
|
+
commit_id: pull.head.sha,
|
|
103
|
+
path: comment.path,
|
|
104
|
+
line: comment.line,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Post PR comment
|
|
109
|
+
await this.client.issues.createComment({
|
|
110
|
+
owner,
|
|
111
|
+
repo: repoName,
|
|
112
|
+
issue_number: pullNumber,
|
|
113
|
+
body: `${comment.content}\n\n${this.commentSignature}\n<!-- SHA: ${pull.head.sha} -->`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Checks if the AI has already commented on the latest commit of a pull request
|
|
119
|
+
* @param repo Repository in format "owner/repo"
|
|
120
|
+
* @param prId Pull request ID
|
|
121
|
+
* @returns True if the AI has commented, false otherwise
|
|
122
|
+
*/
|
|
123
|
+
async hasAICommented(repo, prId) {
|
|
124
|
+
const { owner, repo: repoName } = this.parseRepo(repo);
|
|
125
|
+
const pullNumber = Number(prId);
|
|
126
|
+
const { data: pull } = await this.client.pulls.get({
|
|
127
|
+
owner,
|
|
128
|
+
repo: repoName,
|
|
129
|
+
pull_number: pullNumber,
|
|
130
|
+
});
|
|
131
|
+
const latestCommitSha = pull.head.sha;
|
|
132
|
+
// --- Get issue (summary) comments
|
|
133
|
+
const issueComments = (await this.client.paginate(this.client.issues.listComments.endpoint.merge({
|
|
134
|
+
owner,
|
|
135
|
+
repo: repoName,
|
|
136
|
+
issue_number: pullNumber,
|
|
137
|
+
per_page: 100,
|
|
138
|
+
})));
|
|
139
|
+
// --- Get review (inline) comments
|
|
140
|
+
const reviewComments = (await this.client.paginate(this.client.pulls.listReviewComments.endpoint.merge({
|
|
141
|
+
owner,
|
|
142
|
+
repo: repoName,
|
|
143
|
+
pull_number: pullNumber,
|
|
144
|
+
per_page: 100,
|
|
145
|
+
})));
|
|
146
|
+
const allComments = [...issueComments, ...reviewComments];
|
|
147
|
+
const reviewedShas = allComments
|
|
148
|
+
.filter((c) => c.body?.includes(this.commentSignature))
|
|
149
|
+
.map((c) => {
|
|
150
|
+
const match = c.body?.match(/<!-- SHA: (.+?) -->/);
|
|
151
|
+
return match?.[1];
|
|
152
|
+
})
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
return reviewedShas.includes(latestCommitSha);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.GithubPlatform = GithubPlatform;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Platform, CodeReviewPlatform } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Get platform adapter for the specified platform
|
|
4
|
+
* @param platform Platform to use
|
|
5
|
+
* @returns Platform adapter instance
|
|
6
|
+
*/
|
|
7
|
+
export declare function getPlatform(platform: Platform): Promise<CodeReviewPlatform>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPlatform = getPlatform;
|
|
4
|
+
const github_1 = require("./github");
|
|
5
|
+
/**
|
|
6
|
+
* Get platform adapter for the specified platform
|
|
7
|
+
* @param platform Platform to use
|
|
8
|
+
* @returns Platform adapter instance
|
|
9
|
+
*/
|
|
10
|
+
async function getPlatform(platform) {
|
|
11
|
+
switch (platform) {
|
|
12
|
+
case "github":
|
|
13
|
+
return await github_1.GithubPlatform.init();
|
|
14
|
+
default:
|
|
15
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for the AI Code Reviewer
|
|
3
|
+
*/
|
|
4
|
+
export type Platform = "github";
|
|
5
|
+
export type Provider = "openai";
|
|
6
|
+
export interface ReviewConfig {
|
|
7
|
+
provider: Provider;
|
|
8
|
+
model: string;
|
|
9
|
+
gitPlatform: Platform;
|
|
10
|
+
repo?: string;
|
|
11
|
+
commentStyle?: "inline" | "summary";
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
configPath?: string;
|
|
16
|
+
severity?: "suggestion" | "warning" | "error";
|
|
17
|
+
ignoreFiles?: string[];
|
|
18
|
+
rules?: string[];
|
|
19
|
+
customPrompt?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PullRequest {
|
|
22
|
+
id: string | number;
|
|
23
|
+
number?: number;
|
|
24
|
+
title: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
author: string;
|
|
27
|
+
branch: string;
|
|
28
|
+
baseBranch: string;
|
|
29
|
+
commits?: string[];
|
|
30
|
+
updatedAt: Date;
|
|
31
|
+
url?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface FileChange {
|
|
34
|
+
filename: string;
|
|
35
|
+
status: "added" | "modified" | "deleted" | "renamed";
|
|
36
|
+
additions: number;
|
|
37
|
+
deletions: number;
|
|
38
|
+
patch?: string;
|
|
39
|
+
previousFilename?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface AIComment {
|
|
42
|
+
type: "inline" | "summary";
|
|
43
|
+
path?: string;
|
|
44
|
+
line?: number;
|
|
45
|
+
content: string;
|
|
46
|
+
suggestions?: string[];
|
|
47
|
+
severity?: "suggestion" | "warning" | "error";
|
|
48
|
+
created_at?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface CodeReviewPlatform {
|
|
51
|
+
getPullRequests(repo: string): Promise<PullRequest[]>;
|
|
52
|
+
getPullRequestDiff(repo: string, prId: string | number): Promise<FileChange[]>;
|
|
53
|
+
postComment(repo: string, prId: string | number, comment: AIComment): Promise<void>;
|
|
54
|
+
hasAICommented(repo: string, prId: string | number): Promise<boolean>;
|
|
55
|
+
}
|
|
56
|
+
export interface CodeReviewModel {
|
|
57
|
+
review(diff: FileChange[], config: ReviewConfig): Promise<AIComment[]>;
|
|
58
|
+
}
|
|
59
|
+
export interface ReviewResult {
|
|
60
|
+
prId: string | number;
|
|
61
|
+
commentsPosted: number;
|
|
62
|
+
status: "success" | "failure" | "skipped";
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "diff-hound",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "AI-powered code review bot for GitHub, GitLab, and Bitbucket",
|
|
5
|
+
"main": "./bin/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"diff-hound": "bin/diff-hound.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "ts-node src/index.ts",
|
|
19
|
+
"lint": "eslint src/**/*.ts"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"code-review",
|
|
23
|
+
"ai",
|
|
24
|
+
"github",
|
|
25
|
+
"gitlab",
|
|
26
|
+
"bitbucket",
|
|
27
|
+
"openai",
|
|
28
|
+
"claude",
|
|
29
|
+
"llm"
|
|
30
|
+
],
|
|
31
|
+
"author": "runtimebug",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/runtimebug/diff-hound.git"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@octokit/rest": "^21.1.1",
|
|
42
|
+
"commander": "^11.0.0",
|
|
43
|
+
"dotenv": "^16.3.1",
|
|
44
|
+
"js-yaml": "^4.1.0",
|
|
45
|
+
"openai": "^4.10.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@changesets/cli": "^2.29.2",
|
|
49
|
+
"@types/js-yaml": "^4.0.5",
|
|
50
|
+
"@types/node": "^20.6.0",
|
|
51
|
+
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
|
52
|
+
"@typescript-eslint/parser": "^6.7.0",
|
|
53
|
+
"eslint": "^8.49.0",
|
|
54
|
+
"ts-node": "^10.9.1",
|
|
55
|
+
"typescript": "^5.2.2"
|
|
56
|
+
}
|
|
57
|
+
}
|