Code Review Bot
An AI-powered code reviewer that analyzes files and provides structured feedback.
Features
- Vision Support - Analyze screenshots alongside code
- Structured Output - Issues reported with severity, category, and line numbers
- Tool-Based - Uses function calling for consistent JSON output
- Multi-File - Review individual files or entire directories
- Streaming - Real-time analysis feedback
Quick Start
bash
export OPENAI_API_KEY=your_key
npm run recipe:code-review-bot -- src/utils.tsHow It Works
This recipe creates an AI code reviewer using GPT-4o's vision capabilities to analyze both code and screenshots, outputting structured feedback via tool calling.
Capabilities:
- Analyze source code files for issues
- Review UI screenshots alongside code
- Output structured JSON with severity levels
Flow:
- Read code files and load any screenshots as data URLs
- Send content to GPT-4o with vision enabled
- LLM analyzes and calls
report_reviewtool with findings - Parse structured output (issues, severity, line numbers)
- Display formatted report with quality score
Usage
Review a Single File
bash
npm run recipe:code-review-bot -- src/utils.tsReview Multiple Files
bash
npm run recipe:code-review-bot -- src/index.ts src/utils.ts src/types.tsReview a Directory
bash
npm run recipe:code-review-bot -- src/Recursively finds all supported code files (skips node_modules, dist, build, vendor).
Review with Screenshot
bash
npm run recipe:code-review-bot -- src/app.tsx --screenshot ui.pngThe screenshot is analyzed alongside the code for UI/UX issues.
Example Output
╭─────────────────────────────────────────────╮
│ Code Review Bot │
│ Powered by Meloqui + GPT-4o │
╰─────────────────────────────────────────────╯
Found 3 file(s) to review
Analyzing code...
Thinking: Let me analyze these files for potential issues...
Issues Found:
[ERROR] [SEC] src/auth.ts:45
SQL query constructed with string concatenation, vulnerable to injection
Suggestion: Use parameterized queries with prepared statements
[WARNING] [PERF] src/utils.ts:23
Array.find() called inside loop creates O(n²) complexity
Suggestion: Convert to Map lookup before the loop
[INFO] [STYLE] src/index.ts:12
Magic number 86400 should be a named constant
Suggestion: Define const SECONDS_PER_DAY = 86400
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Review Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Overall the code is well-structured with good separation of concerns.
The main issues are a SQL injection vulnerability that should be fixed
immediately and some performance optimizations for the utility functions.
Issues Found: 3
Errors: 1
Warnings: 1
Info: 1
Code Quality Score: 72/100 GoodTool Schema
The bot uses a report_review tool for structured output:
typescript
interface FileIssue {
file: string; // File path
line: number; // Line number
severity: 'error' | 'warning' | 'info';
category: 'bug' | 'style' | 'security' | 'performance';
message: string; // Issue description
suggestion: string; // How to fix
}
interface ReviewResult {
issues: FileIssue[];
summary: string; // 2-3 sentence overview
score: number; // 0-100 quality score
}Severity Levels
| Level | Description |
|---|---|
error | Critical issues that must be fixed (bugs, security) |
warning | Issues that should be addressed (performance, maintainability) |
info | Suggestions for improvement (style, best practices) |
Categories
| Category | Icon | Description |
|---|---|---|
bug | BUG | Logic errors, null references, race conditions |
security | SEC | Injection, XSS, authentication issues |
performance | PERF | Inefficient algorithms, memory leaks |
style | STYLE | Naming, formatting, code organization |
Code Walkthrough
Tool Registration
typescript
/**
* Code Review Bot Library
*
* Exported functions for the Code Review Bot recipe.
* Snippet markers allow VitePress to extract code for documentation.
*/
import * as fs from 'fs';
import * as path from 'path';
import {
ChatClient,
ToolRegistry,
loadImageAsDataUrl,
ContentPart,
SUPPORTED_IMAGE_EXTENSIONS,
ChatError
} from '../../../src';
// [start:config]
export const MODEL = 'gpt-4o';
export const MAX_FILE_SIZE = 100_000; // 100KB per file
export const MAX_FILES = 50; // Maximum files to review
export const SUPPORTED_CODE_EXTENSIONS = [
'.ts',
'.tsx',
'.js',
'.jsx',
'.py',
'.java',
'.go',
'.rs',
'.c',
'.cpp',
'.h',
'.hpp',
'.cs',
'.rb',
'.php',
'.swift',
'.kt',
'.scala',
'.vue',
'.svelte'
];
export const SKIP_DIRECTORIES = ['node_modules', 'dist', 'build', 'vendor', '.git'];
// [end:config]
// [start:colors]
export const colors = {
reset: '\x1b[0m',
dim: '\x1b[2m',
bold: '\x1b[1m',
red: '\x1b[31m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
green: '\x1b[32m',
magenta: '\x1b[35m'
};
export const severityColors: Record<string, string> = {
error: colors.red,
warning: colors.yellow,
info: colors.blue
};
export const categoryIcons: Record<string, string> = {
bug: 'BUG',
style: 'STYLE',
security: 'SEC',
performance: 'PERF'
};
// [end:colors]
// [start:types]
export interface FileIssue {
file: string;
line: number;
severity: 'error' | 'warning' | 'info';
category: 'bug' | 'style' | 'security' | 'performance';
message: string;
suggestion: string;
}
export interface ReviewResult {
issues: FileIssue[];
summary: string;
score: number;
}
export interface CodeFile {
path: string;
content: string;
language: string;
}
// [end:types]
// [start:file-utils]
export function getLanguageFromExtension(ext: string): string {
const languageMap: Record<string, string> = {
'.ts': 'typescript',
'.tsx': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.py': 'python',
'.java': 'java',
'.go': 'go',
'.rs': 'rust',
'.c': 'c',
'.cpp': 'cpp',
'.h': 'c',
'.hpp': 'cpp',
'.cs': 'csharp',
'.rb': 'ruby',
'.php': 'php',
'.swift': 'swift',
'.kt': 'kotlin',
'.scala': 'scala',
'.vue': 'vue',
'.svelte': 'svelte'
};
return languageMap[ext] || 'unknown';
}
export function addFileIfSupported(filePath: string, files: CodeFile[]): void {
const ext = path.extname(filePath).toLowerCase();
if (!SUPPORTED_CODE_EXTENSIONS.includes(ext)) {
return;
}
const content = fs.readFileSync(filePath, 'utf-8');
if (content.length <= MAX_FILE_SIZE) {
files.push({
path: filePath,
content,
language: getLanguageFromExtension(ext)
});
} else {
console.warn(
`${colors.yellow}Warning: Skipping ${filePath} (exceeds ${MAX_FILE_SIZE} bytes)${colors.reset}`
);
}
}
export function collectFiles(inputPath: string, visited: Set<string> = new Set()): CodeFile[] {
const files: CodeFile[] = [];
const realPath = fs.realpathSync(inputPath);
if (visited.has(realPath)) {
return files;
}
visited.add(realPath);
const stat = fs.statSync(inputPath);
if (stat.isDirectory()) {
const entries = fs.readdirSync(inputPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
if (SKIP_DIRECTORIES.includes(entry.name)) continue;
const fullPath = path.join(inputPath, entry.name);
if (entry.isDirectory() || entry.isSymbolicLink()) {
try {
const targetStat = fs.statSync(fullPath);
if (targetStat.isDirectory()) {
files.push(...collectFiles(fullPath, visited));
} else if (targetStat.isFile()) {
addFileIfSupported(fullPath, files);
}
} catch {
// Skip broken symlinks
}
} else if (entry.isFile()) {
addFileIfSupported(fullPath, files);
}
}
} else if (stat.isFile()) {
addFileIfSupported(inputPath, files);
}
return files;
}
// [end:file-utils]
// [start:formatting]
export function formatIssue(issue: FileIssue): string {
const sevColor = severityColors[issue.severity] || colors.reset;
const catIcon = categoryIcons[issue.category] || issue.category.toUpperCase();
const relativePath = path.relative(process.cwd(), issue.file);
return `${sevColor}[${issue.severity.toUpperCase()}]${colors.reset} ${colors.dim}[${catIcon}]${colors.reset} ${relativePath}:${issue.line}
${issue.message}
${colors.green}Suggestion:${colors.reset} ${issue.suggestion}`;
}
export function getScoreDisplay(score: number): string {
if (score >= 90) return `${colors.green}${score}/100 Excellent${colors.reset}`;
if (score >= 70) return `${colors.cyan}${score}/100 Good${colors.reset}`;
if (score >= 50) return `${colors.yellow}${score}/100 Needs Improvement${colors.reset}`;
return `${colors.red}${score}/100 Poor${colors.reset}`;
}
export function printSummary(result: ReviewResult): void {
const errorCount = result.issues.filter((i) => i.severity === 'error').length;
const warningCount = result.issues.filter((i) => i.severity === 'warning').length;
const infoCount = result.issues.filter((i) => i.severity === 'info').length;
console.log(`
${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
${colors.bold}Review Summary${colors.reset}
${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
${result.summary}
${colors.bold}Issues Found:${colors.reset} ${result.issues.length}
${colors.red}Errors:${colors.reset} ${errorCount}
${colors.yellow}Warnings:${colors.reset} ${warningCount}
${colors.blue}Info:${colors.reset} ${infoCount}
${colors.bold}Code Quality Score:${colors.reset} ${getScoreDisplay(result.score)}
`);
}
export function printBanner(): void {
console.log(`
${colors.cyan}╭─────────────────────────────────────────────╮
│ ${colors.reset}${colors.bold}Code Review Bot${colors.reset}${colors.cyan} │
│ ${colors.dim}Powered by Meloqui + GPT-4o${colors.reset}${colors.cyan} │
╰─────────────────────────────────────────────╯${colors.reset}
`);
}
// [end:formatting]
// [start:tool-registration]
export function createReviewToolRegistry(onResult: (result: ReviewResult) => void): ToolRegistry {
const registry = new ToolRegistry();
registry.registerTool(
'report_review',
async (args: ReviewResult) => {
onResult(args);
return { success: true, issueCount: args.issues.length };
},
{
description:
'Report the code review results with structured issues, summary, and quality score',
parameters: {
type: 'object',
properties: {
issues: {
type: 'array',
description: 'List of issues found in the code',
items: {
type: 'object',
properties: {
file: { type: 'string', description: 'File path where the issue was found' },
line: { type: 'number', description: 'Line number where the issue occurs' },
severity: {
type: 'string',
enum: ['error', 'warning', 'info'],
description: 'Severity level of the issue'
},
category: {
type: 'string',
enum: ['bug', 'style', 'security', 'performance'],
description: 'Category of the issue'
},
message: { type: 'string', description: 'Description of the issue' },
suggestion: { type: 'string', description: 'How to fix the issue' }
},
required: ['file', 'line', 'severity', 'category', 'message', 'suggestion']
}
},
summary: {
type: 'string',
description: 'Brief overall summary of the code quality (2-3 sentences)'
},
score: { type: 'number', description: 'Code quality score from 0-100' }
},
required: ['issues', 'summary', 'score']
}
}
);
return registry;
}
// [end:tool-registration]
// [start:review-prompt]
export const SYSTEM_PROMPT = `You are an expert code reviewer. Analyze the provided code files and report issues using the report_review tool.
Review for:
- Bugs and logical errors
- Security vulnerabilities (injection, XSS, etc.)
- Performance issues
- Code style and best practices
Be thorough but fair. Only report genuine issues, not stylistic preferences unless they impact readability significantly.
For each issue, provide:
- Exact file path and line number
- Clear description of the problem
- Actionable suggestion to fix it
After reviewing, call the report_review tool with your findings.`;
// [end:review-prompt]
// [start:create-client]
export function createReviewClient(
apiKey: string,
onResult: (result: ReviewResult) => void
): ChatClient {
const registry = createReviewToolRegistry(onResult);
return new ChatClient({
provider: 'openai',
model: MODEL,
apiKey,
tools: registry,
systemPrompt: SYSTEM_PROMPT
});
}
// [end:create-client]
// [start:build-content]
export function buildCodeBlocks(codeFiles: CodeFile[]): string {
return codeFiles
.map((f) => `### ${f.path}\n\`\`\`${f.language}\n${f.content}\n\`\`\``)
.join('\n\n');
}
export async function buildContentParts(
codeBlocks: string,
screenshot?: string
): Promise<ContentPart[]> {
const contentParts: ContentPart[] = [
{
type: 'text',
text: `Please review the following code files:\n\n${codeBlocks}`
}
];
if (screenshot) {
if (!fs.existsSync(screenshot)) {
throw new Error(`Screenshot not found: ${screenshot}`);
}
const ext = path.extname(screenshot).toLowerCase().slice(1);
if (!SUPPORTED_IMAGE_EXTENSIONS.includes(ext as (typeof SUPPORTED_IMAGE_EXTENSIONS)[number])) {
throw new Error(`Unsupported image format. Use: ${SUPPORTED_IMAGE_EXTENSIONS.join(', ')}`);
}
const imageDataUrl = await loadImageAsDataUrl(screenshot);
contentParts.push({
type: 'image',
image: imageDataUrl
});
contentParts[0] = {
type: 'text',
text: `Please review the following code files and the attached UI screenshot:\n\n${codeBlocks}\n\nAlso review the UI screenshot for any visual issues, accessibility concerns, or UX problems.`
};
}
return contentParts;
}
// [end:build-content]
// [start:streaming]
export async function streamReview(
client: ChatClient,
contentParts: ContentPart[],
onChunk: (content: string) => void
): Promise<void> {
for await (const chunk of client.stream({ role: 'user', content: contentParts })) {
if (chunk.content) {
onChunk(chunk.content);
}
}
}
// [end:streaming]
// [start:error-handling]
export function handleError(error: unknown): string {
if (error instanceof ChatError) {
return `Error: ${error.message}`;
} else if (error instanceof Error) {
return `Error: ${error.message}`;
} else {
return 'An unexpected error occurred';
}
}
// [end:error-handling]
// [start:argument-parsing]
export function parseArguments(args: string[]): { files: string[]; screenshot?: string } {
const files: string[] = [];
let screenshot: string | undefined;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--screenshot' || args[i] === '-s') {
screenshot = args[++i];
} else if (!args[i].startsWith('-')) {
files.push(args[i]);
}
}
return { files, screenshot };
}
// [end:argument-parsing]Vision Integration
typescript
/**
* Code Review Bot Library
*
* Exported functions for the Code Review Bot recipe.
* Snippet markers allow VitePress to extract code for documentation.
*/
import * as fs from 'fs';
import * as path from 'path';
import {
ChatClient,
ToolRegistry,
loadImageAsDataUrl,
ContentPart,
SUPPORTED_IMAGE_EXTENSIONS,
ChatError
} from '../../../src';
// [start:config]
export const MODEL = 'gpt-4o';
export const MAX_FILE_SIZE = 100_000; // 100KB per file
export const MAX_FILES = 50; // Maximum files to review
export const SUPPORTED_CODE_EXTENSIONS = [
'.ts',
'.tsx',
'.js',
'.jsx',
'.py',
'.java',
'.go',
'.rs',
'.c',
'.cpp',
'.h',
'.hpp',
'.cs',
'.rb',
'.php',
'.swift',
'.kt',
'.scala',
'.vue',
'.svelte'
];
export const SKIP_DIRECTORIES = ['node_modules', 'dist', 'build', 'vendor', '.git'];
// [end:config]
// [start:colors]
export const colors = {
reset: '\x1b[0m',
dim: '\x1b[2m',
bold: '\x1b[1m',
red: '\x1b[31m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
green: '\x1b[32m',
magenta: '\x1b[35m'
};
export const severityColors: Record<string, string> = {
error: colors.red,
warning: colors.yellow,
info: colors.blue
};
export const categoryIcons: Record<string, string> = {
bug: 'BUG',
style: 'STYLE',
security: 'SEC',
performance: 'PERF'
};
// [end:colors]
// [start:types]
export interface FileIssue {
file: string;
line: number;
severity: 'error' | 'warning' | 'info';
category: 'bug' | 'style' | 'security' | 'performance';
message: string;
suggestion: string;
}
export interface ReviewResult {
issues: FileIssue[];
summary: string;
score: number;
}
export interface CodeFile {
path: string;
content: string;
language: string;
}
// [end:types]
// [start:file-utils]
export function getLanguageFromExtension(ext: string): string {
const languageMap: Record<string, string> = {
'.ts': 'typescript',
'.tsx': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.py': 'python',
'.java': 'java',
'.go': 'go',
'.rs': 'rust',
'.c': 'c',
'.cpp': 'cpp',
'.h': 'c',
'.hpp': 'cpp',
'.cs': 'csharp',
'.rb': 'ruby',
'.php': 'php',
'.swift': 'swift',
'.kt': 'kotlin',
'.scala': 'scala',
'.vue': 'vue',
'.svelte': 'svelte'
};
return languageMap[ext] || 'unknown';
}
export function addFileIfSupported(filePath: string, files: CodeFile[]): void {
const ext = path.extname(filePath).toLowerCase();
if (!SUPPORTED_CODE_EXTENSIONS.includes(ext)) {
return;
}
const content = fs.readFileSync(filePath, 'utf-8');
if (content.length <= MAX_FILE_SIZE) {
files.push({
path: filePath,
content,
language: getLanguageFromExtension(ext)
});
} else {
console.warn(
`${colors.yellow}Warning: Skipping ${filePath} (exceeds ${MAX_FILE_SIZE} bytes)${colors.reset}`
);
}
}
export function collectFiles(inputPath: string, visited: Set<string> = new Set()): CodeFile[] {
const files: CodeFile[] = [];
const realPath = fs.realpathSync(inputPath);
if (visited.has(realPath)) {
return files;
}
visited.add(realPath);
const stat = fs.statSync(inputPath);
if (stat.isDirectory()) {
const entries = fs.readdirSync(inputPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
if (SKIP_DIRECTORIES.includes(entry.name)) continue;
const fullPath = path.join(inputPath, entry.name);
if (entry.isDirectory() || entry.isSymbolicLink()) {
try {
const targetStat = fs.statSync(fullPath);
if (targetStat.isDirectory()) {
files.push(...collectFiles(fullPath, visited));
} else if (targetStat.isFile()) {
addFileIfSupported(fullPath, files);
}
} catch {
// Skip broken symlinks
}
} else if (entry.isFile()) {
addFileIfSupported(fullPath, files);
}
}
} else if (stat.isFile()) {
addFileIfSupported(inputPath, files);
}
return files;
}
// [end:file-utils]
// [start:formatting]
export function formatIssue(issue: FileIssue): string {
const sevColor = severityColors[issue.severity] || colors.reset;
const catIcon = categoryIcons[issue.category] || issue.category.toUpperCase();
const relativePath = path.relative(process.cwd(), issue.file);
return `${sevColor}[${issue.severity.toUpperCase()}]${colors.reset} ${colors.dim}[${catIcon}]${colors.reset} ${relativePath}:${issue.line}
${issue.message}
${colors.green}Suggestion:${colors.reset} ${issue.suggestion}`;
}
export function getScoreDisplay(score: number): string {
if (score >= 90) return `${colors.green}${score}/100 Excellent${colors.reset}`;
if (score >= 70) return `${colors.cyan}${score}/100 Good${colors.reset}`;
if (score >= 50) return `${colors.yellow}${score}/100 Needs Improvement${colors.reset}`;
return `${colors.red}${score}/100 Poor${colors.reset}`;
}
export function printSummary(result: ReviewResult): void {
const errorCount = result.issues.filter((i) => i.severity === 'error').length;
const warningCount = result.issues.filter((i) => i.severity === 'warning').length;
const infoCount = result.issues.filter((i) => i.severity === 'info').length;
console.log(`
${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
${colors.bold}Review Summary${colors.reset}
${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
${result.summary}
${colors.bold}Issues Found:${colors.reset} ${result.issues.length}
${colors.red}Errors:${colors.reset} ${errorCount}
${colors.yellow}Warnings:${colors.reset} ${warningCount}
${colors.blue}Info:${colors.reset} ${infoCount}
${colors.bold}Code Quality Score:${colors.reset} ${getScoreDisplay(result.score)}
`);
}
export function printBanner(): void {
console.log(`
${colors.cyan}╭─────────────────────────────────────────────╮
│ ${colors.reset}${colors.bold}Code Review Bot${colors.reset}${colors.cyan} │
│ ${colors.dim}Powered by Meloqui + GPT-4o${colors.reset}${colors.cyan} │
╰─────────────────────────────────────────────╯${colors.reset}
`);
}
// [end:formatting]
// [start:tool-registration]
export function createReviewToolRegistry(onResult: (result: ReviewResult) => void): ToolRegistry {
const registry = new ToolRegistry();
registry.registerTool(
'report_review',
async (args: ReviewResult) => {
onResult(args);
return { success: true, issueCount: args.issues.length };
},
{
description:
'Report the code review results with structured issues, summary, and quality score',
parameters: {
type: 'object',
properties: {
issues: {
type: 'array',
description: 'List of issues found in the code',
items: {
type: 'object',
properties: {
file: { type: 'string', description: 'File path where the issue was found' },
line: { type: 'number', description: 'Line number where the issue occurs' },
severity: {
type: 'string',
enum: ['error', 'warning', 'info'],
description: 'Severity level of the issue'
},
category: {
type: 'string',
enum: ['bug', 'style', 'security', 'performance'],
description: 'Category of the issue'
},
message: { type: 'string', description: 'Description of the issue' },
suggestion: { type: 'string', description: 'How to fix the issue' }
},
required: ['file', 'line', 'severity', 'category', 'message', 'suggestion']
}
},
summary: {
type: 'string',
description: 'Brief overall summary of the code quality (2-3 sentences)'
},
score: { type: 'number', description: 'Code quality score from 0-100' }
},
required: ['issues', 'summary', 'score']
}
}
);
return registry;
}
// [end:tool-registration]
// [start:review-prompt]
export const SYSTEM_PROMPT = `You are an expert code reviewer. Analyze the provided code files and report issues using the report_review tool.
Review for:
- Bugs and logical errors
- Security vulnerabilities (injection, XSS, etc.)
- Performance issues
- Code style and best practices
Be thorough but fair. Only report genuine issues, not stylistic preferences unless they impact readability significantly.
For each issue, provide:
- Exact file path and line number
- Clear description of the problem
- Actionable suggestion to fix it
After reviewing, call the report_review tool with your findings.`;
// [end:review-prompt]
// [start:create-client]
export function createReviewClient(
apiKey: string,
onResult: (result: ReviewResult) => void
): ChatClient {
const registry = createReviewToolRegistry(onResult);
return new ChatClient({
provider: 'openai',
model: MODEL,
apiKey,
tools: registry,
systemPrompt: SYSTEM_PROMPT
});
}
// [end:create-client]
// [start:build-content]
export function buildCodeBlocks(codeFiles: CodeFile[]): string {
return codeFiles
.map((f) => `### ${f.path}\n\`\`\`${f.language}\n${f.content}\n\`\`\``)
.join('\n\n');
}
export async function buildContentParts(
codeBlocks: string,
screenshot?: string
): Promise<ContentPart[]> {
const contentParts: ContentPart[] = [
{
type: 'text',
text: `Please review the following code files:\n\n${codeBlocks}`
}
];
if (screenshot) {
if (!fs.existsSync(screenshot)) {
throw new Error(`Screenshot not found: ${screenshot}`);
}
const ext = path.extname(screenshot).toLowerCase().slice(1);
if (!SUPPORTED_IMAGE_EXTENSIONS.includes(ext as (typeof SUPPORTED_IMAGE_EXTENSIONS)[number])) {
throw new Error(`Unsupported image format. Use: ${SUPPORTED_IMAGE_EXTENSIONS.join(', ')}`);
}
const imageDataUrl = await loadImageAsDataUrl(screenshot);
contentParts.push({
type: 'image',
image: imageDataUrl
});
contentParts[0] = {
type: 'text',
text: `Please review the following code files and the attached UI screenshot:\n\n${codeBlocks}\n\nAlso review the UI screenshot for any visual issues, accessibility concerns, or UX problems.`
};
}
return contentParts;
}
// [end:build-content]
// [start:streaming]
export async function streamReview(
client: ChatClient,
contentParts: ContentPart[],
onChunk: (content: string) => void
): Promise<void> {
for await (const chunk of client.stream({ role: 'user', content: contentParts })) {
if (chunk.content) {
onChunk(chunk.content);
}
}
}
// [end:streaming]
// [start:error-handling]
export function handleError(error: unknown): string {
if (error instanceof ChatError) {
return `Error: ${error.message}`;
} else if (error instanceof Error) {
return `Error: ${error.message}`;
} else {
return 'An unexpected error occurred';
}
}
// [end:error-handling]
// [start:argument-parsing]
export function parseArguments(args: string[]): { files: string[]; screenshot?: string } {
const files: string[] = [];
let screenshot: string | undefined;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--screenshot' || args[i] === '-s') {
screenshot = args[++i];
} else if (!args[i].startsWith('-')) {
files.push(args[i]);
}
}
return { files, screenshot };
}
// [end:argument-parsing]Streaming Analysis
typescript
/**
* Code Review Bot Library
*
* Exported functions for the Code Review Bot recipe.
* Snippet markers allow VitePress to extract code for documentation.
*/
import * as fs from 'fs';
import * as path from 'path';
import {
ChatClient,
ToolRegistry,
loadImageAsDataUrl,
ContentPart,
SUPPORTED_IMAGE_EXTENSIONS,
ChatError
} from '../../../src';
// [start:config]
export const MODEL = 'gpt-4o';
export const MAX_FILE_SIZE = 100_000; // 100KB per file
export const MAX_FILES = 50; // Maximum files to review
export const SUPPORTED_CODE_EXTENSIONS = [
'.ts',
'.tsx',
'.js',
'.jsx',
'.py',
'.java',
'.go',
'.rs',
'.c',
'.cpp',
'.h',
'.hpp',
'.cs',
'.rb',
'.php',
'.swift',
'.kt',
'.scala',
'.vue',
'.svelte'
];
export const SKIP_DIRECTORIES = ['node_modules', 'dist', 'build', 'vendor', '.git'];
// [end:config]
// [start:colors]
export const colors = {
reset: '\x1b[0m',
dim: '\x1b[2m',
bold: '\x1b[1m',
red: '\x1b[31m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
green: '\x1b[32m',
magenta: '\x1b[35m'
};
export const severityColors: Record<string, string> = {
error: colors.red,
warning: colors.yellow,
info: colors.blue
};
export const categoryIcons: Record<string, string> = {
bug: 'BUG',
style: 'STYLE',
security: 'SEC',
performance: 'PERF'
};
// [end:colors]
// [start:types]
export interface FileIssue {
file: string;
line: number;
severity: 'error' | 'warning' | 'info';
category: 'bug' | 'style' | 'security' | 'performance';
message: string;
suggestion: string;
}
export interface ReviewResult {
issues: FileIssue[];
summary: string;
score: number;
}
export interface CodeFile {
path: string;
content: string;
language: string;
}
// [end:types]
// [start:file-utils]
export function getLanguageFromExtension(ext: string): string {
const languageMap: Record<string, string> = {
'.ts': 'typescript',
'.tsx': 'typescript',
'.js': 'javascript',
'.jsx': 'javascript',
'.py': 'python',
'.java': 'java',
'.go': 'go',
'.rs': 'rust',
'.c': 'c',
'.cpp': 'cpp',
'.h': 'c',
'.hpp': 'cpp',
'.cs': 'csharp',
'.rb': 'ruby',
'.php': 'php',
'.swift': 'swift',
'.kt': 'kotlin',
'.scala': 'scala',
'.vue': 'vue',
'.svelte': 'svelte'
};
return languageMap[ext] || 'unknown';
}
export function addFileIfSupported(filePath: string, files: CodeFile[]): void {
const ext = path.extname(filePath).toLowerCase();
if (!SUPPORTED_CODE_EXTENSIONS.includes(ext)) {
return;
}
const content = fs.readFileSync(filePath, 'utf-8');
if (content.length <= MAX_FILE_SIZE) {
files.push({
path: filePath,
content,
language: getLanguageFromExtension(ext)
});
} else {
console.warn(
`${colors.yellow}Warning: Skipping ${filePath} (exceeds ${MAX_FILE_SIZE} bytes)${colors.reset}`
);
}
}
export function collectFiles(inputPath: string, visited: Set<string> = new Set()): CodeFile[] {
const files: CodeFile[] = [];
const realPath = fs.realpathSync(inputPath);
if (visited.has(realPath)) {
return files;
}
visited.add(realPath);
const stat = fs.statSync(inputPath);
if (stat.isDirectory()) {
const entries = fs.readdirSync(inputPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
if (SKIP_DIRECTORIES.includes(entry.name)) continue;
const fullPath = path.join(inputPath, entry.name);
if (entry.isDirectory() || entry.isSymbolicLink()) {
try {
const targetStat = fs.statSync(fullPath);
if (targetStat.isDirectory()) {
files.push(...collectFiles(fullPath, visited));
} else if (targetStat.isFile()) {
addFileIfSupported(fullPath, files);
}
} catch {
// Skip broken symlinks
}
} else if (entry.isFile()) {
addFileIfSupported(fullPath, files);
}
}
} else if (stat.isFile()) {
addFileIfSupported(inputPath, files);
}
return files;
}
// [end:file-utils]
// [start:formatting]
export function formatIssue(issue: FileIssue): string {
const sevColor = severityColors[issue.severity] || colors.reset;
const catIcon = categoryIcons[issue.category] || issue.category.toUpperCase();
const relativePath = path.relative(process.cwd(), issue.file);
return `${sevColor}[${issue.severity.toUpperCase()}]${colors.reset} ${colors.dim}[${catIcon}]${colors.reset} ${relativePath}:${issue.line}
${issue.message}
${colors.green}Suggestion:${colors.reset} ${issue.suggestion}`;
}
export function getScoreDisplay(score: number): string {
if (score >= 90) return `${colors.green}${score}/100 Excellent${colors.reset}`;
if (score >= 70) return `${colors.cyan}${score}/100 Good${colors.reset}`;
if (score >= 50) return `${colors.yellow}${score}/100 Needs Improvement${colors.reset}`;
return `${colors.red}${score}/100 Poor${colors.reset}`;
}
export function printSummary(result: ReviewResult): void {
const errorCount = result.issues.filter((i) => i.severity === 'error').length;
const warningCount = result.issues.filter((i) => i.severity === 'warning').length;
const infoCount = result.issues.filter((i) => i.severity === 'info').length;
console.log(`
${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
${colors.bold}Review Summary${colors.reset}
${colors.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}
${result.summary}
${colors.bold}Issues Found:${colors.reset} ${result.issues.length}
${colors.red}Errors:${colors.reset} ${errorCount}
${colors.yellow}Warnings:${colors.reset} ${warningCount}
${colors.blue}Info:${colors.reset} ${infoCount}
${colors.bold}Code Quality Score:${colors.reset} ${getScoreDisplay(result.score)}
`);
}
export function printBanner(): void {
console.log(`
${colors.cyan}╭─────────────────────────────────────────────╮
│ ${colors.reset}${colors.bold}Code Review Bot${colors.reset}${colors.cyan} │
│ ${colors.dim}Powered by Meloqui + GPT-4o${colors.reset}${colors.cyan} │
╰─────────────────────────────────────────────╯${colors.reset}
`);
}
// [end:formatting]
// [start:tool-registration]
export function createReviewToolRegistry(onResult: (result: ReviewResult) => void): ToolRegistry {
const registry = new ToolRegistry();
registry.registerTool(
'report_review',
async (args: ReviewResult) => {
onResult(args);
return { success: true, issueCount: args.issues.length };
},
{
description:
'Report the code review results with structured issues, summary, and quality score',
parameters: {
type: 'object',
properties: {
issues: {
type: 'array',
description: 'List of issues found in the code',
items: {
type: 'object',
properties: {
file: { type: 'string', description: 'File path where the issue was found' },
line: { type: 'number', description: 'Line number where the issue occurs' },
severity: {
type: 'string',
enum: ['error', 'warning', 'info'],
description: 'Severity level of the issue'
},
category: {
type: 'string',
enum: ['bug', 'style', 'security', 'performance'],
description: 'Category of the issue'
},
message: { type: 'string', description: 'Description of the issue' },
suggestion: { type: 'string', description: 'How to fix the issue' }
},
required: ['file', 'line', 'severity', 'category', 'message', 'suggestion']
}
},
summary: {
type: 'string',
description: 'Brief overall summary of the code quality (2-3 sentences)'
},
score: { type: 'number', description: 'Code quality score from 0-100' }
},
required: ['issues', 'summary', 'score']
}
}
);
return registry;
}
// [end:tool-registration]
// [start:review-prompt]
export const SYSTEM_PROMPT = `You are an expert code reviewer. Analyze the provided code files and report issues using the report_review tool.
Review for:
- Bugs and logical errors
- Security vulnerabilities (injection, XSS, etc.)
- Performance issues
- Code style and best practices
Be thorough but fair. Only report genuine issues, not stylistic preferences unless they impact readability significantly.
For each issue, provide:
- Exact file path and line number
- Clear description of the problem
- Actionable suggestion to fix it
After reviewing, call the report_review tool with your findings.`;
// [end:review-prompt]
// [start:create-client]
export function createReviewClient(
apiKey: string,
onResult: (result: ReviewResult) => void
): ChatClient {
const registry = createReviewToolRegistry(onResult);
return new ChatClient({
provider: 'openai',
model: MODEL,
apiKey,
tools: registry,
systemPrompt: SYSTEM_PROMPT
});
}
// [end:create-client]
// [start:build-content]
export function buildCodeBlocks(codeFiles: CodeFile[]): string {
return codeFiles
.map((f) => `### ${f.path}\n\`\`\`${f.language}\n${f.content}\n\`\`\``)
.join('\n\n');
}
export async function buildContentParts(
codeBlocks: string,
screenshot?: string
): Promise<ContentPart[]> {
const contentParts: ContentPart[] = [
{
type: 'text',
text: `Please review the following code files:\n\n${codeBlocks}`
}
];
if (screenshot) {
if (!fs.existsSync(screenshot)) {
throw new Error(`Screenshot not found: ${screenshot}`);
}
const ext = path.extname(screenshot).toLowerCase().slice(1);
if (!SUPPORTED_IMAGE_EXTENSIONS.includes(ext as (typeof SUPPORTED_IMAGE_EXTENSIONS)[number])) {
throw new Error(`Unsupported image format. Use: ${SUPPORTED_IMAGE_EXTENSIONS.join(', ')}`);
}
const imageDataUrl = await loadImageAsDataUrl(screenshot);
contentParts.push({
type: 'image',
image: imageDataUrl
});
contentParts[0] = {
type: 'text',
text: `Please review the following code files and the attached UI screenshot:\n\n${codeBlocks}\n\nAlso review the UI screenshot for any visual issues, accessibility concerns, or UX problems.`
};
}
return contentParts;
}
// [end:build-content]
// [start:streaming]
export async function streamReview(
client: ChatClient,
contentParts: ContentPart[],
onChunk: (content: string) => void
): Promise<void> {
for await (const chunk of client.stream({ role: 'user', content: contentParts })) {
if (chunk.content) {
onChunk(chunk.content);
}
}
}
// [end:streaming]
// [start:error-handling]
export function handleError(error: unknown): string {
if (error instanceof ChatError) {
return `Error: ${error.message}`;
} else if (error instanceof Error) {
return `Error: ${error.message}`;
} else {
return 'An unexpected error occurred';
}
}
// [end:error-handling]
// [start:argument-parsing]
export function parseArguments(args: string[]): { files: string[]; screenshot?: string } {
const files: string[] = [];
let screenshot: string | undefined;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--screenshot' || args[i] === '-s') {
screenshot = args[++i];
} else if (!args[i].startsWith('-')) {
files.push(args[i]);
}
}
return { files, screenshot };
}
// [end:argument-parsing]Supported File Types
| Extension | Language |
|---|---|
.ts, .tsx | TypeScript |
.js, .jsx | JavaScript |
.py | Python |
.java | Java |
.go | Go |
.rs | Rust |
.c, .h | C |
.cpp, .hpp | C++ |
.cs | C# |
.rb | Ruby |
.php | PHP |
.swift | Swift |
.kt | Kotlin |
.scala | Scala |
.vue | Vue |
.svelte | Svelte |
Configuration
| Constant | Default | Description |
|---|---|---|
MODEL | gpt-4o | OpenAI model (needs vision support) |
MAX_FILE_SIZE | 100KB | Maximum file size to review |
