nayan-ai 1.0.0-beta.2 ā 1.0.0-beta.4
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 +3 -2
- package/package.json +6 -2
- package/.github/workflows/publish.yml +0 -26
- package/src/analyzer.ts +0 -77
- package/src/claude.ts +0 -158
- package/src/cli.ts +0 -192
- package/src/codex.ts +0 -158
- package/src/github.ts +0 -108
- package/src/index.ts +0 -6
- package/src/logs.ts +0 -253
- package/src/prompt.ts +0 -77
- package/src/repo.ts +0 -81
- package/src/types.ts +0 -51
- package/tsconfig.json +0 -19
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Nayan AI š¤
|
|
2
2
|
|
|
3
|
-
A CLI tool that uses [Codex CLI](https://github.com/openai/codex) to review GitHub Pull Requests for bugs, security vulnerabilities, performance issues, error handling, edge cases, and code quality.
|
|
3
|
+
A CLI tool that uses [Codex CLI](https://github.com/openai/codex) or [Claude Code CLI](https://code.claude.com) to review GitHub Pull Requests for bugs, security vulnerabilities, performance issues, error handling, edge cases, and code quality.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **š¤ Agentic Review**: Uses Codex CLI's intelligent coding
|
|
7
|
+
- **š¤ Agentic Review**: Uses Codex CLI or Claude Code CLI's intelligent coding agents for deep code analysis
|
|
8
8
|
- **š Bug Detection**: Finds logic errors, null pointer issues, race conditions, and edge cases
|
|
9
9
|
- **š Security Analysis**: Detects SQL injection, XSS, hardcoded secrets, and auth issues
|
|
10
10
|
- **ā” Performance Checks**: Identifies memory leaks, N+1 queries, and unnecessary computations
|
|
@@ -50,6 +50,7 @@ nayan-ai https://github.com/owner/repo/pull/123 --token ghp_xxx
|
|
|
50
50
|
| Option | Description |
|
|
51
51
|
|--------|--------------------------------------------------------|
|
|
52
52
|
| `-t, --token` | GitHub personal access token to access code (required) |
|
|
53
|
+
| `-l, --llm` | LLM provider: `codex` (default) or `claude` |
|
|
53
54
|
| `-d, --dry` | Analyze without posting comments to Github |
|
|
54
55
|
| `-i, --inline` | Post inline comments on files instead of summary |
|
|
55
56
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nayan-ai",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
4
|
-
"description": "AI
|
|
3
|
+
"version": "1.0.0-beta.4",
|
|
4
|
+
"description": "AI powered code reviewer using Codex & Claude Code agents",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nayan-ai": "dist/cli.js"
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=18.0.0"
|
|
38
38
|
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
39
43
|
"dependencies": {
|
|
40
44
|
"chalk": "^5.6.2",
|
|
41
45
|
"commander": "^14.0.2",
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
name: Publish nayan-ai package to npm
|
|
2
|
-
on: [workflow_dispatch]
|
|
3
|
-
|
|
4
|
-
jobs:
|
|
5
|
-
publish:
|
|
6
|
-
runs-on: ubuntu-latest
|
|
7
|
-
steps:
|
|
8
|
-
- uses: actions/checkout@v4
|
|
9
|
-
|
|
10
|
-
- name: Set up Node.js
|
|
11
|
-
uses: actions/setup-node@v4
|
|
12
|
-
with:
|
|
13
|
-
node-version: 20
|
|
14
|
-
registry-url: 'https://registry.npmjs.org/'
|
|
15
|
-
always-auth: true
|
|
16
|
-
|
|
17
|
-
- name: Install dependencies
|
|
18
|
-
run: npm install
|
|
19
|
-
|
|
20
|
-
- name: Build
|
|
21
|
-
run: npm run build
|
|
22
|
-
|
|
23
|
-
- name: Publish to npm
|
|
24
|
-
run: npm publish --access public
|
|
25
|
-
env:
|
|
26
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH }}
|
package/src/analyzer.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { CodeIssue, ReviewComment } from './types.js';
|
|
2
|
-
|
|
3
|
-
export function issuesToReviewComments(issues: CodeIssue[]): ReviewComment[] {
|
|
4
|
-
return issues.map((issue) => {
|
|
5
|
-
const severityIcon = issue.severity === 'error' ? 'š“' : issue.severity === 'warning' ? 'š”' : 'šµ';
|
|
6
|
-
const categoryIcon = issue.category === 'functionality' ? 'š' : issue.category === 'readability' ? 'š' : 'ā”';
|
|
7
|
-
|
|
8
|
-
let body = `${severityIcon} **${categoryIcon} ${capitalize(issue.category)}**\n\n${issue.message}`;
|
|
9
|
-
if (issue.suggestion) {
|
|
10
|
-
body += `\n\nš” **Suggestion:** ${issue.suggestion}`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
path: issue.filename,
|
|
15
|
-
line: issue.line,
|
|
16
|
-
side: 'RIGHT' as const,
|
|
17
|
-
body,
|
|
18
|
-
};
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function generateSummary(issues: CodeIssue[]): string {
|
|
23
|
-
if (issues.length === 0) {
|
|
24
|
-
return '## ā
AI Code Review Complete\n\nNo issues found. The code looks good!';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const bySeverity = {
|
|
28
|
-
error: issues.filter((i) => i.severity === 'error'),
|
|
29
|
-
warning: issues.filter((i) => i.severity === 'warning'),
|
|
30
|
-
info: issues.filter((i) => i.severity === 'info'),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
let summary = `## š¤ AI Code Review Complete\n\nFound **${issues.length}** issue(s) in this PR.\n\n`;
|
|
34
|
-
|
|
35
|
-
// Group issues by severity and list them
|
|
36
|
-
if (bySeverity.error.length > 0) {
|
|
37
|
-
summary += `### š“ Errors (${bySeverity.error.length})\n\n`;
|
|
38
|
-
bySeverity.error.forEach((issue) => {
|
|
39
|
-
const icon = issue.category === 'functionality' ? 'š' : issue.category === 'readability' ? 'š' : 'ā”';
|
|
40
|
-
summary += `- ${icon} **\`${issue.filename}:${issue.line}\`**\n ${issue.message}`;
|
|
41
|
-
if (issue.suggestion) {
|
|
42
|
-
summary += `\n š” *${issue.suggestion}*`;
|
|
43
|
-
}
|
|
44
|
-
summary += '\n\n';
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (bySeverity.warning.length > 0) {
|
|
49
|
-
summary += `### š” Warnings (${bySeverity.warning.length})\n\n`;
|
|
50
|
-
bySeverity.warning.forEach((issue) => {
|
|
51
|
-
const icon = issue.category === 'functionality' ? 'š' : issue.category === 'readability' ? 'š' : 'ā”';
|
|
52
|
-
summary += `- ${icon} **\`${issue.filename}:${issue.line}\`**\n ${issue.message}`;
|
|
53
|
-
if (issue.suggestion) {
|
|
54
|
-
summary += `\n š” *${issue.suggestion}*`;
|
|
55
|
-
}
|
|
56
|
-
summary += '\n\n';
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (bySeverity.info.length > 0) {
|
|
61
|
-
summary += `### šµ Info (${bySeverity.info.length})\n\n`;
|
|
62
|
-
bySeverity.info.forEach((issue) => {
|
|
63
|
-
const icon = issue.category === 'functionality' ? 'š' : issue.category === 'readability' ? 'š' : 'ā”';
|
|
64
|
-
summary += `- ${icon} **\`${issue.filename}:${issue.line}\`**\n ${issue.message}`;
|
|
65
|
-
if (issue.suggestion) {
|
|
66
|
-
summary += `\n š” *${issue.suggestion}*`;
|
|
67
|
-
}
|
|
68
|
-
summary += '\n\n';
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return summary.trim();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function capitalize(str: string): string {
|
|
76
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
77
|
-
}
|
package/src/claude.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import type { CodeIssue } from './types.js';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { getReviewPrompt } from './prompt.js';
|
|
5
|
-
|
|
6
|
-
export interface ClaudeOptions {
|
|
7
|
-
verbose?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function analyzeWithClaude(
|
|
11
|
-
repoPath: string,
|
|
12
|
-
baseBranch: string = 'origin/main',
|
|
13
|
-
options: ClaudeOptions
|
|
14
|
-
): Promise<CodeIssue[]> {
|
|
15
|
-
const response = await runClaudeExec(repoPath, baseBranch, options);
|
|
16
|
-
return parseClaudeResponse(response);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function runClaudeExec(
|
|
20
|
-
repoPath: string,
|
|
21
|
-
baseBranch: string,
|
|
22
|
-
options: ClaudeOptions
|
|
23
|
-
): Promise<string> {
|
|
24
|
-
const prompt = getReviewPrompt(baseBranch);
|
|
25
|
-
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
// Use claude with -p for print mode and --output-format json
|
|
28
|
-
const args = [
|
|
29
|
-
'-p',
|
|
30
|
-
'--output-format', 'json',
|
|
31
|
-
'--dangerously-skip-permissions',
|
|
32
|
-
prompt
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
if (options.verbose) {
|
|
36
|
-
console.log(`\n[Claude] Running: claude ${args.slice(0, 3).join(' ')} "<prompt>"`);
|
|
37
|
-
console.log(`[Claude] Working directory: ${repoPath}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const startTime = Date.now();
|
|
41
|
-
|
|
42
|
-
const child = spawn('claude', args, {
|
|
43
|
-
cwd: repoPath,
|
|
44
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
let stdout = '';
|
|
48
|
-
let stderr = '';
|
|
49
|
-
|
|
50
|
-
child.stdout.on('data', (data) => {
|
|
51
|
-
const chunk = data.toString();
|
|
52
|
-
stdout += chunk;
|
|
53
|
-
|
|
54
|
-
if (options.verbose) {
|
|
55
|
-
process.stdout.write(chunk);
|
|
56
|
-
} else {
|
|
57
|
-
// Show progress dots
|
|
58
|
-
process.stdout.write(chalk.gray('.'));
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
child.stderr.on('data', (data) => {
|
|
63
|
-
const chunk = data.toString();
|
|
64
|
-
stderr += chunk;
|
|
65
|
-
if (options.verbose) {
|
|
66
|
-
process.stderr.write(chunk);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
child.on('close', (code) => {
|
|
71
|
-
const elapsed = Date.now() - startTime;
|
|
72
|
-
|
|
73
|
-
if (!options.verbose) {
|
|
74
|
-
console.log(); // New line after progress dots
|
|
75
|
-
}
|
|
76
|
-
console.log(`\n[Claude] Completed in ${(elapsed / 1000).toFixed(1)}s`);
|
|
77
|
-
|
|
78
|
-
if (code !== 0) {
|
|
79
|
-
const errorMsg = stderr || stdout || 'Unknown error';
|
|
80
|
-
reject(new Error(`Claude review failed (exit ${code}): ${errorMsg.slice(0, 500)}`));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
resolve(stdout);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
child.on('error', (err) => {
|
|
88
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
89
|
-
reject(new Error('claude CLI not found. Install Claude Code CLI first: https://code.claude.com'));
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
reject(err);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function parseClaudeResponse(response: string): CodeIssue[] {
|
|
98
|
-
// Claude with --output-format json outputs a JSON object with result field
|
|
99
|
-
let parseError: Error | null = null;
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
const parsed = JSON.parse(response);
|
|
103
|
-
|
|
104
|
-
// Claude JSON output has a 'result' field with the text response
|
|
105
|
-
const text = parsed.result || parsed.text || response;
|
|
106
|
-
|
|
107
|
-
// Try to extract JSON from the text
|
|
108
|
-
const jsonMatch = text.match(/\{\s*"issues"\s*:\s*\[[\s\S]*?\]\s*\}/);
|
|
109
|
-
if (jsonMatch) {
|
|
110
|
-
const issuesJson = JSON.parse(jsonMatch[0]);
|
|
111
|
-
if (issuesJson.issues && Array.isArray(issuesJson.issues)) {
|
|
112
|
-
return issuesJson.issues
|
|
113
|
-
.filter((item: any) => item.message)
|
|
114
|
-
.map((item: any) => ({
|
|
115
|
-
filename: item.filename || 'unknown',
|
|
116
|
-
line: item.line || 0,
|
|
117
|
-
category: item.category || 'functionality',
|
|
118
|
-
severity: item.severity || 'info',
|
|
119
|
-
message: item.message,
|
|
120
|
-
suggestion: item.suggestion,
|
|
121
|
-
}));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
} catch (err) {
|
|
125
|
-
parseError = err as Error;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Fallback: try to find raw JSON in the response
|
|
129
|
-
const jsonMatch = response.match(/\{\s*"issues"\s*:\s*\[[\s\S]*?\]\s*\}/);
|
|
130
|
-
if (jsonMatch) {
|
|
131
|
-
try {
|
|
132
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
133
|
-
return (parsed.issues || [])
|
|
134
|
-
.filter((item: any) => item.message)
|
|
135
|
-
.map((item: any) => ({
|
|
136
|
-
filename: item.filename || 'unknown',
|
|
137
|
-
line: item.line || 0,
|
|
138
|
-
category: item.category || 'functionality',
|
|
139
|
-
severity: item.severity || 'info',
|
|
140
|
-
message: item.message,
|
|
141
|
-
suggestion: item.suggestion,
|
|
142
|
-
}));
|
|
143
|
-
} catch (err) {
|
|
144
|
-
parseError = err as Error;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// If we couldn't parse any issues and there was an error, log it
|
|
149
|
-
if (parseError) {
|
|
150
|
-
console.warn(chalk.yellow(`\nā Warning: Failed to parse Claude response: ${parseError.message}`));
|
|
151
|
-
console.warn(chalk.gray(` Response preview: ${response.slice(0, 200)}...`));
|
|
152
|
-
} else if (response.trim() && !response.includes('"issues"')) {
|
|
153
|
-
console.warn(chalk.yellow(`\nā Warning: Claude response does not contain expected issues format`));
|
|
154
|
-
console.warn(chalk.gray(` Response preview: ${response.slice(0, 200)}...`));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return [];
|
|
158
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { program } from 'commander';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import ora from 'ora';
|
|
6
|
-
import { GitHubClient, parseFiles, parsePRReference } from './github.js';
|
|
7
|
-
import { issuesToReviewComments, generateSummary } from './analyzer.js';
|
|
8
|
-
import { analyzeWithCodex, type CodexOptions } from './codex.js';
|
|
9
|
-
import { analyzeWithClaude, type ClaudeOptions } from './claude.js';
|
|
10
|
-
import { cloneRepo } from './repo.js';
|
|
11
|
-
import { createRequire } from 'module';
|
|
12
|
-
import { execSync } from 'child_process';
|
|
13
|
-
import type { CLIOptions, CodeIssue, LLMProvider } from './types.js';
|
|
14
|
-
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
16
|
-
const { version } = require('../package.json');
|
|
17
|
-
|
|
18
|
-
const VALID_LLM_PROVIDERS = ['codex', 'claude'] as const;
|
|
19
|
-
|
|
20
|
-
program
|
|
21
|
-
.name('nayan-ai')
|
|
22
|
-
.description('AI-powered PR code reviewer using Codex CLI')
|
|
23
|
-
.version(version)
|
|
24
|
-
.argument('<pr-url>', 'GitHub PR URL (e.g., https://github.com/owner/repo/pull/123)')
|
|
25
|
-
.requiredOption('-t, --token <token>', 'GitHub personal access token')
|
|
26
|
-
.option('-v, --verbose', 'Show real-time output from Codex CLI', false)
|
|
27
|
-
.option('-d, --dry', 'Only analyze, don\'t post comments', false)
|
|
28
|
-
.option('-i, --inline', 'Post inline comments instead of summary', false)
|
|
29
|
-
.option('-l, --llm <provider>', 'LLM provider: codex (default) or claude', 'codex')
|
|
30
|
-
.option('--format <format>', 'Output format: text, json', 'text')
|
|
31
|
-
.action(run);
|
|
32
|
-
|
|
33
|
-
program.parse();
|
|
34
|
-
|
|
35
|
-
function checkLLMAvailability(provider: LLMProvider): void {
|
|
36
|
-
if (provider === 'codex') {
|
|
37
|
-
execSync('npx @openai/codex --version', { stdio: 'ignore' });
|
|
38
|
-
} else if (provider === 'claude') {
|
|
39
|
-
execSync('claude --version', { stdio: 'ignore' });
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function run(prUrl: string, options: CLIOptions) {
|
|
44
|
-
try {
|
|
45
|
-
// Validate --llm option
|
|
46
|
-
if (!VALID_LLM_PROVIDERS.includes(options.llm as any)) {
|
|
47
|
-
console.error(chalk.red(`Error: Invalid LLM provider '${options.llm}'. Valid options: ${VALID_LLM_PROVIDERS.join(', ')}`));
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Check if the selected LLM CLI is available
|
|
52
|
-
checkLLMAvailability(options.llm);
|
|
53
|
-
|
|
54
|
-
const prInfo = parsePRReference(prUrl);
|
|
55
|
-
const githubUrl = prInfo.githubUrl;
|
|
56
|
-
|
|
57
|
-
console.log(chalk.bold.blue('\nš¤ Nayan AI - PR Reviewer'));
|
|
58
|
-
console.log('ā'.repeat(40));
|
|
59
|
-
console.log(` Repository: ${chalk.cyan(`${prInfo.owner}/${prInfo.repo}`)}`);
|
|
60
|
-
console.log(` PR Number: ${chalk.cyan(`#${prInfo.number}`)}`);
|
|
61
|
-
if (githubUrl) {
|
|
62
|
-
console.log(` GitHub: ${chalk.cyan(githubUrl)}`);
|
|
63
|
-
}
|
|
64
|
-
if (options.dry) {
|
|
65
|
-
console.log(` Mode: ${chalk.yellow('Dry Run (no comments will be posted)')}`);
|
|
66
|
-
}
|
|
67
|
-
console.log('ā'.repeat(40) + '\n');
|
|
68
|
-
|
|
69
|
-
const github = new GitHubClient(options.token, githubUrl);
|
|
70
|
-
|
|
71
|
-
// Fetch PR details
|
|
72
|
-
let spinner = ora('Fetching PR details...').start();
|
|
73
|
-
const pr = await github.getPullRequest(prInfo);
|
|
74
|
-
spinner.succeed(`PR: ${pr.title}`);
|
|
75
|
-
|
|
76
|
-
// Fetch changed files
|
|
77
|
-
spinner = ora('Fetching changed files...').start();
|
|
78
|
-
const files = await github.getPullRequestFiles(prInfo);
|
|
79
|
-
const fileChanges = parseFiles(files);
|
|
80
|
-
spinner.succeed(`Found ${fileChanges.length} files with changes`);
|
|
81
|
-
|
|
82
|
-
// Clone and analyze with Codex CLI
|
|
83
|
-
spinner = ora('Cloning repository...').start();
|
|
84
|
-
const repo = await cloneRepo(prInfo, options.token, githubUrl);
|
|
85
|
-
|
|
86
|
-
let issues: CodeIssue[];
|
|
87
|
-
try {
|
|
88
|
-
spinner.succeed('Repository cloned');
|
|
89
|
-
|
|
90
|
-
const llmName = options.llm === 'claude' ? 'Claude Code' : 'Codex';
|
|
91
|
-
console.log(chalk.cyan(`Running code review with ${llmName} CLI...\n`));
|
|
92
|
-
|
|
93
|
-
if (options.llm === 'claude') {
|
|
94
|
-
const claudeOpts: ClaudeOptions = { verbose: options.verbose };
|
|
95
|
-
issues = await analyzeWithClaude(repo.path, 'origin/main', claudeOpts);
|
|
96
|
-
} else {
|
|
97
|
-
const codexOpts: CodexOptions = { verbose: options.verbose };
|
|
98
|
-
issues = await analyzeWithCodex(repo.path, 'origin/main', codexOpts);
|
|
99
|
-
}
|
|
100
|
-
console.log(chalk.green(`\nā Analysis complete: ${issues.length} issues found`));
|
|
101
|
-
} finally {
|
|
102
|
-
await repo.cleanup();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Display results
|
|
106
|
-
console.log(chalk.bold('\nš Review Summary'));
|
|
107
|
-
console.log('ā'.repeat(41));
|
|
108
|
-
|
|
109
|
-
if (options.format === 'json') {
|
|
110
|
-
console.log(JSON.stringify(issues, null, 2));
|
|
111
|
-
} else {
|
|
112
|
-
printIssuesSummary(issues);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Post to GitHub
|
|
116
|
-
if (!options.dry) {
|
|
117
|
-
spinner = ora('Posting review to GitHub...').start();
|
|
118
|
-
|
|
119
|
-
if (options.inline) {
|
|
120
|
-
// Post inline comments on specific lines
|
|
121
|
-
const reviewComments = issuesToReviewComments(issues);
|
|
122
|
-
const summary = generateSummary(issues);
|
|
123
|
-
await github.postReview(prInfo, pr.head.sha, summary, reviewComments);
|
|
124
|
-
spinner.succeed('Inline review posted to GitHub');
|
|
125
|
-
} else {
|
|
126
|
-
// Post summary as a PR-level comment (default)
|
|
127
|
-
const summary = generateSummary(issues);
|
|
128
|
-
await github.postComment(prInfo, summary);
|
|
129
|
-
spinner.succeed('Review summary posted to GitHub');
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
console.log(chalk.yellow('\nDry run mode - no comments posted to GitHub'));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
console.log(chalk.green('\nā
Review complete!\n'));
|
|
136
|
-
} catch (error) {
|
|
137
|
-
console.error(chalk.red('\nError:'), error instanceof Error ? error.message : error);
|
|
138
|
-
process.exit(1);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function printIssuesSummary(issues: CodeIssue[]) {
|
|
143
|
-
if (issues.length === 0) {
|
|
144
|
-
console.log(chalk.green(' No issues found! The code looks good.'));
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const bySeverity = {
|
|
149
|
-
error: issues.filter((i) => i.severity === 'error'),
|
|
150
|
-
warning: issues.filter((i) => i.severity === 'warning'),
|
|
151
|
-
info: issues.filter((i) => i.severity === 'info'),
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
// Print errors
|
|
155
|
-
if (bySeverity.error.length > 0) {
|
|
156
|
-
console.log(chalk.red.bold(`\n š“ Errors (${bySeverity.error.length}):`));
|
|
157
|
-
for (const issue of bySeverity.error) {
|
|
158
|
-
const icon = issue.category === 'functionality' ? 'š' : issue.category === 'readability' ? 'š' : 'ā”';
|
|
159
|
-
console.log(chalk.red(` ${icon} ${issue.filename}:${issue.line}`));
|
|
160
|
-
console.log(` ${issue.message}`);
|
|
161
|
-
if (issue.suggestion) {
|
|
162
|
-
console.log(chalk.dim(` š” ${issue.suggestion}`));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Print warnings
|
|
168
|
-
if (bySeverity.warning.length > 0) {
|
|
169
|
-
console.log(chalk.yellow.bold(`\n š” Warnings (${bySeverity.warning.length}):`));
|
|
170
|
-
for (const issue of bySeverity.warning) {
|
|
171
|
-
const icon = issue.category === 'functionality' ? 'š' : issue.category === 'readability' ? 'š' : 'ā”';
|
|
172
|
-
console.log(chalk.yellow(` ${icon} ${issue.filename}:${issue.line}`));
|
|
173
|
-
console.log(` ${issue.message}`);
|
|
174
|
-
if (issue.suggestion) {
|
|
175
|
-
console.log(chalk.dim(` š” ${issue.suggestion}`));
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Print info
|
|
181
|
-
if (bySeverity.info.length > 0) {
|
|
182
|
-
console.log(chalk.blue.bold(`\n šµ Info (${bySeverity.info.length}):`));
|
|
183
|
-
for (const issue of bySeverity.info) {
|
|
184
|
-
const icon = issue.category === 'functionality' ? 'š' : issue.category === 'readability' ? 'š' : 'ā”';
|
|
185
|
-
console.log(chalk.blue(` ${icon} ${issue.filename}:${issue.line}`));
|
|
186
|
-
console.log(` ${issue.message}`);
|
|
187
|
-
if (issue.suggestion) {
|
|
188
|
-
console.log(chalk.dim(` š” ${issue.suggestion}`));
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
package/src/codex.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import type { CodeIssue } from './types.js';
|
|
3
|
-
import { logProcessor, type CodexEvent } from './logs.js';
|
|
4
|
-
import { getReviewPrompt } from './prompt.js';
|
|
5
|
-
|
|
6
|
-
export interface CodexOptions {
|
|
7
|
-
verbose?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function analyzeWithCodex(
|
|
11
|
-
repoPath: string,
|
|
12
|
-
baseBranch: string = 'origin/main',
|
|
13
|
-
options: CodexOptions
|
|
14
|
-
): Promise<CodeIssue[]> {
|
|
15
|
-
const response = await runCodexExec(repoPath, baseBranch, options);
|
|
16
|
-
return parseCodexResponse(response);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function runCodexExec(
|
|
20
|
-
repoPath: string,
|
|
21
|
-
baseBranch: string,
|
|
22
|
-
options: CodexOptions
|
|
23
|
-
): Promise<string> {
|
|
24
|
-
const prompt = getReviewPrompt(baseBranch);
|
|
25
|
-
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
// Use codex exec with --json for structured output
|
|
28
|
-
const args = ['@openai/codex', 'exec', '--json', '--full-auto', prompt];
|
|
29
|
-
|
|
30
|
-
if (options.verbose) {
|
|
31
|
-
console.log(`\n[Codex] Running: npx ${args.join(' ')}`);
|
|
32
|
-
console.log(`[Codex] Working directory: ${repoPath}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const startTime = Date.now();
|
|
36
|
-
|
|
37
|
-
const child = spawn('npx', args, {
|
|
38
|
-
cwd: repoPath,
|
|
39
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
let stdout = '';
|
|
43
|
-
let stderr = '';
|
|
44
|
-
|
|
45
|
-
// Reset log processor for new analysis
|
|
46
|
-
logProcessor.reset();
|
|
47
|
-
|
|
48
|
-
child.stdout.on('data', (data) => {
|
|
49
|
-
const chunk = data.toString();
|
|
50
|
-
stdout += chunk;
|
|
51
|
-
|
|
52
|
-
// Parse JSONL and show progress
|
|
53
|
-
for (const line of chunk.split('\n')) {
|
|
54
|
-
if (!line.trim()) continue;
|
|
55
|
-
try {
|
|
56
|
-
const event: CodexEvent = JSON.parse(line);
|
|
57
|
-
|
|
58
|
-
if (options.verbose) {
|
|
59
|
-
console.log(JSON.stringify(event, null, 2));
|
|
60
|
-
} else {
|
|
61
|
-
// Use log processor for nice interactive output
|
|
62
|
-
logProcessor.processEvent(event);
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
// Not JSON
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
child.stderr.on('data', (data) => {
|
|
71
|
-
const chunk = data.toString();
|
|
72
|
-
stderr += chunk;
|
|
73
|
-
if (options.verbose) {
|
|
74
|
-
process.stderr.write(chunk);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
child.on('close', (code) => {
|
|
79
|
-
const elapsed = Date.now() - startTime;
|
|
80
|
-
|
|
81
|
-
console.log(`\n[Codex] Completed in ${(elapsed / 1000).toFixed(1)}s`);
|
|
82
|
-
|
|
83
|
-
if (code !== 0) {
|
|
84
|
-
const errorMsg = stderr || stdout || 'Unknown error';
|
|
85
|
-
reject(new Error(`Codex review failed (exit ${code}): ${errorMsg.slice(0, 500)}`));
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
resolve(stdout);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
child.on('error', (err) => {
|
|
93
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
94
|
-
reject(new Error('npx not found. Install Node.js/npm (Node 18+) to run nayan-ai.'));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
reject(err);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function parseCodexResponse(response: string): CodeIssue[] {
|
|
103
|
-
// codex exec --json outputs JSONL - parse each line looking for agent_message with issues
|
|
104
|
-
const lines = response.split('\n');
|
|
105
|
-
|
|
106
|
-
for (const line of lines) {
|
|
107
|
-
if (!line.trim()) continue;
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const event = JSON.parse(line);
|
|
111
|
-
|
|
112
|
-
// Look for agent_message in item.completed events
|
|
113
|
-
if (event.type === 'item.completed' && event.item?.type === 'agent_message') {
|
|
114
|
-
const text = event.item.text;
|
|
115
|
-
if (text) {
|
|
116
|
-
// Parse the JSON from the agent's message
|
|
117
|
-
const issuesJson = JSON.parse(text);
|
|
118
|
-
if (issuesJson.issues && Array.isArray(issuesJson.issues)) {
|
|
119
|
-
return issuesJson.issues
|
|
120
|
-
.filter((item: any) => item.message)
|
|
121
|
-
.map((item: any) => ({
|
|
122
|
-
filename: item.filename || 'unknown',
|
|
123
|
-
line: item.line || 0,
|
|
124
|
-
category: item.category || 'functionality',
|
|
125
|
-
severity: item.severity || 'info',
|
|
126
|
-
message: item.message,
|
|
127
|
-
suggestion: item.suggestion,
|
|
128
|
-
}));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
} catch {
|
|
133
|
-
// Not valid JSON, skip
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Fallback: try to find raw JSON in the response
|
|
138
|
-
const jsonMatch = response.match(/\{\s*"issues"\s*:\s*\[[\s\S]*?\]\s*\}/);
|
|
139
|
-
if (jsonMatch) {
|
|
140
|
-
try {
|
|
141
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
142
|
-
return (parsed.issues || [])
|
|
143
|
-
.filter((item: any) => item.message)
|
|
144
|
-
.map((item: any) => ({
|
|
145
|
-
filename: item.filename || 'unknown',
|
|
146
|
-
line: item.line || 0,
|
|
147
|
-
category: item.category || 'functionality',
|
|
148
|
-
severity: item.severity || 'info',
|
|
149
|
-
message: item.message,
|
|
150
|
-
suggestion: item.suggestion,
|
|
151
|
-
}));
|
|
152
|
-
} catch {
|
|
153
|
-
// ignore
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return [];
|
|
158
|
-
}
|
package/src/github.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import type { PRInfo, PullRequest, PullRequestFile, FileChange, ReviewComment } from './types.js';
|
|
2
|
-
|
|
3
|
-
export class GitHubClient {
|
|
4
|
-
private token: string;
|
|
5
|
-
private apiBase: string;
|
|
6
|
-
|
|
7
|
-
constructor(token: string, githubUrl?: string) {
|
|
8
|
-
this.token = token;
|
|
9
|
-
this.apiBase = githubUrl
|
|
10
|
-
? `${githubUrl.replace(/\/$/, '')}/api/v3`
|
|
11
|
-
: 'https://api.github.com';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
private async fetch<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
15
|
-
const url = `${this.apiBase}${endpoint}`;
|
|
16
|
-
const response = await fetch(url, {
|
|
17
|
-
...options,
|
|
18
|
-
headers: {
|
|
19
|
-
'Accept': 'application/vnd.github.v3+json',
|
|
20
|
-
'Authorization': `Bearer ${this.token}`,
|
|
21
|
-
'User-Agent': 'nayan-ai',
|
|
22
|
-
...options.headers,
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
if (!response.ok) {
|
|
27
|
-
const body = await response.text();
|
|
28
|
-
throw new Error(`GitHub API error (${response.status}): ${body}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return response.json() as Promise<T>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async getPullRequest(pr: PRInfo): Promise<PullRequest> {
|
|
35
|
-
return this.fetch<PullRequest>(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async getPullRequestFiles(pr: PRInfo): Promise<PullRequestFile[]> {
|
|
39
|
-
return this.fetch<PullRequestFile[]>(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}/files?per_page=100`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async postReview(
|
|
43
|
-
pr: PRInfo,
|
|
44
|
-
commitId: string,
|
|
45
|
-
body: string,
|
|
46
|
-
comments: ReviewComment[]
|
|
47
|
-
): Promise<void> {
|
|
48
|
-
await this.fetch(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}/reviews`, {
|
|
49
|
-
method: 'POST',
|
|
50
|
-
headers: { 'Content-Type': 'application/json' },
|
|
51
|
-
body: JSON.stringify({
|
|
52
|
-
commit_id: commitId,
|
|
53
|
-
body,
|
|
54
|
-
event: 'COMMENT',
|
|
55
|
-
comments,
|
|
56
|
-
}),
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async postComment(pr: PRInfo, body: string): Promise<void> {
|
|
61
|
-
await this.fetch(`/repos/${pr.owner}/${pr.repo}/issues/${pr.number}/comments`, {
|
|
62
|
-
method: 'POST',
|
|
63
|
-
headers: { 'Content-Type': 'application/json' },
|
|
64
|
-
body: JSON.stringify({ body }),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function parseFiles(files: PullRequestFile[]): FileChange[] {
|
|
70
|
-
return files
|
|
71
|
-
.filter((f): f is PullRequestFile & { patch: string } => !!f.patch)
|
|
72
|
-
.map((f) => ({
|
|
73
|
-
filename: f.filename,
|
|
74
|
-
patch: f.patch,
|
|
75
|
-
}));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function parsePRReference(input: string): PRInfo & { githubUrl?: string } {
|
|
79
|
-
// Full URL: https://github.com/owner/repo/pull/123 or https://enterprise.example.com/owner/repo/pull/123
|
|
80
|
-
const urlMatch = input.match(/^(https?:\/\/[^/]+)\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
81
|
-
if (urlMatch) {
|
|
82
|
-
const baseUrl = urlMatch[1];
|
|
83
|
-
// Only github.com (exactly) is public GitHub, everything else is enterprise
|
|
84
|
-
const isEnterprise = !baseUrl.match(/^https?:\/\/(www\.)?github\.com$/i);
|
|
85
|
-
return {
|
|
86
|
-
owner: urlMatch[2],
|
|
87
|
-
repo: urlMatch[3],
|
|
88
|
-
number: parseInt(urlMatch[4], 10),
|
|
89
|
-
githubUrl: isEnterprise ? baseUrl : undefined,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Short reference: owner/repo#123
|
|
94
|
-
const shortMatch = input.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
95
|
-
if (shortMatch) {
|
|
96
|
-
return {
|
|
97
|
-
owner: shortMatch[1],
|
|
98
|
-
repo: shortMatch[2],
|
|
99
|
-
number: parseInt(shortMatch[3], 10),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
throw new Error(
|
|
104
|
-
'Invalid PR reference. Use:\n' +
|
|
105
|
-
' - https://github.com/owner/repo/pull/123\n' +
|
|
106
|
-
' - owner/repo#123'
|
|
107
|
-
);
|
|
108
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { GitHubClient, parseFiles, parsePRReference } from './github.js';
|
|
2
|
-
export { analyzeWithCodex } from './codex.js';
|
|
3
|
-
export { analyzeWithClaude } from './claude.js';
|
|
4
|
-
export { issuesToReviewComments, generateSummary } from './analyzer.js';
|
|
5
|
-
export { cloneRepo, getGitDiff, getChangedFiles } from './repo.js';
|
|
6
|
-
export type * from './types.js';
|
package/src/logs.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import ora, { type Ora } from 'ora';
|
|
3
|
-
|
|
4
|
-
export interface CodexEvent {
|
|
5
|
-
type: string;
|
|
6
|
-
item?: {
|
|
7
|
-
id?: string;
|
|
8
|
-
type?: string;
|
|
9
|
-
text?: string;
|
|
10
|
-
command?: string;
|
|
11
|
-
aggregated_output?: string;
|
|
12
|
-
exit_code?: number | null;
|
|
13
|
-
status?: string;
|
|
14
|
-
};
|
|
15
|
-
thread_id?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class LogProcessor {
|
|
19
|
-
private seenFiles = new Set<string>();
|
|
20
|
-
private currentPhase = '';
|
|
21
|
-
private currentSpinner: Ora | null = null;
|
|
22
|
-
private fileCount = 0;
|
|
23
|
-
private analyzedCount = 0;
|
|
24
|
-
|
|
25
|
-
processEvent(event: CodexEvent): void {
|
|
26
|
-
switch (event.type) {
|
|
27
|
-
case 'thread.started':
|
|
28
|
-
this.startSpinner('Starting analysis...');
|
|
29
|
-
break;
|
|
30
|
-
|
|
31
|
-
case 'turn.started':
|
|
32
|
-
// Silent - just marks the start of a turn
|
|
33
|
-
break;
|
|
34
|
-
|
|
35
|
-
case 'item.completed':
|
|
36
|
-
this.handleItemCompleted(event);
|
|
37
|
-
break;
|
|
38
|
-
|
|
39
|
-
case 'item.started':
|
|
40
|
-
this.handleItemStarted(event);
|
|
41
|
-
break;
|
|
42
|
-
|
|
43
|
-
case 'turn.completed':
|
|
44
|
-
// Analysis complete - handled elsewhere
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private startSpinner(text: string): void {
|
|
50
|
-
this.stopSpinner();
|
|
51
|
-
this.currentSpinner = ora({
|
|
52
|
-
text: chalk.cyan(text),
|
|
53
|
-
prefixText: ' ',
|
|
54
|
-
spinner: 'dots'
|
|
55
|
-
}).start();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private succeedSpinner(text?: string): void {
|
|
59
|
-
if (this.currentSpinner) {
|
|
60
|
-
this.currentSpinner.succeed(text ? chalk.green(text) : undefined);
|
|
61
|
-
this.currentSpinner = null;
|
|
62
|
-
} else if (text) {
|
|
63
|
-
console.log(chalk.green(` ā ${text}`));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private stopSpinner(): void {
|
|
68
|
-
if (this.currentSpinner) {
|
|
69
|
-
this.currentSpinner.stop();
|
|
70
|
-
this.currentSpinner = null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
private logAndSpin(text: string): void {
|
|
75
|
-
// First, succeed the current spinner to print it
|
|
76
|
-
if (this.currentSpinner) {
|
|
77
|
-
this.currentSpinner.succeed();
|
|
78
|
-
}
|
|
79
|
-
// Then start a new spinner with the new text
|
|
80
|
-
this.currentSpinner = ora({
|
|
81
|
-
text: chalk.cyan(text),
|
|
82
|
-
prefixText: ' ',
|
|
83
|
-
spinner: 'dots'
|
|
84
|
-
}).start();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private handleItemStarted(event: CodexEvent): void {
|
|
88
|
-
const item = event.item;
|
|
89
|
-
if (!item) return;
|
|
90
|
-
|
|
91
|
-
if (item.type === 'command_execution' && item.command) {
|
|
92
|
-
const cmd = item.command;
|
|
93
|
-
|
|
94
|
-
// Git diff command
|
|
95
|
-
if (cmd.includes('git diff')) {
|
|
96
|
-
this.setPhase('diff');
|
|
97
|
-
this.logAndSpin('Checking differences...');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private handleItemCompleted(event: CodexEvent): void {
|
|
103
|
-
const item = event.item;
|
|
104
|
-
if (!item) return;
|
|
105
|
-
|
|
106
|
-
if (item.type === 'reasoning' && item.text) {
|
|
107
|
-
this.handleReasoning(item.text);
|
|
108
|
-
} else if (item.type === 'command_execution') {
|
|
109
|
-
this.handleCommandCompleted(item);
|
|
110
|
-
} else if (item.type === 'agent_message') {
|
|
111
|
-
this.succeedSpinner('Analysis complete');
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
private handleReasoning(text: string): void {
|
|
116
|
-
// Clean up the reasoning text - remove ** markers
|
|
117
|
-
const cleanText = text.replace(/\*\*/g, '').trim();
|
|
118
|
-
|
|
119
|
-
if (!cleanText) return;
|
|
120
|
-
|
|
121
|
-
// Split into title (first line/sentence) and details
|
|
122
|
-
const lines = cleanText.split('\n').filter(Boolean);
|
|
123
|
-
|
|
124
|
-
// Check if it's a short title or has additional content
|
|
125
|
-
if (lines.length === 1 && cleanText.length < 80) {
|
|
126
|
-
// Short reasoning - just show as spinner
|
|
127
|
-
this.logAndSpin(`š ${cleanText}`);
|
|
128
|
-
} else {
|
|
129
|
-
// Long reasoning - extract title and show details as bullet points
|
|
130
|
-
let title = lines[0];
|
|
131
|
-
let details = '';
|
|
132
|
-
|
|
133
|
-
// If single line but long, split at first semicolon or period after 40 chars
|
|
134
|
-
if (lines.length === 1) {
|
|
135
|
-
const splitMatch = cleanText.match(/^(.{30,80}?[;.])\s*(.+)$/);
|
|
136
|
-
if (splitMatch) {
|
|
137
|
-
title = splitMatch[1];
|
|
138
|
-
details = splitMatch[2];
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
details = lines.slice(1).join(' ');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Show title with spinner
|
|
145
|
-
this.logAndSpin(`š ${title}`);
|
|
146
|
-
|
|
147
|
-
// If there are details, print them as indented bullet points
|
|
148
|
-
if (details) {
|
|
149
|
-
// Succeed current spinner first
|
|
150
|
-
if (this.currentSpinner) {
|
|
151
|
-
this.currentSpinner.succeed();
|
|
152
|
-
this.currentSpinner = null;
|
|
153
|
-
}
|
|
154
|
-
// Print details indented
|
|
155
|
-
const detailLines = this.wrapText(details, 70);
|
|
156
|
-
detailLines.forEach(line => {
|
|
157
|
-
console.log(chalk.dim(` ${line}`));
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
private wrapText(text: string, maxWidth: number): string[] {
|
|
164
|
-
const words = text.split(' ');
|
|
165
|
-
const lines: string[] = [];
|
|
166
|
-
let currentLine = '';
|
|
167
|
-
|
|
168
|
-
for (const word of words) {
|
|
169
|
-
if (currentLine.length + word.length + 1 <= maxWidth) {
|
|
170
|
-
currentLine += (currentLine ? ' ' : '') + word;
|
|
171
|
-
} else {
|
|
172
|
-
if (currentLine) lines.push(currentLine);
|
|
173
|
-
currentLine = word;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (currentLine) lines.push(currentLine);
|
|
177
|
-
return lines;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
private handleCommandCompleted(item: CodexEvent['item']): void {
|
|
181
|
-
if (!item) return;
|
|
182
|
-
|
|
183
|
-
const cmd = item.command || '';
|
|
184
|
-
const output = item.aggregated_output || '';
|
|
185
|
-
|
|
186
|
-
// Git diff completed - show changed files count
|
|
187
|
-
if (cmd.includes('git diff') && output) {
|
|
188
|
-
const diffFiles = output.match(/diff --git a\/([^\s]+)/g);
|
|
189
|
-
if (diffFiles && diffFiles.length > 0) {
|
|
190
|
-
this.fileCount = diffFiles.length;
|
|
191
|
-
this.succeedSpinner(`Found ${diffFiles.length} changed files`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// File listing with rg --files
|
|
196
|
-
if (cmd.includes('rg --files') && output) {
|
|
197
|
-
const files = output.trim().split('\n').filter(Boolean);
|
|
198
|
-
if (files.length > 0) {
|
|
199
|
-
this.succeedSpinner(`Identified ${files.length} files to analyze`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// File reading commands - extract and show file path
|
|
204
|
-
const fileMatch = this.extractFilePath(cmd);
|
|
205
|
-
if (fileMatch && !this.seenFiles.has(fileMatch)) {
|
|
206
|
-
this.seenFiles.add(fileMatch);
|
|
207
|
-
this.analyzedCount++;
|
|
208
|
-
this.logAndSpin(`š Analyzing ${fileMatch}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private extractFilePath(cmd: string): string | null {
|
|
213
|
-
// Skip git commands - they're not file reads
|
|
214
|
-
if (cmd.includes('git ')) return null;
|
|
215
|
-
|
|
216
|
-
// Match patterns like:
|
|
217
|
-
// sed -n '1,260p' path/to/file.tsx
|
|
218
|
-
// nl -ba path/to/file.ts
|
|
219
|
-
// cat path/to/file.ts
|
|
220
|
-
const patterns = [
|
|
221
|
-
/sed\s+-n\s+'[^']+'\s+([^\s|]+\.[a-z]{1,4})/i,
|
|
222
|
-
/nl\s+-ba\s+([^\s|]+\.[a-z]{1,4})/i,
|
|
223
|
-
/cat\s+([^\s|]+\.[a-z]{1,4})/i,
|
|
224
|
-
];
|
|
225
|
-
|
|
226
|
-
for (const pattern of patterns) {
|
|
227
|
-
const match = cmd.match(pattern);
|
|
228
|
-
if (match && match[1]) {
|
|
229
|
-
// Skip if it's a glob pattern or doesn't look like a real file
|
|
230
|
-
if (match[1].includes('*')) continue;
|
|
231
|
-
// Must have a proper file extension (1-4 chars)
|
|
232
|
-
if (!/\.[a-z]{1,4}$/i.test(match[1])) continue;
|
|
233
|
-
return match[1];
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private setPhase(phase: string): void {
|
|
241
|
-
this.currentPhase = phase;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
reset(): void {
|
|
245
|
-
this.stopSpinner();
|
|
246
|
-
this.seenFiles.clear();
|
|
247
|
-
this.currentPhase = '';
|
|
248
|
-
this.fileCount = 0;
|
|
249
|
-
this.analyzedCount = 0;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export const logProcessor = new LogProcessor();
|
package/src/prompt.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
export function getReviewPrompt(baseBranch: string): string {
|
|
2
|
-
return `You are an expert code reviewer. Perform a DEEP, comprehensive review of ALL code changes in this PR.
|
|
3
|
-
|
|
4
|
-
STEP 1: Run "git diff ${baseBranch}...HEAD" to identify all changed files.
|
|
5
|
-
|
|
6
|
-
STEP 2: For EACH changed file (skip lock files like yarn.lock, package-lock.json, bun.lock):
|
|
7
|
-
- Read the FULL file to understand the complete context
|
|
8
|
-
- Understand how the changes interact with existing code
|
|
9
|
-
- Check imports, exports, and dependencies affected by the change
|
|
10
|
-
- Analyze the entire function/class that contains the change
|
|
11
|
-
- Look for issues that may not be visible in the diff alone
|
|
12
|
-
|
|
13
|
-
STEP 3: Perform DEEP analysis - check each change for these categories:
|
|
14
|
-
|
|
15
|
-
**BUGS & LOGIC ERRORS:**
|
|
16
|
-
- Off-by-one errors in loops/arrays
|
|
17
|
-
- Null/undefined access without checks
|
|
18
|
-
- Race conditions in async code
|
|
19
|
-
- Incorrect boolean logic or conditions
|
|
20
|
-
- Wrong variable usage or typos
|
|
21
|
-
- Infinite loops or recursion without exit
|
|
22
|
-
- Array mutation while iterating
|
|
23
|
-
|
|
24
|
-
**ERROR HANDLING:**
|
|
25
|
-
- Missing try/catch for async operations
|
|
26
|
-
- Unhandled promise rejections
|
|
27
|
-
- Missing error states in UI
|
|
28
|
-
- Silent failures that should be logged
|
|
29
|
-
|
|
30
|
-
**SECURITY:**
|
|
31
|
-
- SQL injection, XSS vulnerabilities
|
|
32
|
-
- Hardcoded secrets or credentials
|
|
33
|
-
- Unsafe user input handling
|
|
34
|
-
- Missing authentication/authorization checks
|
|
35
|
-
|
|
36
|
-
**PERFORMANCE:**
|
|
37
|
-
- Unnecessary re-renders or computations
|
|
38
|
-
- Missing memoization where needed
|
|
39
|
-
- N+1 query patterns
|
|
40
|
-
- Memory leaks (event listeners, subscriptions not cleaned up)
|
|
41
|
-
- Large objects in state that should be normalized
|
|
42
|
-
|
|
43
|
-
**TYPE SAFETY:**
|
|
44
|
-
- Any type usage that should be specific
|
|
45
|
-
- Missing null checks for optional values
|
|
46
|
-
- Type assertions that could fail at runtime
|
|
47
|
-
|
|
48
|
-
**EDGE CASES:**
|
|
49
|
-
- Empty arrays/objects not handled
|
|
50
|
-
- Boundary conditions (0, negative, max values)
|
|
51
|
-
- Network failures not handled
|
|
52
|
-
- Loading/error states missing
|
|
53
|
-
|
|
54
|
-
**CODE QUALITY:**
|
|
55
|
-
- Dead code or unused variables
|
|
56
|
-
- Duplicated logic that should be extracted
|
|
57
|
-
- Overly complex functions that should be split
|
|
58
|
-
- Missing cleanup in useEffect/subscriptions
|
|
59
|
-
|
|
60
|
-
**TEST COVERAGE:**
|
|
61
|
-
- Check if tests are added for new functionality
|
|
62
|
-
- Check if existing tests need to be updated for changed code
|
|
63
|
-
- Identify critical logic that should have unit tests
|
|
64
|
-
- Flag functions with complex branching that need test coverage
|
|
65
|
-
- Check if edge cases in the code have corresponding test cases
|
|
66
|
-
|
|
67
|
-
STEP 4: Output ALL issues as JSON (be thorough - report every issue you find):
|
|
68
|
-
{"issues":[{"filename":"<path>","line":<num>,"category":"functionality"|"performance"|"readability","severity":"error"|"warning"|"info","message":"<description>","suggestion":"<fix>"}]}
|
|
69
|
-
|
|
70
|
-
**SEVERITY GUIDELINES (be consistent):**
|
|
71
|
-
- **error**: Will cause bugs, crashes, security vulnerabilities, or data loss. Examples: null pointer access, unhandled exceptions, SQL injection, race conditions, infinite loops.
|
|
72
|
-
- **warning**: Potential problems that may cause issues in edge cases or under certain conditions. Examples: missing error handling, possible memory leaks, missing null checks for optional values, performance issues.
|
|
73
|
-
- **info**: Code quality improvements, best practices, style suggestions. Examples: dead code, code duplication, missing tests, readability improvements.
|
|
74
|
-
|
|
75
|
-
CRITICAL: Review EVERY changed file thoroughly. Read the FULL file for each change to understand complete context. Do not skip any file. Be thorough - quality over speed.
|
|
76
|
-
If truly no issues after deep analysis, return {"issues":[]}`;
|
|
77
|
-
}
|
package/src/repo.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { mkdtemp, rm } from 'fs/promises';
|
|
3
|
-
import { tmpdir } from 'os';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import type { PRInfo } from './types.js';
|
|
6
|
-
|
|
7
|
-
export interface ClonedRepo {
|
|
8
|
-
path: string;
|
|
9
|
-
cleanup: () => Promise<void>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function cloneRepo(
|
|
13
|
-
prInfo: PRInfo,
|
|
14
|
-
token: string,
|
|
15
|
-
githubUrl?: string
|
|
16
|
-
): Promise<ClonedRepo> {
|
|
17
|
-
const tempDir = await mkdtemp(join(tmpdir(), 'nayan-ai-'));
|
|
18
|
-
|
|
19
|
-
const baseUrl = githubUrl?.replace(/\/$/, '') || 'https://github.com';
|
|
20
|
-
const cloneUrl = `https://${token}@${baseUrl.replace(/^https?:\/\//, '')}/${prInfo.owner}/${prInfo.repo}.git`;
|
|
21
|
-
|
|
22
|
-
// Clone with enough depth to have main branch history
|
|
23
|
-
await runGit(['clone', '--depth', '100', cloneUrl, tempDir]);
|
|
24
|
-
// Fetch the main branch explicitly for comparison
|
|
25
|
-
await runGit(['fetch', 'origin', 'main:refs/remotes/origin/main', '--depth', '100'], tempDir);
|
|
26
|
-
// Fetch the PR branch
|
|
27
|
-
await runGit(['fetch', 'origin', `pull/${prInfo.number}/head:pr-branch`], tempDir);
|
|
28
|
-
await runGit(['checkout', 'pr-branch'], tempDir);
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
path: tempDir,
|
|
32
|
-
cleanup: async () => {
|
|
33
|
-
try {
|
|
34
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
35
|
-
} catch {
|
|
36
|
-
// ignore cleanup errors
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function getGitDiff(repoPath: string): Promise<string> {
|
|
43
|
-
return runGit(['diff', 'origin/main...HEAD', '--unified=3'], repoPath);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function getChangedFiles(repoPath: string): Promise<string[]> {
|
|
47
|
-
const output = await runGit(['diff', 'origin/main...HEAD', '--name-only'], repoPath);
|
|
48
|
-
return output.split('\n').filter(Boolean);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function runGit(args: string[], cwd?: string): Promise<string> {
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
const child = spawn('git', args, {
|
|
54
|
-
cwd,
|
|
55
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
let stdout = '';
|
|
59
|
-
let stderr = '';
|
|
60
|
-
|
|
61
|
-
child.stdout.on('data', (data) => {
|
|
62
|
-
stdout += data.toString();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
child.stderr.on('data', (data) => {
|
|
66
|
-
stderr += data.toString();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
child.on('close', (code) => {
|
|
70
|
-
if (code !== 0) {
|
|
71
|
-
reject(new Error(`git ${args[0]} failed: ${stderr || stdout || 'Unknown error'}`));
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
resolve(stdout);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
child.on('error', (err) => {
|
|
78
|
-
reject(err);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
export interface PRInfo {
|
|
2
|
-
owner: string;
|
|
3
|
-
repo: string;
|
|
4
|
-
number: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface PullRequest {
|
|
8
|
-
number: number;
|
|
9
|
-
title: string;
|
|
10
|
-
head: {
|
|
11
|
-
sha: string;
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface PullRequestFile {
|
|
16
|
-
filename: string;
|
|
17
|
-
status: string;
|
|
18
|
-
patch?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface FileChange {
|
|
22
|
-
filename: string;
|
|
23
|
-
patch: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface CodeIssue {
|
|
27
|
-
filename: string;
|
|
28
|
-
line: number;
|
|
29
|
-
category: 'functionality' | 'readability' | 'performance';
|
|
30
|
-
severity: 'error' | 'warning' | 'info';
|
|
31
|
-
message: string;
|
|
32
|
-
suggestion?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ReviewComment {
|
|
36
|
-
path: string;
|
|
37
|
-
line: number;
|
|
38
|
-
side: 'RIGHT';
|
|
39
|
-
body: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export type LLMProvider = 'codex' | 'claude';
|
|
43
|
-
|
|
44
|
-
export interface CLIOptions {
|
|
45
|
-
token: string;
|
|
46
|
-
verbose: boolean;
|
|
47
|
-
dry: boolean;
|
|
48
|
-
inline: boolean;
|
|
49
|
-
format: string;
|
|
50
|
-
llm: LLMProvider;
|
|
51
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "./src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"declaration": true,
|
|
15
|
-
"sourceMap": true
|
|
16
|
-
},
|
|
17
|
-
"include": ["src/**/*"],
|
|
18
|
-
"exclude": ["node_modules", "dist"]
|
|
19
|
-
}
|