Add support for reading Claude Code settings from ~/.claude/settings.json
This commit is contained in:
1 parent
6452299a45
commit
3090e477e3
3 files changed
+61
-7
No files matched your search
+15
-2
@@ -7,6 +7,7 @@ import { join } from 'node:path';
|
|||||||
import { withFallback, type FallbackEvent } from './fallback';
|
import { withFallback, type FallbackEvent } from './fallback';
|
||||||
import type { McpServerConfig } from './mcp';
|
import type { McpServerConfig } from './mcp';
|
||||||
import { parsePermissions, type PermissionConfig } from './permission';
|
import { parsePermissions, type PermissionConfig } from './permission';
|
||||||
|
import { readClaudeCodeSettings } from './providers';
|
||||||
import { isToolSetName, type ToolSetName } from './tools';
|
import { isToolSetName, type ToolSetName } from './tools';
|
||||||
|
|
||||||
export type ProviderName = 'anthropic' | 'openai';
|
export type ProviderName = 'anthropic' | 'openai';
|
||||||
@@ -83,11 +84,23 @@ export async function loadConfig(): Promise<Config> {
|
|||||||
const envProvider = process.env['SHIRO_PROVIDER'];
|
const envProvider = process.env['SHIRO_PROVIDER'];
|
||||||
const provider = isProvider(envProvider) ? envProvider : isProvider(file.provider) ? file.provider : 'anthropic';
|
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 {
|
return {
|
||||||
provider,
|
provider,
|
||||||
model: process.env['SHIRO_MODEL'] ?? file.model ?? DEFAULT_MODEL[provider],
|
model: process.env['SHIRO_MODEL'] ?? file.model ?? DEFAULT_MODEL[provider],
|
||||||
baseURL: process.env['SHIRO_BASE_URL'] ?? file.baseURL ?? DEFAULT_BASE_URL[provider],
|
baseURL:
|
||||||
apiKey: process.env['SHIRO_API_KEY'] ?? file.apiKey ?? process.env[ENV_KEY[provider]],
|
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.presetId ? { presetId: file.presetId } : {}),
|
||||||
...(file.maxRetries !== undefined ? { maxRetries: file.maxRetries } : {}),
|
...(file.maxRetries !== undefined ? { maxRetries: file.maxRetries } : {}),
|
||||||
...(file.agent ? { agent: file.agent } : {}),
|
...(file.agent ? { agent: file.agent } : {}),
|
||||||
|
|||||||
+35
-1
@@ -1,5 +1,31 @@
|
|||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
import type { ProviderName } from './config';
|
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<string, unknown>;
|
||||||
|
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 = {
|
export type ProviderPreset = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -22,7 +48,15 @@ export const PRESETS: ProviderPreset[] = [
|
|||||||
baseURL: 'https://api.anthropic.com/v1',
|
baseURL: 'https://api.anthropic.com/v1',
|
||||||
envKey: 'ANTHROPIC_API_KEY',
|
envKey: 'ANTHROPIC_API_KEY',
|
||||||
keyHint: 'sk-ant-...',
|
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',
|
id: 'openai',
|
||||||
|
|||||||
+11
-4
@@ -4,7 +4,7 @@ import TextInput from 'ink-text-input';
|
|||||||
import Spinner from 'ink-spinner';
|
import Spinner from 'ink-spinner';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import type { Config, ProviderName } from '../config';
|
import type { Config, ProviderName } from '../config';
|
||||||
import { fetchModels, PRESETS, type ProviderPreset } from '../providers';
|
import { fetchModels, PRESETS, readClaudeCodeSettings, type ProviderPreset } from '../providers';
|
||||||
|
|
||||||
export type OnboardResult = {
|
export type OnboardResult = {
|
||||||
presetId: string;
|
presetId: string;
|
||||||
@@ -65,9 +65,9 @@ export function Onboard({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const afterBaseUrl = useCallback(
|
const afterBaseUrl = useCallback(
|
||||||
(preset: ProviderPreset, baseURL: string) => {
|
(preset: ProviderPreset, baseURL: string, settingsKey?: string) => {
|
||||||
const fromEnv = preset.envKey ? process.env[preset.envKey] : undefined;
|
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);
|
if (key) return void loadModels(preset, baseURL, key);
|
||||||
setDraft('');
|
setDraft('');
|
||||||
setStep({ name: 'api-key', preset, baseURL });
|
setStep({ name: 'api-key', preset, baseURL });
|
||||||
@@ -77,9 +77,16 @@ export function Onboard({
|
|||||||
|
|
||||||
const pickProvider = useCallback(
|
const pickProvider = useCallback(
|
||||||
(preset: ProviderPreset) => {
|
(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);
|
if (preset.baseURL) return afterBaseUrl(preset, preset.baseURL);
|
||||||
|
void readClaudeCodeSettings().then(({ baseURL, apiKey }) =>
|
||||||
|
baseURL
|
||||||
|
? afterBaseUrl(preset, baseURL, apiKey)
|
||||||
|
: setStep({ name: 'base-url', preset }),
|
||||||
|
);
|
||||||
setDraft('');
|
setDraft('');
|
||||||
setStep({ name: 'base-url', preset });
|
|
||||||
},
|
},
|
||||||
[afterBaseUrl],
|
[afterBaseUrl],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user