Skip to content

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.ts

How 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:

  1. Read code files and load any screenshots as data URLs
  2. Send content to GPT-4o with vision enabled
  3. LLM analyzes and calls report_review tool with findings
  4. Parse structured output (issues, severity, line numbers)
  5. Display formatted report with quality score

Usage

Review a Single File

bash
npm run recipe:code-review-bot -- src/utils.ts

Review Multiple Files

bash
npm run recipe:code-review-bot -- src/index.ts src/utils.ts src/types.ts

Review 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.png

The 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 Good

Tool 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

LevelDescription
errorCritical issues that must be fixed (bugs, security)
warningIssues that should be addressed (performance, maintainability)
infoSuggestions for improvement (style, best practices)

Categories

CategoryIconDescription
bugBUGLogic errors, null references, race conditions
securitySECInjection, XSS, authentication issues
performancePERFInefficient algorithms, memory leaks
styleSTYLENaming, 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

ExtensionLanguage
.ts, .tsxTypeScript
.js, .jsxJavaScript
.pyPython
.javaJava
.goGo
.rsRust
.c, .hC
.cpp, .hppC++
.csC#
.rbRuby
.phpPHP
.swiftSwift
.ktKotlin
.scalaScala
.vueVue
.svelteSvelte

Configuration

ConstantDefaultDescription
MODELgpt-4oOpenAI model (needs vision support)
MAX_FILE_SIZE100KBMaximum file size to review

Full Source

View on GitHub

Released under the MIT License.