UNPKG

mcp-chrome-bridge

Version:

Chrome Native-Messaging host (Node)

311 lines 12.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.detectCcr = detectCcr; exports.clearCcrCache = clearCcrCache; exports.validateCcrConfig = validateCcrConfig; /** * Claude Code Router (CCR) Auto-Detection Module. * * This module provides automatic detection of CCR configuration * for users who have already set up CCR on their system. * * CCR config location: ~/.claude-code-router/config.json * CCR uses env vars: ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN * * The detection flow: * 1. Check if CCR env vars are already set (skip if yes) * 2. Read CCR config file * 3. Parse JSON5 config with env var interpolation * 4. Verify CCR is running via health check * 5. Return derived env vars if healthy */ const promises_1 = require("node:fs/promises"); const node_path_1 = __importDefault(require("node:path")); const node_os_1 = __importDefault(require("node:os")); /** * Default CCR port. */ const DEFAULT_CCR_PORT = 9898; /** * CCR config file path. */ const CCR_CONFIG_PATH = node_path_1.default.join(node_os_1.default.homedir(), '.claude-code-router', 'config.json'); /** * Health check timeout in milliseconds. */ const HEALTH_CHECK_TIMEOUT = 2000; /** * Cache for CCR detection result (to avoid repeated file reads and health checks). * Cached for the lifetime of the process. */ let cachedResult = null; let cacheTimestamp = 0; const CACHE_TTL = 60000; // 1 minute /** * Detect CCR configuration and verify it's running. * * This function: * 1. Returns cached result if still valid * 2. Checks if CCR env vars are already set in process.env * 3. If not, reads and parses CCR config file * 4. Verifies CCR is running via health check * * @returns Detection result with baseUrl and authToken if CCR is available */ async function detectCcr() { var _a, _b, _c, _d, _e; // Check cache const now = Date.now(); if (cachedResult && now - cacheTimestamp < CACHE_TTL) { return cachedResult; } try { // First, check if env vars are already set (user ran `eval "$(ccr activate)"`) const envBaseUrl = process.env.ANTHROPIC_BASE_URL; const envAuthToken = process.env.ANTHROPIC_AUTH_TOKEN; if (envBaseUrl && envAuthToken) { // Verify CCR is running const healthy = await checkCcrHealth(envBaseUrl); if (healthy) { cachedResult = { detected: true, baseUrl: envBaseUrl, authToken: envAuthToken, source: 'env', }; cacheTimestamp = now; return cachedResult; } // Env vars set but CCR not healthy - fall through to config detection } // Try to read CCR config file const configResult = await readCcrConfig(); if (!configResult.config) { cachedResult = { detected: false, error: configResult.error || 'CCR config not found or invalid', }; cacheTimestamp = now; return cachedResult; } const config = configResult.config; // Derive env vars from config (support both uppercase and lowercase field names) const port = (_b = (_a = config.PORT) !== null && _a !== void 0 ? _a : config.port) !== null && _b !== void 0 ? _b : DEFAULT_CCR_PORT; const host = (_d = (_c = config.HOST) !== null && _c !== void 0 ? _c : config.host) !== null && _d !== void 0 ? _d : '127.0.0.1'; const baseUrl = `http://${host}:${port}`; // APIKEY can be empty string in config, use 'APIKEY' as fallback (CCR accepts this) const apiKey = (_e = config.APIKEY) !== null && _e !== void 0 ? _e : config.apiKey; const authToken = apiKey && apiKey.length > 0 ? apiKey : 'APIKEY'; // Verify CCR is running const healthy = await checkCcrHealth(baseUrl); if (!healthy) { cachedResult = { detected: false, error: 'CCR config found but service not running', }; cacheTimestamp = now; return cachedResult; } cachedResult = { detected: true, baseUrl, authToken, source: 'config', }; cacheTimestamp = now; return cachedResult; } catch (error) { const message = error instanceof Error ? error.message : String(error); cachedResult = { detected: false, error: message }; cacheTimestamp = now; return cachedResult; } } /** * Read and parse CCR config file. */ async function readCcrConfig() { try { const content = await (0, promises_1.readFile)(CCR_CONFIG_PATH, 'utf-8'); const config = parseJson5Config(content); if (!config) { return { config: null, error: 'Failed to parse CCR config file' }; } return { config }; } catch (error) { const err = error; if (err.code === 'ENOENT') { // Config file doesn't exist - CCR not installed return { config: null, error: 'CCR config file not found' }; } return { config: null, error: `Failed to read CCR config: ${err.message}` }; } } /** * Parse CCR config file. * * CCR config is standard JSON (not JSON5), so we can use JSON.parse directly. * We only need to handle env var interpolation: ${VAR_NAME} * * Note: Previous implementation tried to strip comments using regex which * incorrectly matched "http://" URLs inside strings. */ function parseJson5Config(content) { try { // First try standard JSON parse (CCR config is usually valid JSON) // Only interpolate env vars if needed let processed = content; // Interpolate env vars: ${VAR_NAME} -> value // Only do this outside of the JSON parsing to avoid breaking strings if (content.includes('${')) { processed = content.replace(/\$\{([^}]+)\}/g, (_, varName) => { const value = process.env[varName.trim()]; return value || ''; }); } const parsed = JSON.parse(processed); return parsed; } catch (parseError) { // Log parse error for debugging console.error('[CCR] Failed to parse config:', parseError); return null; } } /** * Check if CCR is running by hitting its health endpoint. */ async function checkCcrHealth(baseUrl) { try { const healthUrl = `${baseUrl}/health`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT); try { const response = await fetch(healthUrl, { method: 'GET', signal: controller.signal, }); clearTimeout(timeoutId); return response.ok; } catch (_a) { clearTimeout(timeoutId); return false; } } catch (_b) { return false; } } /** * Clear the CCR detection cache. * Useful for testing or when user wants to re-detect. */ function clearCcrCache() { cachedResult = null; cacheTimestamp = 0; } /** * Validate CCR configuration for common misconfigurations. * * This function checks for issues that would cause runtime errors in CCR, * particularly the "Router.default must be provider,model" requirement. * * The most common misconfiguration is setting Router.default to just a provider * name (e.g., "venus") instead of the required "provider,model" format * (e.g., "venus,claude-4-5-sonnet-20250929"). This causes CCR to crash with * "Cannot read properties of undefined (reading 'includes')" when it tries * to split the model name. */ async function validateCcrConfig() { var _a, _b, _c; const configResult = await readCcrConfig(); // If we can't read the config, return early (not our problem to report) if (!configResult.config) { return { checked: false, valid: true, configPath: CCR_CONFIG_PATH, issue: configResult.error, }; } const config = configResult.config; const router = (_a = config.Router) !== null && _a !== void 0 ? _a : config.router; const routerDefault = (_b = router === null || router === void 0 ? void 0 : router.default) === null || _b === void 0 ? void 0 : _b.trim(); // No Router.default configured if (!routerDefault) { return { checked: true, valid: false, configPath: CCR_CONFIG_PATH, issue: 'CCR Router.default is not configured.', suggestion: `Edit ${CCR_CONFIG_PATH} and set Router.default to "provider,model" format, then restart CCR.`, }; } // Check if Router.default contains a comma (required format: provider,model) if (!routerDefault.includes(',')) { const suggestedFix = inferSuggestedRouterDefault(routerDefault, config, router); const example = suggestedFix !== null && suggestedFix !== void 0 ? suggestedFix : `${routerDefault},<model>`; return { checked: true, valid: false, configPath: CCR_CONFIG_PATH, routerDefault, issue: `CCR Router.default must be "provider,model" format, but got "${routerDefault}" (missing model).`, suggestedFix, suggestion: `Edit ${CCR_CONFIG_PATH} and change Router.default from "${routerDefault}" to "${example}", then restart CCR.`, }; } // Validate the model part is not empty after splitting const [providerPart, modelPart] = routerDefault.split(',', 2); if (!(providerPart === null || providerPart === void 0 ? void 0 : providerPart.trim()) || !(modelPart === null || modelPart === void 0 ? void 0 : modelPart.trim())) { const suggestedFix = inferSuggestedRouterDefault((_c = providerPart === null || providerPart === void 0 ? void 0 : providerPart.trim()) !== null && _c !== void 0 ? _c : '', config, router); return { checked: true, valid: false, configPath: CCR_CONFIG_PATH, routerDefault, issue: `CCR Router.default "${routerDefault}" has empty provider or model part.`, suggestedFix, suggestion: `Edit ${CCR_CONFIG_PATH} and set Router.default to a valid "provider,model" format, then restart CCR.`, }; } return { checked: true, valid: true, configPath: CCR_CONFIG_PATH, routerDefault, }; } /** * Try to infer a suggested Router.default value based on available providers and models. */ function inferSuggestedRouterDefault(providerName, config, router) { var _a, _b, _c; const normalizedProvider = providerName.toLowerCase(); if (!normalizedProvider) return undefined; // Try to find the provider in Providers array and get its first model const providers = (_b = (_a = config.Providers) !== null && _a !== void 0 ? _a : config.providers) !== null && _b !== void 0 ? _b : []; const matchedProvider = providers.find((p) => { var _a; return ((_a = p.name) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === normalizedProvider; }); if ((matchedProvider === null || matchedProvider === void 0 ? void 0 : matchedProvider.name) && ((_c = matchedProvider.models) === null || _c === void 0 ? void 0 : _c[0])) { return `${matchedProvider.name},${matchedProvider.models[0]}`; } // Fallback: look at other Router entries that have valid "provider,model" format const routerEntries = [router === null || router === void 0 ? void 0 : router.background, router === null || router === void 0 ? void 0 : router.think, router === null || router === void 0 ? void 0 : router.longContext]; for (const entry of routerEntries) { if (!entry || !entry.includes(',')) continue; const [p, m] = entry.split(',', 2); if ((p === null || p === void 0 ? void 0 : p.trim().toLowerCase()) === normalizedProvider && (m === null || m === void 0 ? void 0 : m.trim())) { return `${providerName},${m.trim()}`; } } return undefined; } //# sourceMappingURL=ccr-detector.js.map