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.