diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 3c90fd385..cfe46e238 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -83,6 +83,7 @@ import { createNotificationsRoutes } from './routes/notifications/index.js'; import { getNotificationService } from './services/notification-service.js'; import { createEventHistoryRoutes } from './routes/event-history/index.js'; import { getEventHistoryService } from './services/event-history-service.js'; +import { initGlobalSettingsAccessor } from './lib/global-settings-accessor.js'; // Load environment variables dotenv.config(); @@ -227,6 +228,7 @@ const events: EventEmitter = createEventEmitter(); // Create services // Note: settingsService is created first so it can be injected into other services const settingsService = new SettingsService(DATA_DIR); +initGlobalSettingsAccessor(settingsService); const agentService = new AgentService(DATA_DIR, events, settingsService); const featureLoader = new FeatureLoader(); const autoModeService = new AutoModeService(events, settingsService); diff --git a/apps/server/src/lib/ccr.ts b/apps/server/src/lib/ccr.ts new file mode 100644 index 000000000..ad29f37c6 --- /dev/null +++ b/apps/server/src/lib/ccr.ts @@ -0,0 +1,162 @@ +/** + * Claude Code Router (CCR) Utilities + * + * Provides functions to detect CCR installation, check server status, + * and read configuration for integration with Claude Agent SDK. + */ + +import { exec } from 'node:child_process'; +import { readFileSync, existsSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; +import type { CCRStatus, CCRConfig } from '@automaker/types'; +import { createLogger } from '@automaker/utils'; + +const execAsync = promisify(exec); +const logger = createLogger('CCR'); + +/** Default CCR configuration directory */ +const CCR_CONFIG_DIR = join(homedir(), '.claude-code-router'); +const CCR_CONFIG_FILE = join(CCR_CONFIG_DIR, 'config.json'); + +// Cache for CCR status to avoid spamming the process list +let statusCache: { status: CCRStatus; timestamp: number } | null = null; +const STATUS_CACHE_TTL = 2000; // 2 seconds + +/** + * Strips ANSI escape codes from a string + */ +function stripAnsi(str: string): string { + const ansiRegex = new RegExp( + '[\\u001b\\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]', + 'g' + ); + return str.replace(ansiRegex, ''); +} + +/** + * Get the CCR configuration from disk + */ +export function getCCRConfig(): CCRConfig | null { + try { + if (!existsSync(CCR_CONFIG_FILE)) { + return null; + } + const content = readFileSync(CCR_CONFIG_FILE, 'utf-8'); + return JSON.parse(content) as CCRConfig; + } catch (error) { + logger.debug('Failed to read CCR config:', error); + return null; + } +} + +/** + * Check if CCR CLI is installed (async) + */ +export async function isCCRInstalled(): Promise { + try { + // Use 'where' on Windows, 'which' on Unix + const cmd = process.platform === 'win32' ? 'where ccr' : 'which ccr'; + await execAsync(cmd); + return true; + } catch { + return false; + } +} + +/** + * Get CCR server status (async with caching) + */ +export async function getCCRStatus(): Promise { + // Check cache first + const now = Date.now(); + if (statusCache && now - statusCache.timestamp < STATUS_CACHE_TTL) { + return statusCache.status; + } + + const installed = await isCCRInstalled(); + if (!installed) { + const status = { installed: false, running: false }; + statusCache = { status, timestamp: now }; + return status; + } + + // Get config once to reuse + const config = getCCRConfig(); + const port = config?.PORT ?? 3456; + const rawHost = config?.HOST || '127.0.0.1'; + // If host is 0.0.0.0, use 127.0.0.1 for client connection + const host = rawHost === '0.0.0.0' ? '127.0.0.1' : rawHost; + + try { + // Run 'ccr status' to check if server is running + const { stdout } = await execAsync('ccr status', { + timeout: 5000, + encoding: 'utf-8', + }); + + // Parse the output to determine status + const cleanStdout = stripAnsi(stdout); + // Use regex to avoid matching "not running" + // Look for word "running" not preceded by "not" (case insensitive) + const isRunning = /\b(? | null { + if (!status.installed || !status.running) { + return null; + } + + const effectiveConfig = config ?? getCCRConfig(); + const port = status.port ?? effectiveConfig?.PORT ?? 3456; + const rawHost = effectiveConfig?.HOST || '127.0.0.1'; + // If host is 0.0.0.0, use 127.0.0.1 for client connection + const host = rawHost === '0.0.0.0' ? '127.0.0.1' : rawHost; + const apiKey = effectiveConfig?.APIKEY || 'ccr-default-key'; + const timeoutMs = effectiveConfig?.API_TIMEOUT_MS ?? 600000; + + return { + ANTHROPIC_AUTH_TOKEN: apiKey, + ANTHROPIC_BASE_URL: `http://${host}:${port}`, + NO_PROXY: '127.0.0.1', + DISABLE_TELEMETRY: 'true', + DISABLE_COST_WARNINGS: 'true', + API_TIMEOUT_MS: String(timeoutMs), + // Unset Bedrock to prevent conflicts + CLAUDE_CODE_USE_BEDROCK: undefined, + }; +} diff --git a/apps/server/src/lib/global-settings-accessor.ts b/apps/server/src/lib/global-settings-accessor.ts new file mode 100644 index 000000000..0bdfdee73 --- /dev/null +++ b/apps/server/src/lib/global-settings-accessor.ts @@ -0,0 +1,81 @@ +/** + * Global Settings Accessor + * + * Provides a centralized way to access global settings from anywhere in the codebase + * without needing to pass SettingsService through every function call. + * + * This is particularly useful for: + * - Provider-level code that needs global settings (like CCR routing) + * - Middleware that needs to check settings + * - Utility functions that need global configuration + * + * Usage: + * 1. Initialize at server startup: initGlobalSettingsAccessor(settingsService) + * 2. Access anywhere: const settings = await getGlobalSettingsFromAccessor() + * + * Design decisions: + * - Uses a module-level singleton pattern for simplicity + * - Returns null if not initialized (graceful degradation) + * - Async to support potential caching/optimization in the future + */ + +import type { GlobalSettings } from '@automaker/types'; + +/** + * Minimal interface for settings access + * This allows decoupling from the full SettingsService + */ +interface GlobalSettingsProvider { + getGlobalSettings(): Promise; +} + +/** + * Module-level singleton for settings access + */ +let settingsProvider: GlobalSettingsProvider | null = null; + +/** + * Initialize the global settings accessor + * + * Should be called once at server startup with the SettingsService instance. + * + * @param provider - The SettingsService or compatible provider + */ +export function initGlobalSettingsAccessor(provider: GlobalSettingsProvider): void { + settingsProvider = provider; +} + +/** + * Get global settings from the accessor + * + * Returns null if the accessor hasn't been initialized. + * Callers should handle the null case gracefully. + * + * @returns Promise resolving to GlobalSettings or null + */ +export async function getGlobalSettingsFromAccessor(): Promise { + if (!settingsProvider) { + return null; + } + return settingsProvider.getGlobalSettings(); +} + +/** + * Check if CCR is enabled in global settings + * + * Convenience function for the common CCR check pattern. + * Returns false if settings are not available. + * + * @returns Promise resolving to boolean + */ +export async function isCCREnabled(): Promise { + const settings = await getGlobalSettingsFromAccessor(); + return settings?.ccrEnabled ?? false; +} + +/** + * Reset the accessor (useful for testing) + */ +export function resetGlobalSettingsAccessor(): void { + settingsProvider = null; +} diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts index cfb590932..3b460b58d 100644 --- a/apps/server/src/providers/claude-provider.ts +++ b/apps/server/src/providers/claude-provider.ts @@ -8,6 +8,8 @@ import { query, type Options } from '@anthropic-ai/claude-agent-sdk'; import { BaseProvider } from './base-provider.js'; import { classifyError, getUserFriendlyErrorMessage, createLogger } from '@automaker/utils'; +import { getCCREnvFromStatus, getCCRStatus } from '../lib/ccr.js'; +import { isCCREnabled } from '../lib/global-settings-accessor.js'; const logger = createLogger('ClaudeProvider'); import { @@ -70,22 +72,61 @@ function isClaudeCompatibleProvider(config: ProviderConfig): config is ClaudeCom /** * Build environment for the SDK with only explicitly allowed variables. + * When CCR is enabled, uses CCR's configuration for routing. * When a provider/profile is provided, uses its configuration (clean switch - don't inherit from process.env). * When no provider is provided, uses direct Anthropic API settings from process.env. * - * Supports both: - * - ClaudeCompatibleProvider (new system with models[] array) - * - ClaudeApiProfile (legacy system with modelMappings) + * Priority order: + * 1. CCR enabled - routes through Claude Code Router proxy + * 2. ClaudeCompatibleProvider (new system with models[] array) + * 3. ClaudeApiProfile (legacy system with modelMappings) + * 4. Direct Anthropic API * * @param providerConfig - Optional provider configuration for alternative endpoint * @param credentials - Optional credentials object for resolving 'credentials' apiKeySource + * @param ccrEnabledOverride - Explicit CCR setting (if undefined, auto-resolves from global settings) */ -function buildEnv( +async function buildEnv( providerConfig?: ProviderConfig, - credentials?: Credentials -): Record { + credentials?: Credentials, + ccrEnabledOverride?: boolean +): Promise> { const env: Record = {}; + // Auto-resolve CCR setting if not explicitly provided + // This allows providers to work without callers needing to pass ccrEnabled + const ccrEnabled = ccrEnabledOverride ?? (await isCCREnabled()); + + // Priority 1: CCR enabled - use Claude Code Router for API routing + if (ccrEnabled) { + const status = await getCCRStatus(); + if (status.installed && status.running) { + const ccrEnv = getCCREnvFromStatus(status); + if (ccrEnv) { + logger.debug('[buildEnv] Using Claude Code Router (CCR) for API routing', { + baseUrl: ccrEnv.ANTHROPIC_BASE_URL, + }); + // Apply CCR environment variables + Object.assign(env, ccrEnv); + // Always add system vars from process.env + for (const key of SYSTEM_ENV_VARS) { + if (process.env[key]) { + env[key] = process.env[key]; + } + } + return env; + } + } else { + logger.warn('[buildEnv] CCR enabled but not available', { + installed: status.installed, + running: status.running, + error: status.error, + }); + // Fall through to other methods if CCR is not available + } + } + + // Priority 2/3: Provider configuration if (providerConfig) { // Use provider configuration (clean switch - don't inherit non-system vars from process.env) logger.debug('[buildEnv] Using provider configuration:', { @@ -213,6 +254,7 @@ export class ClaudeProvider extends BaseProvider { claudeApiProfile, claudeCompatibleProvider, credentials, + ccrEnabled, } = options; // Determine which provider config to use @@ -229,9 +271,8 @@ export class ClaudeProvider extends BaseProvider { maxTurns, cwd, // Pass only explicitly allowed environment variables to SDK - // When a provider is active, uses provider settings (clean switch) - // When no provider, uses direct Anthropic API (from process.env or CLI OAuth) - env: buildEnv(providerConfig, credentials), + // Priority: CCR (if enabled) > provider config > direct Anthropic API + env: await buildEnv(providerConfig, credentials, ccrEnabled), // Pass through allowedTools if provided by caller (decided by sdk-options.ts) ...(allowedTools && { allowedTools }), // AUTONOMOUS MODE: Always bypass permissions for fully autonomous operation @@ -284,6 +325,7 @@ export class ClaudeProvider extends BaseProvider { hasApiKey: !!envForSdk?.['ANTHROPIC_API_KEY'], hasAuthToken: !!envForSdk?.['ANTHROPIC_AUTH_TOKEN'], providerName: providerConfig?.name || '(direct Anthropic)', + ccrEnabled: !!ccrEnabled, maxTurns: sdkOptions.maxTurns, maxThinkingTokens: sdkOptions.maxThinkingTokens, }); diff --git a/apps/server/src/routes/settings/index.ts b/apps/server/src/routes/settings/index.ts index 6f6f6d408..f283211bf 100644 --- a/apps/server/src/routes/settings/index.ts +++ b/apps/server/src/routes/settings/index.ts @@ -24,6 +24,7 @@ import { createUpdateProjectHandler } from './routes/update-project.js'; import { createMigrateHandler } from './routes/migrate.js'; import { createStatusHandler } from './routes/status.js'; import { createDiscoverAgentsHandler } from './routes/discover-agents.js'; +import { createGetCCRStatusHandler } from './routes/get-ccr-status.js'; /** * Create settings router with all endpoints @@ -77,5 +78,8 @@ export function createSettingsRoutes(settingsService: SettingsService): Router { // Filesystem agents discovery (read-only) router.post('/agents/discover', createDiscoverAgentsHandler()); + // CCR (Claude Code Router) status + router.get('/ccr/status', createGetCCRStatusHandler()); + return router; } diff --git a/apps/server/src/routes/settings/routes/get-ccr-status.ts b/apps/server/src/routes/settings/routes/get-ccr-status.ts new file mode 100644 index 000000000..2b5ce12de --- /dev/null +++ b/apps/server/src/routes/settings/routes/get-ccr-status.ts @@ -0,0 +1,32 @@ +/** + * CCR Status Route Handler + * + * Returns the current status of Claude Code Router (CCR) installation and server. + */ + +import type { Request, Response } from 'express'; +import { getCCRStatus } from '../../../lib/ccr.js'; + +/** + * Create handler for GET /api/settings/ccr/status + * + * Returns CCR installation and running status: + * - installed: Whether CCR CLI is available + * - running: Whether CCR server is currently running + * - port: Port the server is listening on + * - apiEndpoint: Full API endpoint URL + */ +export function createGetCCRStatusHandler() { + return async (_req: Request, res: Response) => { + try { + const status = await getCCRStatus(); + res.json(status); + } catch (error) { + res.status(500).json({ + installed: false, + running: false, + error: error instanceof Error ? error.message : 'Failed to get CCR status', + }); + } + }; +} diff --git a/apps/ui/src/components/views/settings-view/claude/ccr-settings.tsx b/apps/ui/src/components/views/settings-view/claude/ccr-settings.tsx new file mode 100644 index 000000000..7308a3ca8 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/claude/ccr-settings.tsx @@ -0,0 +1,156 @@ +import { useState, useEffect } from 'react'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { Router, CheckCircle2, XCircle, Loader2, ExternalLink } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { apiGet } from '@/lib/api-fetch'; +import type { CCRStatus } from '@automaker/types'; + +interface CCRSettingsProps { + ccrEnabled: boolean; + onCcrEnabledChange: (enabled: boolean) => void; +} + +/** + * CCRSettings Component + * + * UI controls for Claude Code Router (CCR) integration: + * - Status indicator showing if CCR is installed and running + * - Toggle switch to enable/disable CCR API routing + */ +export function CCRSettings({ ccrEnabled, onCcrEnabledChange }: CCRSettingsProps) { + const [ccrStatus, setCcrStatus] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchStatus = async () => { + try { + const response = await apiGet('/api/settings/ccr/status'); + setCcrStatus(response); + } catch (error) { + setCcrStatus({ installed: false, running: false, error: 'Failed to check CCR status' }); + } finally { + setIsLoading(false); + } + }; + fetchStatus(); + }, []); + + const getStatusDisplay = () => { + if (isLoading) { + return ( +
+ + Checking CCR status... +
+ ); + } + + if (!ccrStatus?.installed) { + return ( +
+ + CCR not installed +
+ ); + } + + if (!ccrStatus?.running) { + return ( +
+ + + CCR installed but not running (run{' '} + ccr start) + +
+ ); + } + + return ( +
+ + CCR running on port {ccrStatus.port} +
+ ); + }; + + const canEnable = ccrStatus?.installed && ccrStatus?.running; + + // Logic: allow disabling even if CCR is down (to fix broken state), + // but only allow enabling if CCR is actually running. + const isSwitchDisabled = !ccrEnabled && !canEnable; + + return ( +
+
+
+
+ +
+

+ Claude Code Router +

+
+

+ Route API requests through CCR for model switching and cost optimization. +

+
+
+ {/* Status indicator */} +
+ Status + {getStatusDisplay()} +
+ + {/* Enable/Disable toggle */} +
+
+ +

+ When enabled, all Claude API requests are routed through CCR proxy. + {!ccrStatus?.installed && ( + <> + {' '} + + Install CCR + + + + )} +

+
+ +
+
+
+ ); +} diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx index 57b432d08..7bfaa7a2b 100644 --- a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx @@ -4,6 +4,7 @@ import { useSetupStore } from '@/store/setup-store'; import { useCliStatus } from '../hooks/use-cli-status'; import { ClaudeCliStatus } from '../cli-status/claude-cli-status'; import { ClaudeMdSettings } from '../claude/claude-md-settings'; +import { CCRSettings } from '../claude/ccr-settings'; import { ClaudeUsageSection } from '../api-keys/claude-usage-section'; import { SkillsSection } from './claude-settings-tab/skills-section'; import { SubagentsSection } from './claude-settings-tab/subagents-section'; @@ -12,7 +13,8 @@ import { ProviderToggle } from './provider-toggle'; import { Info } from 'lucide-react'; export function ClaudeSettingsTab() { - const { apiKeys, autoLoadClaudeMd, setAutoLoadClaudeMd } = useAppStore(); + const { apiKeys, autoLoadClaudeMd, setAutoLoadClaudeMd, ccrEnabled, setCcrEnabled } = + useAppStore(); const { claudeAuthStatus } = useSetupStore(); // Use CLI status hook @@ -47,6 +49,9 @@ export function ClaudeSettingsTab() { onRefresh={handleRefreshClaudeCli} /> + {/* Claude Code Router */} + + {/* Claude-compatible providers */} diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index b77fba5bc..05e0cf0d7 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -192,6 +192,8 @@ export function parseLocalStorageSettings(): Partial | null { state.enabledDynamicModelIds as GlobalSettings['enabledDynamicModelIds'], disabledProviders: (state.disabledProviders ?? []) as GlobalSettings['disabledProviders'], autoLoadClaudeMd: state.autoLoadClaudeMd as boolean, + skipSandboxWarning: state.skipSandboxWarning as boolean, + ccrEnabled: state.ccrEnabled as boolean, keyboardShortcuts: state.keyboardShortcuts as GlobalSettings['keyboardShortcuts'], mcpServers: state.mcpServers as GlobalSettings['mcpServers'], promptCustomization: state.promptCustomization as GlobalSettings['promptCustomization'], @@ -724,6 +726,7 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void { disabledProviders: settings.disabledProviders ?? [], autoLoadClaudeMd: settings.autoLoadClaudeMd ?? false, skipSandboxWarning: settings.skipSandboxWarning ?? false, + ccrEnabled: settings.ccrEnabled ?? false, keyboardShortcuts: { ...current.keyboardShortcuts, ...(settings.keyboardShortcuts as unknown as Partial), @@ -806,6 +809,7 @@ function buildSettingsUpdateFromStore(): Record { disabledProviders: state.disabledProviders, autoLoadClaudeMd: state.autoLoadClaudeMd, skipSandboxWarning: state.skipSandboxWarning, + ccrEnabled: state.ccrEnabled, keyboardShortcuts: state.keyboardShortcuts, mcpServers: state.mcpServers, promptCustomization: state.promptCustomization, diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index 8ede5600a..71b348672 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -68,6 +68,7 @@ const SETTINGS_FIELDS_TO_SYNC = [ 'enabledDynamicModelIds', 'disabledProviders', 'autoLoadClaudeMd', + 'ccrEnabled', 'keyboardShortcuts', 'mcpServers', 'defaultEditorCommand', @@ -661,6 +662,7 @@ export async function refreshSettingsFromServer(): Promise { enabledDynamicModelIds: sanitizedDynamicModelIds, disabledProviders: serverSettings.disabledProviders ?? [], autoLoadClaudeMd: serverSettings.autoLoadClaudeMd ?? false, + ccrEnabled: serverSettings.ccrEnabled ?? false, keyboardShortcuts: { ...currentAppState.keyboardShortcuts, ...(serverSettings.keyboardShortcuts as unknown as Partial< diff --git a/apps/ui/src/main.ts b/apps/ui/src/main.ts index 4d093106d..3f7754e9a 100644 --- a/apps/ui/src/main.ts +++ b/apps/ui/src/main.ts @@ -39,6 +39,12 @@ import { const logger = createLogger('Electron'); const serverLogger = createLogger('Server'); +// Fix for blank screen issues on Windows (especially when running as Administrator) +// This disables GPU acceleration which can cause rendering failures in some environments +if (process.platform === 'win32') { + app.disableHardwareAcceleration(); +} + // Development environment const isDev = !app.isPackaged; const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL; diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 63dd79601..6c73daaec 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -729,6 +729,7 @@ export interface AppState { // Claude Agent SDK Settings autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option skipSandboxWarning: boolean; // Skip the sandbox environment warning dialog on startup + ccrEnabled: boolean; // Enable Claude Code Router for API routing // MCP Servers mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use @@ -1211,6 +1212,7 @@ export interface AppActions { // Claude Agent SDK Settings actions setAutoLoadClaudeMd: (enabled: boolean) => Promise; setSkipSandboxWarning: (skip: boolean) => Promise; + setCcrEnabled: (enabled: boolean) => Promise; // Editor Configuration actions setDefaultEditorCommand: (command: string | null) => void; @@ -1490,6 +1492,7 @@ const initialState: AppState = { disabledProviders: [], // No providers disabled by default autoLoadClaudeMd: false, // Default to disabled (user must opt-in) skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog) + ccrEnabled: false, // Default to disabled (user must opt-in) mcpServers: [], // No MCP servers configured by default defaultEditorCommand: null, // Auto-detect: Cursor > VS Code > first available defaultTerminalId: null, // Integrated terminal by default @@ -2701,6 +2704,17 @@ export const useAppStore = create()((set, get) => ({ set({ skipSandboxWarning: previous }); } }, + setCcrEnabled: async (enabled) => { + const previous = get().ccrEnabled; + set({ ccrEnabled: enabled }); + // Sync to server settings file + const { syncSettingsToServer } = await import('@/hooks/use-settings-migration'); + const ok = await syncSettingsToServer(); + if (!ok) { + logger.error('Failed to sync ccrEnabled setting to server - reverting'); + set({ ccrEnabled: previous }); + } + }, // Editor Configuration actions setDefaultEditorCommand: (command) => set({ defaultEditorCommand: command }), diff --git a/libs/types/src/ccr.ts b/libs/types/src/ccr.ts new file mode 100644 index 000000000..2ec044ccc --- /dev/null +++ b/libs/types/src/ccr.ts @@ -0,0 +1,39 @@ +/** + * Claude Code Router (CCR) Types + * + * Types for CCR integration, status detection, and configuration. + */ + +/** + * CCRStatus - Status of Claude Code Router installation and server + */ +export interface CCRStatus { + /** Whether CCR CLI is installed and accessible */ + installed: boolean; + /** Whether CCR server is currently running */ + running: boolean; + /** Port the CCR server is listening on (if running) */ + port?: number; + /** API endpoint URL (e.g., http://127.0.0.1:13456) */ + apiEndpoint?: string; + /** Version of CCR if detected */ + version?: string; + /** Error message if status check failed */ + error?: string; +} + +/** + * CCRConfig - Parsed CCR configuration from ~/.claude-code-router/config.json + */ +export interface CCRConfig { + /** Host to bind the server to */ + HOST?: string; + /** Port to bind the server to */ + PORT?: number; + /** API key for authentication */ + APIKEY?: string; + /** API timeout in milliseconds */ + API_TIMEOUT_MS?: string | number; + /** Whether logging is enabled */ + LOG?: boolean; +} diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index a8f2644db..df4f7003e 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -329,3 +329,6 @@ export { PR_STATES, validatePRState } from './worktree.js'; // Terminal types export type { TerminalInfo } from './terminal.js'; + +// Claude Code Router (CCR) types +export type { CCRStatus, CCRConfig } from './ccr.js'; diff --git a/libs/types/src/provider.ts b/libs/types/src/provider.ts index 335000486..915dd1a17 100644 --- a/libs/types/src/provider.ts +++ b/libs/types/src/provider.ts @@ -233,6 +233,12 @@ export interface ExecuteOptions { * When a profile/provider has apiKeySource='credentials', the Anthropic key from this object is used. */ credentials?: Credentials; + /** + * Enable Claude Code Router (CCR) for API routing. + * When true, routes API requests through CCR proxy if installed and running. + * CCR must be installed and `ccr start` must be running for this to work. + */ + ccrEnabled?: boolean; } /** diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index 8a10a6f81..b334e5878 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -933,6 +933,10 @@ export interface GlobalSettings { /** Skip the sandbox environment warning dialog on startup */ skipSandboxWarning?: boolean; + // Claude Code Router (CCR) Settings + /** Enable Claude Code Router integration for API routing */ + ccrEnabled?: boolean; + // Codex CLI Settings /** Auto-load .codex/AGENTS.md instructions into Codex prompts */ codexAutoLoadAgents?: boolean; @@ -1301,6 +1305,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { lastSelectedSessionByProject: {}, autoLoadClaudeMd: true, skipSandboxWarning: false, + ccrEnabled: false, codexAutoLoadAgents: DEFAULT_CODEX_AUTO_LOAD_AGENTS, codexSandboxMode: DEFAULT_CODEX_SANDBOX_MODE, codexApprovalPolicy: DEFAULT_CODEX_APPROVAL_POLICY,