nayan-ai 1.0.0-beta.2 → 1.0.0-beta.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nayan-ai",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "AI-powered PR code reviewer CLI using Codex CLI agent",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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
- }