import chalk from 'chalk' import figures from 'figures' import * as React from 'react' import { color, Text } from '@anthropic/ink' import type { MCPServerConnection } from '../services/mcp/types.js' import { getAccountInformation, isClaudeAISubscriber } from './auth.js' import { getLargeMemoryFiles, getMemoryFiles, MAX_MEMORY_CHARACTER_COUNT, } from './claudemd.js' import { getDoctorDiagnostic } from './doctorDiagnostic.js' import { getAWSRegion, getDefaultVertexRegion, isEnvTruthy, } from './envUtils.js' import { getDisplayPath } from './file.js' import { formatNumber } from './format.js' import { getIdeClientName, type IDEExtensionInstallationStatus, isJetBrainsIde, toIDEDisplayName, } from './ide.js' import { getClaudeAiUserDefaultModelDescription, modelDisplayString, } from './model/model.js' import { getAPIProvider } from './model/providers.js' import { getMTLSConfig } from './mtls.js' import { checkInstall } from './nativeInstaller/index.js' import { getProxyUrl } from './proxy.js' import { SandboxManager } from './sandbox/sandbox-adapter.js' import { getSettingsWithAllErrors } from './settings/allErrors.js' import { getEnabledSettingSources, getSettingSourceDisplayNameCapitalized, } from './settings/constants.js' import { getManagedFileSettingsPresence, getPolicySettingsOrigin, getSettingsForSource, } from './settings/settings.js' import type { ThemeName } from './theme.js' export type Property = { label?: string value: React.ReactNode | Array } export type Diagnostic = React.ReactNode export function buildSandboxProperties(): Property[] { if (process.env.USER_TYPE !== 'ant') { return [] } const isSandboxed = SandboxManager.isSandboxingEnabled() return [ { label: 'Bash Sandbox', value: isSandboxed ? 'Enabled' : 'Disabled', }, ] } export function buildIDEProperties( mcpClients: MCPServerConnection[], ideInstallationStatus: IDEExtensionInstallationStatus | null = null, theme: ThemeName, ): Property[] { const ideClient = mcpClients?.find(client => client.name === 'ide') if (ideInstallationStatus) { const ideName = toIDEDisplayName(ideInstallationStatus.ideType) const pluginOrExtension = isJetBrainsIde(ideInstallationStatus.ideType) ? 'plugin' : 'extension' if (ideInstallationStatus.error) { return [ { label: 'IDE', value: ( {color('error', theme)(figures.cross)} Error installing {ideName}{' '} {pluginOrExtension}: {ideInstallationStatus.error} {'\n'}Please restart your IDE and try again. ), }, ] } if (ideInstallationStatus.installed) { if (ideClient && ideClient.type === 'connected') { if ( ideInstallationStatus.installedVersion !== ideClient.serverInfo?.version ) { return [ { label: 'IDE', value: `Connected to ${ideName} ${pluginOrExtension} version ${ideInstallationStatus.installedVersion} (server version: ${ideClient.serverInfo?.version})`, }, ] } else { return [ { label: 'IDE', value: `Connected to ${ideName} ${pluginOrExtension} version ${ideInstallationStatus.installedVersion}`, }, ] } } else { return [ { label: 'IDE', value: `Installed ${ideName} ${pluginOrExtension}`, }, ] } } } else if (ideClient) { const ideName = getIdeClientName(ideClient) ?? 'IDE' if (ideClient.type === 'connected') { return [ { label: 'IDE', value: `Connected to ${ideName} extension`, }, ] } else { return [ { label: 'IDE', value: `${color('error', theme)(figures.cross)} Not connected to ${ideName}`, }, ] } } return [] } export function buildMcpProperties( clients: MCPServerConnection[] = [], theme: ThemeName, ): Property[] { const servers = clients.filter(client => client.name !== 'ide') if (!servers.length) { return [] } // Summary instead of a full server list — 20+ servers wrapped onto many // rows, dominating the Status pane. Show counts by state + /mcp hint. const byState = { connected: 0, pending: 0, needsAuth: 0, failed: 0 } for (const s of servers) { if (s.type === 'connected') byState.connected++ else if (s.type === 'pending') byState.pending++ else if (s.type === 'needs-auth') byState.needsAuth++ else byState.failed++ } const parts: string[] = [] if (byState.connected) parts.push(color('success', theme)(`${byState.connected} connected`)) if (byState.needsAuth) parts.push(color('warning', theme)(`${byState.needsAuth} need auth`)) if (byState.pending) parts.push(color('inactive', theme)(`${byState.pending} pending`)) if (byState.failed) parts.push(color('error', theme)(`${byState.failed} failed`)) return [ { label: 'MCP servers', value: `${parts.join(', ')} ${color('inactive', theme)('· /mcp')}`, }, ] } export async function buildMemoryDiagnostics(): Promise { const files = await getMemoryFiles() const largeFiles = getLargeMemoryFiles(files) const diagnostics: Diagnostic[] = [] largeFiles.forEach(file => { const displayPath = getDisplayPath(file.path) diagnostics.push( `Large ${displayPath} will impact performance (${formatNumber(file.content.length)} chars > ${formatNumber(MAX_MEMORY_CHARACTER_COUNT)})`, ) }) return diagnostics } export function buildSettingSourcesProperties(): Property[] { const enabledSources = getEnabledSettingSources() // Filter to only sources that actually have settings loaded const sourcesWithSettings = enabledSources.filter(source => { const settings = getSettingsForSource(source) return settings !== null && Object.keys(settings).length > 0 }) // Map internal names to user-friendly names // For policySettings, distinguish between remote and local (or skip if neither exists) const sourceNames = sourcesWithSettings .map(source => { if (source === 'policySettings') { const origin = getPolicySettingsOrigin() if (origin === null) { return null // Skip - no policy settings exist } switch (origin) { case 'remote': return 'Enterprise managed settings (remote)' case 'plist': return 'Enterprise managed settings (plist)' case 'hklm': return 'Enterprise managed settings (HKLM)' case 'file': { const { hasBase, hasDropIns } = getManagedFileSettingsPresence() if (hasBase && hasDropIns) { return 'Enterprise managed settings (file + drop-ins)' } if (hasDropIns) { return 'Enterprise managed settings (drop-ins)' } return 'Enterprise managed settings (file)' } case 'hkcu': return 'Enterprise managed settings (HKCU)' } } return getSettingSourceDisplayNameCapitalized(source) }) .filter((name): name is string => name !== null) return [ { label: 'Setting sources', value: sourceNames, }, ] } export async function buildInstallationDiagnostics(): Promise { const installWarnings = await checkInstall() return installWarnings.map(warning => warning.message) } export async function buildInstallationHealthDiagnostics(): Promise< Diagnostic[] > { const diagnostic = await getDoctorDiagnostic() const items: Diagnostic[] = [] const { errors: validationErrors } = getSettingsWithAllErrors() if (validationErrors.length > 0) { const invalidFiles = Array.from( new Set(validationErrors.map(error => error.file)), ) const fileList = invalidFiles.join(', ') items.push( `Found invalid settings files: ${fileList}. They will be ignored.`, ) } // Add warnings from doctor diagnostic (includes leftover installations, config mismatches, etc.) diagnostic.warnings.forEach(warning => { items.push(warning.issue) }) if (diagnostic.hasUpdatePermissions === false) { items.push('No write permissions for auto-updates (requires sudo)') } return items } export function buildAccountProperties(): Property[] { const accountInfo = getAccountInformation() if (!accountInfo) { return [] } const properties: Property[] = [] if (accountInfo.subscription) { properties.push({ label: 'Login method', value: `${accountInfo.subscription} Account`, }) } if (accountInfo.tokenSource) { properties.push({ label: 'Auth token', value: accountInfo.tokenSource, }) } if (accountInfo.apiKeySource) { properties.push({ label: 'API key', value: accountInfo.apiKeySource, }) } // Hide sensitive account info in demo mode if (accountInfo.organization && !process.env.IS_DEMO) { properties.push({ label: 'Organization', value: accountInfo.organization, }) } if (accountInfo.email && !process.env.IS_DEMO) { properties.push({ label: 'Email', value: accountInfo.email, }) } return properties } export function buildAPIProviderProperties(): Property[] { const apiProvider = getAPIProvider() const properties: Property[] = [] if (apiProvider !== 'firstParty') { const providerLabel = { bedrock: 'AWS Bedrock', vertex: 'Google Vertex AI', foundry: 'Microsoft Foundry', gemini: 'Gemini API', grok: 'Grok API', openai: 'OpenAI API', }[apiProvider] properties.push({ label: 'API provider', value: providerLabel, }) } if (apiProvider === 'firstParty') { const anthropicBaseUrl = process.env.ANTHROPIC_BASE_URL if (anthropicBaseUrl) { properties.push({ label: 'Anthropic base URL', value: anthropicBaseUrl, }) } } else if (apiProvider === 'bedrock') { const bedrockBaseUrl = process.env.BEDROCK_BASE_URL if (bedrockBaseUrl) { properties.push({ label: 'Bedrock base URL', value: bedrockBaseUrl, }) } properties.push({ label: 'AWS region', value: getAWSRegion(), }) if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH)) { properties.push({ value: 'AWS auth skipped', }) } } else if (apiProvider === 'vertex') { const vertexBaseUrl = process.env.VERTEX_BASE_URL if (vertexBaseUrl) { properties.push({ label: 'Vertex base URL', value: vertexBaseUrl, }) } const gcpProject = process.env.ANTHROPIC_VERTEX_PROJECT_ID if (gcpProject) { properties.push({ label: 'GCP project', value: gcpProject, }) } properties.push({ label: 'Default region', value: getDefaultVertexRegion(), }) if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_VERTEX_AUTH)) { properties.push({ value: 'GCP auth skipped', }) } } else if (apiProvider === 'foundry') { const foundryBaseUrl = process.env.ANTHROPIC_FOUNDRY_BASE_URL if (foundryBaseUrl) { properties.push({ label: 'Microsoft Foundry base URL', value: foundryBaseUrl, }) } const foundryResource = process.env.ANTHROPIC_FOUNDRY_RESOURCE if (foundryResource) { properties.push({ label: 'Microsoft Foundry resource', value: foundryResource, }) } if (isEnvTruthy(process.env.CLAUDE_CODE_SKIP_FOUNDRY_AUTH)) { properties.push({ value: 'Microsoft Foundry auth skipped', }) } } else if (apiProvider === 'gemini') { const geminiBaseUrl = process.env.GEMINI_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta' properties.push({ label: 'Gemini base URL', value: geminiBaseUrl, }) } else if (apiProvider === 'grok') { const grokBaseUrl = process.env.GROK_BASE_URL properties.push({ label: 'Grok base URL', value: grokBaseUrl, }) } else if (apiProvider === 'openai') { const openaiBaseUrl = process.env.OPENAI_BASE_URL properties.push({ label: 'OpenAI base URL', value: openaiBaseUrl, }) } const proxyUrl = getProxyUrl() if (proxyUrl) { properties.push({ label: 'Proxy', value: proxyUrl, }) } const mtlsConfig = getMTLSConfig() if (process.env.NODE_EXTRA_CA_CERTS) { properties.push({ label: 'Additional CA cert(s)', value: process.env.NODE_EXTRA_CA_CERTS, }) } if (mtlsConfig) { if (mtlsConfig.cert && process.env.CLAUDE_CODE_CLIENT_CERT) { properties.push({ label: 'mTLS client cert', value: process.env.CLAUDE_CODE_CLIENT_CERT, }) } if (mtlsConfig.key && process.env.CLAUDE_CODE_CLIENT_KEY) { properties.push({ label: 'mTLS client key', value: process.env.CLAUDE_CODE_CLIENT_KEY, }) } } return properties } export function getModelDisplayLabel(mainLoopModel: string | null): string { let modelLabel = modelDisplayString(mainLoopModel) if (mainLoopModel === null && isClaudeAISubscriber()) { const description = getClaudeAiUserDefaultModelDescription() modelLabel = `${chalk.bold('Default')} ${description}` } return modelLabel }