UNPKG

@bobmatnyc/ai-code-review

Version:

A TypeScript-based tool for automated code reviews using AI models from Google Gemini, Anthropic Claude, and OpenRouter

1,430 lines (1,417 loc) 1.29 MB
#!/usr/bin/env node "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // src/utils/logger.ts function getCurrentLogLevel() { const shouldLog = process.argv.includes("--trace-logger") && !isInitializing; if (shouldLog) { console.error(`Debug: getCurrentLogLevel called, AI_CODE_REVIEW_LOG_LEVEL=${process.env.AI_CODE_REVIEW_LOG_LEVEL}`); } if (process.argv.includes("--debug")) { if (shouldLog) { console.error("Debug: Debug flag found in process.argv, forcing DEBUG level"); } return 0 /* DEBUG */; } const envLogLevel = process.env.AI_CODE_REVIEW_LOG_LEVEL?.toLowerCase(); if (envLogLevel) { if (shouldLog) { console.error(`Debug: Found AI_CODE_REVIEW_LOG_LEVEL environment variable: ${envLogLevel}`); } if (envLogLevel in LOG_LEVEL_MAP) { if (shouldLog) { console.error(`Debug: Mapped log level ${envLogLevel} -> ${LOG_LEVEL_MAP[envLogLevel]}`); } return LOG_LEVEL_MAP[envLogLevel]; } else if (shouldLog) { console.error(`Debug: Invalid log level: ${envLogLevel}, valid options are: ${Object.keys(LOG_LEVEL_MAP).join(", ")}`); } } else if (shouldLog) { console.error("Debug: AI_CODE_REVIEW_LOG_LEVEL environment variable not found"); } if (shouldLog) { console.error("Debug: No valid log level found, defaulting to INFO"); } return 1 /* INFO */; } function setLogLevel(level) { const shouldLog = process.argv.includes("--trace-logger"); if (shouldLog) { console.error(`Debug: setLogLevel called with ${level}`); } if (typeof level === "string") { const levelLower = level.toLowerCase(); if (levelLower in LOG_LEVEL_MAP) { currentLogLevel = LOG_LEVEL_MAP[levelLower]; if (shouldLog) { console.error(`Debug: Log level set to ${levelLower} -> ${currentLogLevel}`); } } else { console.warn(`Invalid log level: ${level}. Using default.`); } } else { currentLogLevel = level; if (shouldLog) { console.error(`Debug: Log level set to numeric value ${level}`); } } } function getLogLevel() { return currentLogLevel; } function formatLogMessage(level, message) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const levelUpper = level.toUpperCase().padEnd(5); return `${COLORS.time}[${timestamp}]${COLORS.reset} ${COLORS[level]}${levelUpper}${COLORS.reset} ${message}`; } function log(level, levelName, message, ...args) { if (level >= currentLogLevel) { const formattedMessage = formatLogMessage(levelName, message); switch (level) { case 0 /* DEBUG */: console.debug(formattedMessage, ...args); break; case 1 /* INFO */: console.log(formattedMessage, ...args); break; case 2 /* WARN */: console.warn(formattedMessage, ...args); break; case 3 /* ERROR */: console.error(formattedMessage, ...args); break; } } else if (level === 0 /* DEBUG */ && process.argv.includes("--trace-logger")) { console.error(`Suppressing DEBUG log because currentLogLevel=${currentLogLevel}, message was: ${message}`); } } function debug(message, ...args) { log(0 /* DEBUG */, "debug", message, ...args); } function info(message, ...args) { log(1 /* INFO */, "info", message, ...args); } function warn(message, ...args) { log(2 /* WARN */, "warn", message, ...args); } function error(message, ...args) { log(3 /* ERROR */, "error", message, ...args); } function createLogger(prefix) { return { debug: (message, ...args) => debug(`[${prefix}] ${message}`, ...args), info: (message, ...args) => info(`[${prefix}] ${message}`, ...args), warn: (message, ...args) => warn(`[${prefix}] ${message}`, ...args), error: (message, ...args) => error(`[${prefix}] ${message}`, ...args) }; } var LogLevel, LOG_LEVEL_MAP, COLORS, isInitializing, currentLogLevel, logger_default; var init_logger = __esm({ "src/utils/logger.ts"() { "use strict"; LogLevel = /* @__PURE__ */ ((LogLevel3) => { LogLevel3[LogLevel3["DEBUG"] = 0] = "DEBUG"; LogLevel3[LogLevel3["INFO"] = 1] = "INFO"; LogLevel3[LogLevel3["WARN"] = 2] = "WARN"; LogLevel3[LogLevel3["ERROR"] = 3] = "ERROR"; LogLevel3[LogLevel3["NONE"] = 4] = "NONE"; return LogLevel3; })(LogLevel || {}); LOG_LEVEL_MAP = { debug: 0 /* DEBUG */, info: 1 /* INFO */, warn: 2 /* WARN */, error: 3 /* ERROR */, none: 4 /* NONE */ }; COLORS = { reset: "\x1B[0m", dim: "\x1B[2m", bright: "\x1B[1m", debug: "\x1B[36m", // Cyan info: "\x1B[32m", // Green warn: "\x1B[33m", // Yellow error: "\x1B[31m", // Red time: "\x1B[90m" // Gray }; isInitializing = false; currentLogLevel = getCurrentLogLevel(); logger_default = { debug, info, warn, error, setLogLevel, getLogLevel, createLogger, LogLevel }; } }); // src/utils/envLoader.ts var envLoader_exports = {}; __export(envLoader_exports, { getAnthropicApiKey: () => getAnthropicApiKey, getGoogleApiKey: () => getGoogleApiKey, getOpenAIApiKey: () => getOpenAIApiKey, getOpenRouterApiKey: () => getOpenRouterApiKey, loadEnvVariables: () => loadEnvVariables, validateRequiredEnvVars: () => validateRequiredEnvVars }); function debugLog(message) { if (process.argv.includes("--debug") || process.env.AI_CODE_REVIEW_LOG_LEVEL?.toLowerCase() === "debug") { console.log(`\x1B[36m[DEBUG:ENV]\x1B[0m ${message}`); } } function traceEnvVarLoading(message) { console.log(`\x1B[35m[ENV-TRACE]\x1B[0m ${message}`); } async function loadEnvVariables(envFilePath) { try { let envLocalPath; if (envFilePath) { envLocalPath = envFilePath; } else { const projectEnvLocal = path.resolve(process.cwd(), ".env.local"); const projectEnv = path.resolve(process.cwd(), ".env"); try { await import_promises.default.access(projectEnvLocal); envLocalPath = projectEnvLocal; debugLog(`Found project-level .env.local: ${projectEnvLocal}`); } catch { try { await import_promises.default.access(projectEnv); envLocalPath = projectEnv; debugLog(`Found project-level .env: ${projectEnv}`); } catch { const possibleToolDirectories2 = [ path.resolve(__dirname, "..", ".."), // Local development or npm link path.resolve(__dirname, "..", "..", ".."), // Global npm installation "/opt/homebrew/lib/node_modules/@bobmatnyc/ai-code-review" // Homebrew global installation ]; if (process.env.AI_CODE_REVIEW_DIR) { possibleToolDirectories2.unshift(process.env.AI_CODE_REVIEW_DIR); debugLog(`Using tool directory from AI_CODE_REVIEW_DIR: ${process.env.AI_CODE_REVIEW_DIR}`); } envLocalPath = projectEnvLocal; for (const dir of possibleToolDirectories2) { const potentialEnvPath = path.resolve(dir, ".env.local"); debugLog(`Checking for tool .env.local in: ${potentialEnvPath}`); try { await import_promises.default.access(potentialEnvPath); envLocalPath = potentialEnvPath; debugLog(`Found .env.local in tool directory: ${potentialEnvPath}`); break; } catch (statError) { debugLog(`No .env.local in ${potentialEnvPath}`); } } } } } try { await import_promises.default.access(envLocalPath); } catch (error2) { const errorMessage = error2 instanceof Error ? error2.message : "Unknown error"; traceEnvVarLoading(`Environment file not found: ${envLocalPath} (${errorMessage}). Continuing without it.`); return { success: true, message: `No .env.local file found. You may need to set API keys via environment variables or command line options.`, envFile: envLocalPath }; } traceEnvVarLoading(`Attempting to load environment variables from: ${envLocalPath}`); const beforeModel = process.env.AI_CODE_REVIEW_MODEL; const result = dotenv.config({ path: envLocalPath, override: true }); if (beforeModel !== process.env.AI_CODE_REVIEW_MODEL) { traceEnvVarLoading(`AI_CODE_REVIEW_MODEL changed from '${beforeModel}' to '${process.env.AI_CODE_REVIEW_MODEL}'`); } if (result.error) { traceEnvVarLoading(`Error loading environment variables: ${result.error.message}`); return { success: false, message: `Error loading environment variables: ${result.error.message}`, envFile: envLocalPath }; } traceEnvVarLoading(`Successfully loaded environment variables from ${envLocalPath}`); const envVarNames = Object.keys(result.parsed || {}); if (envVarNames.length > 0) { traceEnvVarLoading("Variables found in .env.local (names only):"); if (envVarNames.includes("AI_CODE_REVIEW_LOG_LEVEL")) { traceEnvVarLoading(`AI_CODE_REVIEW_LOG_LEVEL is set to: ${process.env.AI_CODE_REVIEW_LOG_LEVEL}`); } else { traceEnvVarLoading("AI_CODE_REVIEW_LOG_LEVEL is NOT present in .env.local"); } debugLog(envVarNames.join(", ")); } else { traceEnvVarLoading("No variables found in .env.local"); } return { success: true, message: `Successfully loaded environment variables from ${envLocalPath}`, envFile: envLocalPath }; } catch (error2) { const errorMessage = error2 instanceof Error ? error2.message : String(error2); console.error(`Error loading environment variables: ${errorMessage}`); return { success: false, message: `Unexpected error loading environment variables: ${errorMessage}`, envFile: envFilePath }; } } function getGoogleApiKey() { const apiKeyNew = process.env.AI_CODE_REVIEW_GOOGLE_API_KEY; const apiKeyLegacy = process.env.CODE_REVIEW_GOOGLE_API_KEY; const apiKeyGenAI = process.env.GOOGLE_GENERATIVE_AI_KEY; const apiKeyStudio = process.env.GOOGLE_AI_STUDIO_KEY; if (apiKeyNew) { debugLog("Google API key found: AI_CODE_REVIEW_GOOGLE_API_KEY"); return { apiKey: apiKeyNew, source: "AI_CODE_REVIEW_GOOGLE_API_KEY", message: "Using AI_CODE_REVIEW_GOOGLE_API_KEY" }; } if (apiKeyLegacy) { console.warn( "Warning: Using deprecated environment variable CODE_REVIEW_GOOGLE_API_KEY. Please switch to AI_CODE_REVIEW_GOOGLE_API_KEY." ); debugLog("Google API key found: CODE_REVIEW_GOOGLE_API_KEY (deprecated)"); return { apiKey: apiKeyLegacy, source: "CODE_REVIEW_GOOGLE_API_KEY", message: "Using deprecated CODE_REVIEW_GOOGLE_API_KEY" }; } if (apiKeyGenAI) { console.warn( "Warning: Using generic environment variable GOOGLE_GENERATIVE_AI_KEY. Consider using AI_CODE_REVIEW_GOOGLE_API_KEY for better isolation." ); debugLog("Google API key found: GOOGLE_GENERATIVE_AI_KEY"); return { apiKey: apiKeyGenAI, source: "GOOGLE_GENERATIVE_AI_KEY", message: "Using GOOGLE_GENERATIVE_AI_KEY" }; } if (apiKeyStudio) { console.warn( "Warning: Using deprecated environment variable GOOGLE_AI_STUDIO_KEY. Please switch to AI_CODE_REVIEW_GOOGLE_API_KEY." ); debugLog("Google API key found: GOOGLE_AI_STUDIO_KEY (deprecated)"); return { apiKey: apiKeyStudio, source: "GOOGLE_AI_STUDIO_KEY", message: "Using deprecated GOOGLE_AI_STUDIO_KEY" }; } return { apiKey: void 0, source: "none", message: "No Google API key found. Please set AI_CODE_REVIEW_GOOGLE_API_KEY in your .env.local file." }; } function getOpenRouterApiKey() { const apiKeyNew = process.env.AI_CODE_REVIEW_OPENROUTER_API_KEY; const apiKeyLegacy = process.env.CODE_REVIEW_OPENROUTER_API_KEY; const apiKeyGeneric = process.env.OPENROUTER_API_KEY; if (apiKeyNew) { debugLog("OpenRouter API key found: AI_CODE_REVIEW_OPENROUTER_API_KEY"); return { apiKey: apiKeyNew, source: "AI_CODE_REVIEW_OPENROUTER_API_KEY", message: "Using AI_CODE_REVIEW_OPENROUTER_API_KEY" }; } if (apiKeyLegacy) { console.warn( "Warning: Using deprecated environment variable CODE_REVIEW_OPENROUTER_API_KEY. Please switch to AI_CODE_REVIEW_OPENROUTER_API_KEY." ); debugLog( "OpenRouter API key found: CODE_REVIEW_OPENROUTER_API_KEY (deprecated)" ); return { apiKey: apiKeyLegacy, source: "CODE_REVIEW_OPENROUTER_API_KEY", message: "Using deprecated CODE_REVIEW_OPENROUTER_API_KEY" }; } if (apiKeyGeneric) { console.warn( "Warning: Using generic environment variable OPENROUTER_API_KEY. Consider using AI_CODE_REVIEW_OPENROUTER_API_KEY for better isolation." ); debugLog("OpenRouter API key found: OPENROUTER_API_KEY"); return { apiKey: apiKeyGeneric, source: "OPENROUTER_API_KEY", message: "Using OPENROUTER_API_KEY" }; } return { apiKey: void 0, source: "none", message: "No OpenRouter API key found. Please set AI_CODE_REVIEW_OPENROUTER_API_KEY in your .env.local file." }; } function getAnthropicApiKey() { const apiKeyNew = process.env.AI_CODE_REVIEW_ANTHROPIC_API_KEY; const apiKeyLegacy = process.env.CODE_REVIEW_ANTHROPIC_API_KEY; const apiKeyGeneric = process.env.ANTHROPIC_API_KEY; if (apiKeyNew) { debugLog("Anthropic API key found: AI_CODE_REVIEW_ANTHROPIC_API_KEY"); return { apiKey: apiKeyNew, source: "AI_CODE_REVIEW_ANTHROPIC_API_KEY", message: "Using AI_CODE_REVIEW_ANTHROPIC_API_KEY" }; } if (apiKeyLegacy) { console.warn( "Warning: Using deprecated environment variable CODE_REVIEW_ANTHROPIC_API_KEY. Please switch to AI_CODE_REVIEW_ANTHROPIC_API_KEY." ); debugLog( "Anthropic API key found: CODE_REVIEW_ANTHROPIC_API_KEY (deprecated)" ); return { apiKey: apiKeyLegacy, source: "CODE_REVIEW_ANTHROPIC_API_KEY", message: "Using deprecated CODE_REVIEW_ANTHROPIC_API_KEY" }; } if (apiKeyGeneric) { console.warn( "Warning: Using generic environment variable ANTHROPIC_API_KEY. Consider using AI_CODE_REVIEW_ANTHROPIC_API_KEY for better isolation." ); debugLog("Anthropic API key found: ANTHROPIC_API_KEY"); return { apiKey: apiKeyGeneric, source: "ANTHROPIC_API_KEY", message: "Using ANTHROPIC_API_KEY" }; } return { apiKey: void 0, source: "none", message: "No Anthropic API key found. Please set AI_CODE_REVIEW_ANTHROPIC_API_KEY in your .env.local file." }; } function getOpenAIApiKey() { const apiKeyNew = process.env.AI_CODE_REVIEW_OPENAI_API_KEY; const apiKeyLegacy = process.env.CODE_REVIEW_OPENAI_API_KEY; const apiKeyGeneric = process.env.OPENAI_API_KEY; if (apiKeyNew) { debugLog("OpenAI API key found: AI_CODE_REVIEW_OPENAI_API_KEY"); return { apiKey: apiKeyNew, source: "AI_CODE_REVIEW_OPENAI_API_KEY", message: "Using AI_CODE_REVIEW_OPENAI_API_KEY" }; } if (apiKeyLegacy) { console.warn( "Warning: Using deprecated environment variable CODE_REVIEW_OPENAI_API_KEY. Please switch to AI_CODE_REVIEW_OPENAI_API_KEY." ); debugLog("OpenAI API key found: CODE_REVIEW_OPENAI_API_KEY (deprecated)"); return { apiKey: apiKeyLegacy, source: "CODE_REVIEW_OPENAI_API_KEY", message: "Using deprecated CODE_REVIEW_OPENAI_API_KEY" }; } if (apiKeyGeneric) { console.warn( "Warning: Using generic environment variable OPENAI_API_KEY. Consider using AI_CODE_REVIEW_OPENAI_API_KEY for better isolation." ); debugLog("OpenAI API key found: OPENAI_API_KEY"); return { apiKey: apiKeyGeneric, source: "OPENAI_API_KEY", message: "Using OPENAI_API_KEY" }; } return { apiKey: void 0, source: "none", message: "No OpenAI API key found. Please set AI_CODE_REVIEW_OPENAI_API_KEY in your .env.local file." }; } function validateRequiredEnvVars() { const googleApiKey = getGoogleApiKey(); const openRouterApiKey = getOpenRouterApiKey(); const anthropicApiKey = getAnthropicApiKey(); const openaiApiKey = getOpenAIApiKey(); if (googleApiKey.apiKey || openRouterApiKey.apiKey || anthropicApiKey.apiKey || openaiApiKey.apiKey) { return { valid: true, message: "At least one API key is available" }; } return { valid: false, message: "No API keys found. Please set either AI_CODE_REVIEW_GOOGLE_API_KEY or AI_CODE_REVIEW_OPENROUTER_API_KEY in your .env.local file." }; } var path, dotenv, import_promises; var init_envLoader = __esm({ "src/utils/envLoader.ts"() { "use strict"; path = __toESM(require("path")); dotenv = __toESM(require("dotenv")); import_promises = __toESM(require("fs/promises")); } }); // src/utils/config.ts var config_exports = {}; __export(config_exports, { appConfigSchema: () => appConfigSchema, getApiKeyForProvider: () => getApiKeyForProvider, getConfig: () => getConfig, getPromptsPath: () => getPromptsPath, hasAnyApiKey: () => hasAnyApiKey, resetConfig: () => resetConfig, validateConfigForSelectedModel: () => validateConfigForSelectedModel }); function loadConfig(cliOptions) { const googleApiKeyResult = getGoogleApiKey(); const openRouterApiKeyResult = getOpenRouterApiKey(); const anthropicApiKeyResult = getAnthropicApiKey(); const openAIApiKeyResult = getOpenAIApiKey(); const googleApiKey = cliOptions?.apiKey?.google || cliOptions?.apiKeys?.google || googleApiKeyResult.apiKey; const openRouterApiKey = cliOptions?.apiKey?.openrouter || cliOptions?.apiKeys?.openrouter || openRouterApiKeyResult.apiKey; const anthropicApiKey = cliOptions?.apiKey?.anthropic || cliOptions?.apiKeys?.anthropic || anthropicApiKeyResult.apiKey; const openAIApiKey = cliOptions?.apiKey?.openai || cliOptions?.apiKeys?.openai || openAIApiKeyResult.apiKey; const selectedModel = cliOptions?.model || process.env.AI_CODE_REVIEW_MODEL || "gemini:gemini-2.5-pro-preview"; const writerModel = cliOptions?.writerModel || process.env.AI_CODE_REVIEW_WRITER_MODEL || void 0; const debug2 = cliOptions?.debug || process.env.AI_CODE_REVIEW_DEBUG === "true" || process.argv.includes("--debug"); const logLevel = cliOptions?.logLevel || process.env.AI_CODE_REVIEW_LOG_LEVEL || "info"; const contextPathsStr = process.env.AI_CODE_REVIEW_CONTEXT; const contextPaths = contextPathsStr ? contextPathsStr.split(",").map((p) => p.trim()) : void 0; const outputDir = cliOptions?.outputDir || process.env.AI_CODE_REVIEW_OUTPUT_DIR || "ai-code-review-docs"; const configObj = { googleApiKey, openRouterApiKey, anthropicApiKey, openAIApiKey, selectedModel, writerModel, debug: debug2, logLevel, contextPaths, outputDir }; try { return appConfigSchema.parse(configObj); } catch (error2) { if (error2 instanceof import_zod.z.ZodError) { logger_default.error("Configuration validation failed:", error2.errors); throw new Error( `Configuration validation failed: ${error2.errors.map((e) => e.message).join(", ")}` ); } throw error2; } } function getConfig(cliOptions) { if (!config2 || cliOptions) { try { config2 = loadConfig(cliOptions); } catch (error2) { logger_default.error("Failed to load configuration:", error2); throw error2; } } return config2; } function hasAnyApiKey() { const { googleApiKey, openRouterApiKey, anthropicApiKey, openAIApiKey } = getConfig(); return !!(googleApiKey || openRouterApiKey || anthropicApiKey || openAIApiKey); } function getApiKeyForProvider(provider) { const config4 = getConfig(); switch (provider.toLowerCase()) { case "gemini": return config4.googleApiKey; case "openrouter": return config4.openRouterApiKey; case "anthropic": return config4.anthropicApiKey; case "openai": return config4.openAIApiKey; default: return void 0; } } function resetConfig() { config2 = null; } function validateConfigForSelectedModel() { const config4 = getConfig(); const [provider] = config4.selectedModel.split(":"); if (!provider) { return { valid: false, message: `Invalid model format: ${config4.selectedModel}. Expected format: provider:model-name` }; } switch (provider.toLowerCase()) { case "gemini": if (!config4.googleApiKey) { return { valid: false, message: `Missing Google API key for model ${config4.selectedModel}. Set AI_CODE_REVIEW_GOOGLE_API_KEY in your .env.local file.` }; } break; case "openrouter": if (!config4.openRouterApiKey) { return { valid: false, message: `Missing OpenRouter API key for model ${config4.selectedModel}. Set AI_CODE_REVIEW_OPENROUTER_API_KEY in your .env.local file.` }; } break; case "anthropic": if (!config4.anthropicApiKey) { return { valid: false, message: `Missing Anthropic API key for model ${config4.selectedModel}. Set AI_CODE_REVIEW_ANTHROPIC_API_KEY in your .env.local file.` }; } break; case "openai": if (!config4.openAIApiKey) { return { valid: false, message: `Missing OpenAI API key for model ${config4.selectedModel}. Set AI_CODE_REVIEW_OPENAI_API_KEY in your .env.local file.` }; } break; default: return { valid: false, message: `Unknown provider: ${provider}. Supported providers are: gemini, openrouter, anthropic, openai` }; } return { valid: true, message: "Configuration is valid for the selected model" }; } function getPromptsPath() { const possiblePaths = [ // For local development import_path.default.resolve("prompts"), // For npm package import_path.default.resolve(__dirname, "..", "..", "prompts"), // For global installation import_path.default.resolve(__dirname, "..", "..", "..", "prompts") ]; for (const p of possiblePaths) { if (import_fs.default.existsSync(p)) { return p; } } return possiblePaths[0]; } var import_path, import_fs, import_zod, appConfigSchema, config2; var init_config = __esm({ "src/utils/config.ts"() { "use strict"; init_envLoader(); init_logger(); import_path = __toESM(require("path")); import_fs = __toESM(require("fs")); import_zod = require("zod"); appConfigSchema = import_zod.z.object({ // API Keys googleApiKey: import_zod.z.string().optional(), openRouterApiKey: import_zod.z.string().optional(), anthropicApiKey: import_zod.z.string().optional(), openAIApiKey: import_zod.z.string().optional(), // Model configuration selectedModel: import_zod.z.string(), writerModel: import_zod.z.string().optional(), // Other configuration debug: import_zod.z.boolean(), logLevel: import_zod.z.enum(["debug", "info", "warn", "error", "none"]).default("info"), contextPaths: import_zod.z.array(import_zod.z.string()).optional(), outputDir: import_zod.z.string().default("ai-code-review-docs") }); config2 = null; } }); // src/tokenizers/baseTokenizer.ts function getTokenizer(modelName) { const modelNameLower = modelName.toLowerCase(); if (modelNameLower.includes("gpt")) { return TokenizerRegistry.getAllTokenizers().find( (t2) => t2.getModelName() === "gpt" ) || new FallbackTokenizer(); } if (modelNameLower.includes("claude")) { return TokenizerRegistry.getAllTokenizers().find( (t2) => t2.getModelName() === "claude" ) || new FallbackTokenizer(); } if (modelNameLower.includes("gemini")) { return TokenizerRegistry.getAllTokenizers().find( (t2) => t2.getModelName() === "gemini" ) || new FallbackTokenizer(); } return new FallbackTokenizer(); } function countTokens(text, modelName) { const tokenizer = getTokenizer(modelName); const count = tokenizer.countTokens(text); if (modelName === "test-small-context") { return text.length; } return count; } var TokenizerRegistry, FallbackTokenizer; var init_baseTokenizer = __esm({ "src/tokenizers/baseTokenizer.ts"() { "use strict"; TokenizerRegistry = class _TokenizerRegistry { static tokenizers = []; /** * Register a tokenizer * @param tokenizer Tokenizer to register */ static register(tokenizer) { _TokenizerRegistry.tokenizers.push(tokenizer); } /** * Get a tokenizer for a specific model * @param modelName Name of the model * @returns Tokenizer for the model, or undefined if none found */ static getTokenizer(modelName) { return _TokenizerRegistry.tokenizers.find((t2) => t2.supportsModel(modelName)); } /** * Get all registered tokenizers * @returns Array of all registered tokenizers */ static getAllTokenizers() { return [..._TokenizerRegistry.tokenizers]; } }; FallbackTokenizer = class { /** * Count the number of tokens in a text using a simple approximation * @param text Text to count tokens for * @returns Estimated token count */ countTokens(text) { return Math.ceil(text.length / 4); } /** * Get the model name for this tokenizer * @returns 'fallback' */ getModelName() { return "fallback"; } /** * This tokenizer is used as a fallback for any model * @param _modelName Name of the model (unused but required by interface) * @returns Always true */ supportsModel(_modelName) { return true; } }; TokenizerRegistry.register(new FallbackTokenizer()); } }); // src/tokenizers/gptTokenizer.ts var import_gpt_tokenizer, GPTTokenizer; var init_gptTokenizer = __esm({ "src/tokenizers/gptTokenizer.ts"() { "use strict"; import_gpt_tokenizer = require("gpt-tokenizer"); init_baseTokenizer(); GPTTokenizer = class { modelPatterns = [/gpt/i]; /** * Count the number of tokens in a text using the GPT tokenizer * @param text Text to count tokens for * @returns Actual token count */ countTokens(text) { try { const tokens = (0, import_gpt_tokenizer.encode)(text); return tokens.length; } catch (error2) { console.warn(`Error counting tokens with GPT tokenizer: ${error2}`); return Math.ceil(text.length / 4); } } /** * Get the model name for this tokenizer * @returns 'gpt' */ getModelName() { return "gpt"; } /** * Check if this tokenizer supports a given model * @param modelName Name of the model to check * @returns True if the model is supported, false otherwise */ supportsModel(modelName) { const lowerModelName = modelName.toLowerCase(); return this.modelPatterns.some((pattern) => pattern.test(lowerModelName)); } }; TokenizerRegistry.register(new GPTTokenizer()); } }); // src/tokenizers/claudeTokenizer.ts var import_tokenizer, ClaudeTokenizer; var init_claudeTokenizer = __esm({ "src/tokenizers/claudeTokenizer.ts"() { "use strict"; import_tokenizer = require("@anthropic-ai/tokenizer"); init_baseTokenizer(); ClaudeTokenizer = class { modelPatterns = [/claude/i]; /** * Count the number of tokens in a text using the Claude tokenizer * @param text Text to count tokens for * @returns Actual token count */ countTokens(text) { try { return (0, import_tokenizer.countTokens)(text); } catch (error2) { console.warn(`Error counting tokens with Claude tokenizer: ${error2}`); return Math.ceil(text.length / 4); } } /** * Get the model name for this tokenizer * @returns 'claude' */ getModelName() { return "claude"; } /** * Check if this tokenizer supports a given model * @param modelName Name of the model to check * @returns True if the model is supported, false otherwise */ supportsModel(modelName) { const lowerModelName = modelName.toLowerCase(); return this.modelPatterns.some((pattern) => pattern.test(lowerModelName)); } }; TokenizerRegistry.register(new ClaudeTokenizer()); } }); // src/tokenizers/geminiTokenizer.ts var GeminiTokenizer; var init_geminiTokenizer = __esm({ "src/tokenizers/geminiTokenizer.ts"() { "use strict"; init_baseTokenizer(); GeminiTokenizer = class { modelPatterns = [/gemini/i]; /** * Count the number of tokens in a text using an approximation for Gemini models * @param text Text to count tokens for * @returns Estimated token count */ countTokens(text) { return Math.ceil(text.length / 4); } /** * Get the model name for this tokenizer * @returns 'gemini' */ getModelName() { return "gemini"; } /** * Check if this tokenizer supports a given model * @param modelName Name of the model to check * @returns True if the model is supported, false otherwise */ supportsModel(modelName) { const lowerModelName = modelName.toLowerCase(); return this.modelPatterns.some((pattern) => pattern.test(lowerModelName)); } }; TokenizerRegistry.register(new GeminiTokenizer()); } }); // src/tokenizers/index.ts var init_tokenizers = __esm({ "src/tokenizers/index.ts"() { "use strict"; init_baseTokenizer(); init_gptTokenizer(); init_claudeTokenizer(); init_geminiTokenizer(); } }); // src/estimators/abstractEstimator.ts var AbstractTokenEstimator; var init_abstractEstimator = __esm({ "src/estimators/abstractEstimator.ts"() { "use strict"; init_tokenizers(); AbstractTokenEstimator = class { /** * Estimate the number of tokens in a text * @param text Text to estimate tokens for * @param modelName Optional model name to use for tokenization * @returns Estimated token count */ estimateTokenCount(text, modelName) { return countTokens(text, modelName || this.getDefaultModel()); } /** * Format a cost value as a currency string * @param cost Cost value in USD * @returns Formatted cost string */ formatCost(cost) { return `$${cost.toFixed(6)} USD`; } /** * Get cost information based on token counts * @param inputTokens Number of input tokens * @param outputTokens Number of output tokens * @param modelName Name of the model (optional) * @returns Cost information */ getCostInfo(inputTokens, outputTokens, modelName) { const totalTokens = inputTokens + outputTokens; const estimatedCost = this.calculateCost( inputTokens, outputTokens, modelName ); return { inputTokens, outputTokens, totalTokens, estimatedCost, formattedCost: this.formatCost(estimatedCost) }; } /** * Get cost information based on text * @param inputText Input text * @param outputText Output text * @param modelName Name of the model (optional) * @returns Cost information */ getCostInfoFromText(inputText, outputText, modelName) { const model = modelName || this.getDefaultModel(); const inputTokens = this.estimateTokenCount(inputText, model); const outputTokens = this.estimateTokenCount(outputText, model); return this.getCostInfo(inputTokens, outputTokens, model); } }; } }); // src/estimators/geminiEstimator.ts var GeminiTokenEstimator; var init_geminiEstimator = __esm({ "src/estimators/geminiEstimator.ts"() { "use strict"; init_abstractEstimator(); GeminiTokenEstimator = class _GeminiTokenEstimator extends AbstractTokenEstimator { static instance; /** * Get the singleton instance of the estimator * @returns GeminiTokenEstimator instance */ static getInstance() { if (!_GeminiTokenEstimator.instance) { _GeminiTokenEstimator.instance = new _GeminiTokenEstimator(); } return _GeminiTokenEstimator.instance; } /** * Pricing information for Gemini models */ MODEL_PRICING = { // Gemini 2.5 models "gemini-2.5-pro": { type: "tiered", tiers: [ { threshold: 0, inputTokenCost: 125e-5, // $1.25 per 1M tokens (≤200k tokens) outputTokenCost: 0.01 // $10.00 per 1M tokens (≤200k tokens) }, { threshold: 2e5, inputTokenCost: 25e-4, // $2.50 per 1M tokens (>200k tokens) outputTokenCost: 0.015 // $15.00 per 1M tokens (>200k tokens) } ] }, "gemini-2.5-pro-preview": { type: "tiered", tiers: [ { threshold: 0, inputTokenCost: 125e-5, // $1.25 per 1M tokens (≤200k tokens) outputTokenCost: 0.01 // $10.00 per 1M tokens (≤200k tokens) }, { threshold: 2e5, inputTokenCost: 25e-4, // $2.50 per 1M tokens (>200k tokens) outputTokenCost: 0.015 // $15.00 per 1M tokens (>200k tokens) } ] }, "gemini-2.5-pro-exp": { type: "tiered", tiers: [ { threshold: 0, inputTokenCost: 125e-5, // $1.25 per 1M tokens (≤200k tokens) outputTokenCost: 0.01 // $10.00 per 1M tokens (≤200k tokens) }, { threshold: 2e5, inputTokenCost: 25e-4, // $2.50 per 1M tokens (>200k tokens) outputTokenCost: 0.015 // $15.00 per 1M tokens (>200k tokens) } ] }, "gemini-2.0-flash": { type: "standard", inputTokenCost: 1e-4, // $0.10 per 1M tokens outputTokenCost: 4e-4 // $0.40 per 1M tokens }, "gemini-2.0-flash-lite": { type: "standard", inputTokenCost: 75e-6, // $0.075 per 1M tokens outputTokenCost: 3e-4 // $0.30 per 1M tokens }, // Gemini 1.5 models "gemini-1.5-pro": { type: "tiered", tiers: [ { threshold: 0, inputTokenCost: 125e-5, // $1.25 per 1M tokens (≤128k tokens) outputTokenCost: 5e-3 // $5.00 per 1M tokens (≤128k tokens) }, { threshold: 128e3, inputTokenCost: 25e-4, // $2.50 per 1M tokens (>128k tokens) outputTokenCost: 0.01 // $10.00 per 1M tokens (>128k tokens) } ] }, "gemini-1.5-flash": { type: "tiered", tiers: [ { threshold: 0, inputTokenCost: 75e-6, // $0.075 per 1M tokens (≤128k tokens) outputTokenCost: 3e-4 // $0.30 per 1M tokens (≤128k tokens) }, { threshold: 128e3, inputTokenCost: 15e-5, // $0.15 per 1M tokens (>128k tokens) outputTokenCost: 6e-4 // $0.60 per 1M tokens (>128k tokens) } ] }, "gemini-1.5-flash-8b": { type: "tiered", tiers: [ { threshold: 0, inputTokenCost: 375e-7, // $0.0375 per 1M tokens (≤128k tokens) outputTokenCost: 15e-5 // $0.15 per 1M tokens (≤128k tokens) }, { threshold: 128e3, inputTokenCost: 75e-6, // $0.075 per 1M tokens (>128k tokens) outputTokenCost: 3e-4 // $0.30 per 1M tokens (>128k tokens) } ] }, // Default fallback pricing default: { type: "standard", inputTokenCost: 1e-3, // $1.00 per 1M tokens outputTokenCost: 2e-3 // $2.00 per 1M tokens } }; /** * Private constructor to enforce singleton pattern */ constructor() { super(); } /** * Get the pricing for a specific model * @param modelName Name of the model * @returns Pricing information for the model */ getModelPricing(modelName) { return this.MODEL_PRICING[modelName] || this.MODEL_PRICING["default"]; } /** * Calculate the cost for a specific tier * @param tokens Number of tokens * @param tokenCost Cost per 1K tokens * @param tierStart Start of the tier * @param tierEnd End of the tier (or undefined for no upper limit) * @returns Cost for this tier */ calculateTierCost(tokens, tokenCost, tierStart, tierEnd) { const tierTokens = tierEnd ? Math.min(Math.max(0, tokens - tierStart), tierEnd - tierStart) : Math.max(0, tokens - tierStart); return tierTokens / 1e3 * tokenCost; } /** * Calculate the cost for a given number of input and output tokens * @param inputTokens Number of input tokens * @param outputTokens Number of output tokens * @param modelName Name of the model (optional, uses default if not provided) * @returns Estimated cost in USD */ calculateCost(inputTokens, outputTokens, modelName = this.getDefaultModel()) { const pricing = this.getModelPricing(modelName); let inputCost = 0; let outputCost = 0; if (pricing.type === "standard") { inputCost = inputTokens / 1e3 * pricing.inputTokenCost; outputCost = outputTokens / 1e3 * pricing.outputTokenCost; } else if (pricing.type === "tiered") { const tiers = pricing.tiers; for (let i = 0; i < tiers.length; i++) { const tierStart = tiers[i].threshold; const tierEnd = i < tiers.length - 1 ? tiers[i + 1].threshold : void 0; inputCost += this.calculateTierCost( inputTokens, tiers[i].inputTokenCost, tierStart, tierEnd ); } for (let i = 0; i < tiers.length; i++) { const tierStart = tiers[i].threshold; const tierEnd = i < tiers.length - 1 ? tiers[i + 1].threshold : void 0; outputCost += this.calculateTierCost( outputTokens, tiers[i].outputTokenCost, tierStart, tierEnd ); } } return inputCost + outputCost; } /** * Get the default model name for this estimator * @returns Default model name */ getDefaultModel() { return "gemini-1.5-pro"; } /** * Check if this estimator supports a given model * @param modelName Name of the model to check * @returns True if the model is supported, false otherwise */ supportsModel(modelName) { return modelName in this.MODEL_PRICING || modelName.startsWith("gemini-"); } }; } }); // src/clients/utils/modelMaps/types.ts var ModelCategory; var init_types = __esm({ "src/clients/utils/modelMaps/types.ts"() { "use strict"; ModelCategory = /* @__PURE__ */ ((ModelCategory2) => { ModelCategory2["REASONING"] = "reasoning"; ModelCategory2["FAST_INFERENCE"] = "fast-inference"; ModelCategory2["COST_OPTIMIZED"] = "cost-optimized"; ModelCategory2["LONG_CONTEXT"] = "long-context"; ModelCategory2["MULTIMODAL"] = "multimodal"; ModelCategory2["CODING"] = "coding"; return ModelCategory2; })(ModelCategory || {}); } }); // src/clients/utils/modelMaps/data/gemini.ts var GEMINI_MODELS; var init_gemini = __esm({ "src/clients/utils/modelMaps/data/gemini.ts"() { "use strict"; init_types(); GEMINI_MODELS = { "gemini:gemini-2.5-pro-preview": { apiIdentifier: "gemini-2.5-pro-preview-05-06", displayName: "Gemini 2.5 Pro Preview", provider: "gemini", useV1Beta: true, contextWindow: 1e6, description: "Most advanced reasoning and multimodal capabilities", apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY", supportsToolCalling: false, status: "preview", categories: ["reasoning" /* REASONING */, "long-context" /* LONG_CONTEXT */, "multimodal" /* MULTIMODAL */], capabilities: ["advanced-reasoning", "multimodal", "code-generation", "long-context"], tieredPricing: [ { tokenThreshold: 0, inputPricePerMillion: 1.25, outputPricePerMillion: 5 }, { tokenThreshold: 2e5, inputPricePerMillion: 2.5, outputPricePerMillion: 10 } ], providerFeatures: { supportsStreaming: true, supportsBatch: true, toolCallingSupport: "partial" } }, "gemini:gemini-2.5-pro": { apiIdentifier: "gemini-2.5-pro-preview-05-06", displayName: "Gemini 2.5 Pro", provider: "gemini", useV1Beta: true, contextWindow: 1e6, description: "Production-ready advanced reasoning model", apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY", supportsToolCalling: false, status: "available", categories: ["reasoning" /* REASONING */, "long-context" /* LONG_CONTEXT */, "multimodal" /* MULTIMODAL */], capabilities: ["advanced-reasoning", "multimodal", "code-generation", "long-context"], tieredPricing: [ { tokenThreshold: 0, inputPricePerMillion: 1.25, outputPricePerMillion: 5 }, { tokenThreshold: 2e5, inputPricePerMillion: 2.5, outputPricePerMillion: 10 } ], providerFeatures: { supportsStreaming: true, supportsBatch: true, toolCallingSupport: "partial" } }, "gemini:gemini-2.0-flash-lite": { apiIdentifier: "gemini-2.0-flash-lite", displayName: "Gemini 2.0 Flash Lite", provider: "gemini", useV1Beta: true, contextWindow: 1e6, description: "Ultra-fast, cost-efficient model for simple tasks", apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY", supportsToolCalling: false, status: "available", categories: ["fast-inference" /* FAST_INFERENCE */, "cost-optimized" /* COST_OPTIMIZED */], capabilities: ["fast-inference", "basic-reasoning"], inputPricePerMillion: 0.05, outputPricePerMillion: 0.15, providerFeatures: { supportsStreaming: true, supportsBatch: true, toolCallingSupport: "none" } }, "gemini:gemini-2.0-flash": { apiIdentifier: "gemini-2.0-flash-preview-05-07", displayName: "Gemini 2.0 Flash", provider: "gemini", useV1Beta: true, contextWindow: 1e6, description: "Fast, efficient model with strong performance", apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY", supportsToolCalling: false, status: "preview", categories: ["fast-inference" /* FAST_INFERENCE */, "long-context" /* LONG_CONTEXT */], capabilities: ["fast-inference", "good-reasoning", "long-context"], inputPricePerMillion: 0.3, outputPricePerMillion: 1.2, providerFeatures: { supportsStreaming: true, supportsBatch: true, toolCallingSupport: "partial" } }, "gemini:gemini-1.5-pro": { apiIdentifier: "gemini-1.5-pro", displayName: "Gemini 1.5 Pro", provider: "gemini", useV1Beta: false, contextWindow: 1e6, description: "Previous generation large context model", apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY", supportsToolCalling: false, status: "available", categories: ["long-context" /* LONG_CONTEXT */], capabilities: ["long-context", "good-reasoning"], tieredPricing: [ { tokenThreshold: 0, inputPricePerMillion: 1.25, outputPricePerMillion: 5 }, { tokenThreshold: 128e3, inputPricePerMillion: 2.5, outputPricePerMillion: 10 } ], providerFeatures: { supportsStreaming: true, supportsBatch: true, toolCallingSupport: "partial" } }, "gemini:gemini-1.5-flash": { apiIdentifier: "gemini-1.5-flash", displayName: "Gemini 1.5 Flash", provider: "gemini", useV1Beta: false, contextWindow: 1e6, description: "Previous generation fast model", apiKeyEnvVar: "AI_CODE_REVIEW_GOOGLE_API_KEY", supportsToolCalling: false, status: "available", categories: ["fast-inference" /* FAST_INFERENCE */, "long-context" /* LONG_CONTEXT */], capabilities: ["fast-inference", "long-context"], tieredPricing: [ { tokenThreshold: 0, inputPricePerMillion: 0.075, outputPricePerMillion: 0.3 }, { tokenThreshold: 128e3, inputPricePerMillion: 0.15, outputPricePerMillion: 0.6 } ], providerFeatures: { supportsStreaming: true, supportsBatch: true, toolCallingSupport: "partial" } } }; } }); // src/clients/utils/modelMaps/data/anthropic.ts var ANTHROPIC_MODELS; var init_anthropic = __esm({ "src/clients/utils/modelMaps/data/anthropic.ts"() { "use strict"; init_types(); ANTHROPIC_MODELS = { "anthropic:claude-4-opus": { apiIdentifier: "claude-4-opus-20241022", displayName: "Claude 4 Opus", provider: "anthropic", contextWindow: 2e5, description: "Most capable Claude model with superior reasoning", apiKeyEnvVar: "AI_CODE_REVIEW_ANTHROPIC_API_KEY", supportsToolCalling: true, status: "available", categories: ["reasoning" /* REASONING */, "coding" /* CODING */], capabilities: ["advanced-reasoning", "code-generation", "code-review", "analysis"], inputPricePerMillion: 15, outputPricePerMillion: 75, providerFeatures: { supportsStreaming: true, supportsBatch: true, supportsPromptCaching: true, toolCallingSupport: "full" } }, "anthropic:claude-4-sonnet": { apiIdentifier: "claude-4-sonnet-20241022", displayName: "Claude 4 Sonnet", provider: "anthropic", contextWindow: 2e5, description: "Balanced performance and cost for code review", apiKeyEnvVar: "AI_CODE_REVIEW_ANTHROPIC_API_KEY", supportsToolCalling: true, status: "available", categories: ["reasoning" /* REASONING */, "coding" /* CODING */, "cost-optimized" /* COST_OPTIMIZED */], capabilities: ["good-reasoning", "code-generation", "code-review"], inputPricePerMillion: 3, outputPricePerMillion: 15, providerFeatures: { supportsStreaming: true, supportsBatch: true, supportsPromptCaching: true, toolCallingSupport: "full" }, notes: "Recommended model for code review tasks" }, "anthropic:claude-3.5-sonnet": { apiIdentifier: "claude-3-5-sonnet-20241022", displayName: "Claude 3.5 Sonnet", provider: "anthropic", contextWindow: 2e5, description: "Enhanced Claude 3 with improved capabilities", apiKeyEnvVar: "AI_CODE_REVIEW_ANTHROPIC_API_KEY", supportsToolCalling: true, st