From 3090e477e3ac753b3fe8365f34d0c6a57facea7c Mon Sep 17 00:00:00 2001 From: asepharyana Date: Thu, 3 Sep 2026 17:39:34 +0700 Subject: [PATCH] Add support for reading Claude Code settings from ~/.claude/settings.json --- src/config.ts | 17 +++++++++++++++-- src/providers.ts | 36 +++++++++++++++++++++++++++++++++++- src/ui/Onboard.tsx | 15 +++++++++++---- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/config.ts b/src/config.ts index 9fb72d4..26f6e1a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -7,6 +7,7 @@ import { join } from 'node:path'; import { withFallback, type FallbackEvent } from './fallback'; import type { McpServerConfig } from './mcp'; import { parsePermissions, type PermissionConfig } from './permission'; +import { readClaudeCodeSettings } from './providers'; import { isToolSetName, type ToolSetName } from './tools'; export type ProviderName = 'anthropic' | 'openai'; @@ -83,11 +84,23 @@ export async function loadConfig(): Promise { const envProvider = process.env['SHIRO_PROVIDER']; const provider = isProvider(envProvider) ? envProvider : isProvider(file.provider) ? file.provider : 'anthropic'; + // When using the claude-code preset, read from ~/.claude/settings.json + const isClaudeCode = file.presetId === 'claude-code'; + const claudeSettings = isClaudeCode ? await readClaudeCodeSettings() : {}; + return { provider, model: process.env['SHIRO_MODEL'] ?? file.model ?? DEFAULT_MODEL[provider], - baseURL: process.env['SHIRO_BASE_URL'] ?? file.baseURL ?? DEFAULT_BASE_URL[provider], - apiKey: process.env['SHIRO_API_KEY'] ?? file.apiKey ?? process.env[ENV_KEY[provider]], + baseURL: + process.env['SHIRO_BASE_URL'] ?? + file.baseURL ?? + claudeSettings.baseURL ?? + DEFAULT_BASE_URL[provider], + apiKey: + process.env['SHIRO_API_KEY'] ?? + file.apiKey ?? + claudeSettings.apiKey ?? + process.env[ENV_KEY[provider]], ...(file.presetId ? { presetId: file.presetId } : {}), ...(file.maxRetries !== undefined ? { maxRetries: file.maxRetries } : {}), ...(file.agent ? { agent: file.agent } : {}), diff --git a/src/providers.ts b/src/providers.ts index 90dabae..546fed0 100644 --- a/src/providers.ts +++ b/src/providers.ts @@ -1,5 +1,31 @@ +import { homedir } from 'node:os'; +import { join } from 'node:path'; import type { ProviderName } from './config'; +/** + * Reads ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY from ~/.claude/settings.json + * so the claude-code preset reuses the endpoint and key Claude Code already has + * configured, rather than asking the user to type them again. + */ +export async function readClaudeCodeSettings(): Promise<{ baseURL?: string; apiKey?: string }> { + try { + const settingsPath = join(homedir(), '.claude', 'settings.json'); + const f = Bun.file(settingsPath); + if (!(await f.exists())) return {}; + const parsed: unknown = await f.json(); + if (!parsed || typeof parsed !== 'object') return {}; + const env = (parsed as { env?: unknown }).env; + if (!env || typeof env !== 'object') return {}; + const envObj = env as Record; + return { + baseURL: typeof envObj['ANTHROPIC_BASE_URL'] === 'string' ? envObj['ANTHROPIC_BASE_URL'] : undefined, + apiKey: typeof envObj['ANTHROPIC_API_KEY'] === 'string' ? envObj['ANTHROPIC_API_KEY'] : undefined, + }; + } catch { + return {}; + } +} + export type ProviderPreset = { id: string; label: string; @@ -22,7 +48,15 @@ export const PRESETS: ProviderPreset[] = [ baseURL: 'https://api.anthropic.com/v1', envKey: 'ANTHROPIC_API_KEY', keyHint: 'sk-ant-...', - fallbackModels: ['claude-sonnet-4-5', 'claude-opus-4-1', 'claude-haiku-4-5'], + fallbackModels: ['claude-sonnet-5', 'claude-opus-5', 'claude-haiku-4-5-20251001'], + }, + { + id: 'claude-code', + label: 'Claude Code (via ~/.claude/settings.json)', + kind: 'anthropic', + baseURL: '', // Read from ~/.claude/settings.json + envKey: 'ANTHROPIC_API_KEY', + fallbackModels: ['claude-sonnet-5', 'claude-opus-5', 'claude-haiku-4-5-20251001'], }, { id: 'openai', diff --git a/src/ui/Onboard.tsx b/src/ui/Onboard.tsx index 37b8b83..ebd9dc6 100644 --- a/src/ui/Onboard.tsx +++ b/src/ui/Onboard.tsx @@ -4,7 +4,7 @@ import TextInput from 'ink-text-input'; import Spinner from 'ink-spinner'; import React, { useCallback, useState } from 'react'; import type { Config, ProviderName } from '../config'; -import { fetchModels, PRESETS, type ProviderPreset } from '../providers'; +import { fetchModels, PRESETS, readClaudeCodeSettings, type ProviderPreset } from '../providers'; export type OnboardResult = { presetId: string; @@ -65,9 +65,9 @@ export function Onboard({ ); const afterBaseUrl = useCallback( - (preset: ProviderPreset, baseURL: string) => { + (preset: ProviderPreset, baseURL: string, settingsKey?: string) => { const fromEnv = preset.envKey ? process.env[preset.envKey] : undefined; - const key = preset.keyless ? 'local' : (fromEnv ?? ''); + const key = preset.keyless ? 'local' : (settingsKey ?? fromEnv ?? ''); if (key) return void loadModels(preset, baseURL, key); setDraft(''); setStep({ name: 'api-key', preset, baseURL }); @@ -77,9 +77,16 @@ export function Onboard({ const pickProvider = useCallback( (preset: ProviderPreset) => { + // The claude-code preset has no fixed endpoint: read the base URL and key + // from ~/.claude/settings.json, falling back to asking either if the file + // is absent or lacks the variable. if (preset.baseURL) return afterBaseUrl(preset, preset.baseURL); + void readClaudeCodeSettings().then(({ baseURL, apiKey }) => + baseURL + ? afterBaseUrl(preset, baseURL, apiKey) + : setStep({ name: 'base-url', preset }), + ); setDraft(''); - setStep({ name: 'base-url', preset }); }, [afterBaseUrl], );