mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat(cli-settings): support multi-provider settings for Claude, Codex, and Gemini
Decouple CLI settings architecture from Claude-only to support multiple
providers. Each provider has independent settings UI and backend handling.
- Add CliProvider type discriminator ('claude' | 'codex' | 'gemini')
- Add CodexCliSettings (profile, authJson, configToml) and GeminiCliSettings types
- Update EndpointSettings with provider field (defaults 'claude' for backward compat)
- Refactor CliSettingsModal with provider selector and provider-specific forms
- Remove includeCoAuthoredBy field across all layers
- Extend CliConfigModal to show Config Profile for all tools (not just claude)
- Add provider-aware argument injection in cli-session-manager (--settings/--profile/env)
- Rename addClaudeCustomEndpoint to addCustomEndpoint (old name kept as deprecated alias)
- Replace providerBasedCount/directCount with per-provider counts in useCliSettings hook
- Update CliSettingsList with provider badges and per-provider stat cards
- Add Codex and Gemini test cases for validateSettings and createDefaultSettings
This commit is contained in:
@@ -10,7 +10,8 @@ import { join } from 'path';
|
||||
import * as os from 'os';
|
||||
import { getCCWHome, ensureStorageDir } from './storage-paths.js';
|
||||
import {
|
||||
ClaudeCliSettings,
|
||||
CliSettings,
|
||||
CliProvider,
|
||||
EndpointSettings,
|
||||
SettingsListResponse,
|
||||
SettingsOperationResult,
|
||||
@@ -19,8 +20,8 @@ import {
|
||||
createDefaultSettings
|
||||
} from '../types/cli-settings.js';
|
||||
import {
|
||||
addClaudeCustomEndpoint,
|
||||
removeClaudeCustomEndpoint
|
||||
addCustomEndpoint,
|
||||
removeCustomEndpoint
|
||||
} from '../tools/claude-cli-tools.js';
|
||||
|
||||
/**
|
||||
@@ -108,6 +109,7 @@ export function saveEndpointSettings(request: SaveEndpointRequest): SettingsOper
|
||||
id: endpointId,
|
||||
name: request.name,
|
||||
description: request.description,
|
||||
provider: request.provider || 'claude',
|
||||
enabled: request.enabled ?? true,
|
||||
createdAt: existing?.createdAt || now,
|
||||
updatedAt: now
|
||||
@@ -129,13 +131,13 @@ export function saveEndpointSettings(request: SaveEndpointRequest): SettingsOper
|
||||
// Merge user-provided tags with cli-wrapper tag for proper type registration
|
||||
const userTags = request.settings.tags || [];
|
||||
const tags = [...new Set([...userTags, 'cli-wrapper'])]; // Dedupe and ensure cli-wrapper tag
|
||||
addClaudeCustomEndpoint(projectDir, {
|
||||
addCustomEndpoint(projectDir, {
|
||||
id: endpointId,
|
||||
name: request.name,
|
||||
enabled: request.enabled ?? true,
|
||||
tags,
|
||||
availableModels: request.settings.availableModels,
|
||||
settingsFile: request.settings.settingsFile
|
||||
settingsFile: 'settingsFile' in request.settings ? (request.settings as any).settingsFile : undefined
|
||||
});
|
||||
console.log(`[CliSettings] Synced endpoint ${endpointId} to cli-tools.json tools (cli-wrapper)`);
|
||||
} catch (syncError) {
|
||||
@@ -182,13 +184,15 @@ export function loadEndpointSettings(endpointId: string): EndpointSettings | nul
|
||||
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
|
||||
if (!validateSettings(settings)) {
|
||||
const provider = (metadata as any).provider || 'claude';
|
||||
if (!validateSettings(settings, provider)) {
|
||||
console.error(`[CliSettings] Invalid settings format for ${endpointId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
provider,
|
||||
settings
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -225,7 +229,7 @@ export function deleteEndpointSettings(endpointId: string): SettingsOperationRes
|
||||
// Step 3: Remove from cli-tools.json tools (api-endpoint type)
|
||||
try {
|
||||
const projectDir = os.homedir();
|
||||
removeClaudeCustomEndpoint(projectDir, endpointId);
|
||||
removeCustomEndpoint(projectDir, endpointId);
|
||||
console.log(`[CliSettings] Removed endpoint ${endpointId} from cli-tools.json tools`);
|
||||
} catch (syncError) {
|
||||
console.warn(`[CliSettings] Failed to remove from cli-tools.json: ${syncError}`);
|
||||
@@ -262,10 +266,12 @@ export function listAllSettings(): SettingsListResponse {
|
||||
|
||||
try {
|
||||
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
const provider: CliProvider = (metadata as any).provider || 'claude';
|
||||
|
||||
if (validateSettings(settings)) {
|
||||
if (validateSettings(settings, provider)) {
|
||||
endpoints.push({
|
||||
...metadata,
|
||||
provider,
|
||||
settings
|
||||
});
|
||||
}
|
||||
@@ -315,13 +321,13 @@ export function toggleEndpointEnabled(endpointId: string, enabled: boolean): Set
|
||||
const endpoint = loadEndpointSettings(endpointId);
|
||||
const userTags = endpoint?.settings.tags || [];
|
||||
const tags = [...new Set([...userTags, 'cli-wrapper'])]; // Dedupe and ensure cli-wrapper tag
|
||||
addClaudeCustomEndpoint(projectDir, {
|
||||
addCustomEndpoint(projectDir, {
|
||||
id: endpointId,
|
||||
name: metadata.name,
|
||||
enabled: enabled,
|
||||
tags,
|
||||
availableModels: endpoint?.settings.availableModels,
|
||||
settingsFile: endpoint?.settings.settingsFile
|
||||
settingsFile: endpoint?.settings && 'settingsFile' in endpoint.settings ? (endpoint.settings as any).settingsFile : undefined
|
||||
});
|
||||
console.log(`[CliSettings] Synced endpoint ${endpointId} enabled=${enabled} to cli-tools.json tools`);
|
||||
} catch (syncError) {
|
||||
@@ -368,9 +374,8 @@ export function createSettingsFromProvider(provider: {
|
||||
name?: string;
|
||||
}, options?: {
|
||||
model?: string;
|
||||
includeCoAuthoredBy?: boolean;
|
||||
}): ClaudeCliSettings {
|
||||
const settings = createDefaultSettings();
|
||||
}): CliSettings {
|
||||
const settings = createDefaultSettings('claude');
|
||||
|
||||
// Map provider credentials to env
|
||||
if (provider.apiKey) {
|
||||
@@ -384,9 +389,6 @@ export function createSettingsFromProvider(provider: {
|
||||
if (options?.model) {
|
||||
settings.model = options.model;
|
||||
}
|
||||
if (options?.includeCoAuthoredBy !== undefined) {
|
||||
settings.includeCoAuthoredBy = options.includeCoAuthoredBy;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ export async function handleCliSettingsRoutes(ctx: RouteContext): Promise<boolea
|
||||
if (!request.settings || !request.settings.env) {
|
||||
return { error: 'settings.env is required', status: 400 };
|
||||
}
|
||||
// Deep validation of settings object
|
||||
if (!validateSettings(request.settings)) {
|
||||
// Deep validation of settings object (provider-aware)
|
||||
if (!validateSettings(request.settings, request.provider)) {
|
||||
return { error: 'Invalid settings object format', status: 400 };
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import { getCliSessionPolicy } from './cli-session-policy.js';
|
||||
import { appendCliSessionAudit } from './cli-session-audit.js';
|
||||
import { getLaunchConfig } from './cli-launch-registry.js';
|
||||
import { assembleInstruction, type InstructionType } from './cli-instruction-assembler.js';
|
||||
import { loadEndpointSettings } from '../../config/cli-settings-manager.js';
|
||||
import { getToolConfig } from '../../tools/claude-cli-tools.js';
|
||||
|
||||
export interface CliSession {
|
||||
sessionKey: string;
|
||||
@@ -41,6 +43,8 @@ export interface CreateCliSessionOptions {
|
||||
resumeKey?: string;
|
||||
/** Launch mode for native CLI sessions. */
|
||||
launchMode?: 'default' | 'yolo';
|
||||
/** Settings endpoint ID for injecting env vars and settings into CLI process. */
|
||||
settingsEndpointId?: string;
|
||||
}
|
||||
|
||||
export interface ExecuteInCliSessionOptions {
|
||||
@@ -221,15 +225,56 @@ export class CliSessionManager {
|
||||
let args: string[];
|
||||
let cliTool: string | undefined;
|
||||
|
||||
// Load settings endpoint env vars and extra args if specified
|
||||
let endpointEnv: Record<string, string> = {};
|
||||
let endpointExtraArgs: string[] = [];
|
||||
|
||||
if (options.settingsEndpointId) {
|
||||
try {
|
||||
const endpoint = loadEndpointSettings(options.settingsEndpointId);
|
||||
if (endpoint) {
|
||||
// Merge env vars (skip undefined/empty values)
|
||||
for (const [key, value] of Object.entries(endpoint.settings.env)) {
|
||||
if (value !== undefined && value !== '') {
|
||||
endpointEnv[key] = value;
|
||||
}
|
||||
}
|
||||
// Provider-specific argument injection
|
||||
const provider = endpoint.provider || 'claude';
|
||||
if (provider === 'claude' && 'settingsFile' in endpoint.settings && endpoint.settings.settingsFile) {
|
||||
endpointExtraArgs.push('--settings', endpoint.settings.settingsFile);
|
||||
} else if (provider === 'codex' && 'profile' in endpoint.settings && endpoint.settings.profile) {
|
||||
endpointExtraArgs.push('--profile', endpoint.settings.profile);
|
||||
}
|
||||
// Gemini: env vars only, no extra CLI flags
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[CliSessionManager] Failed to load settings endpoint:', options.settingsEndpointId, err);
|
||||
}
|
||||
} else if (options.tool) {
|
||||
// Fallback: read settingsFile from cli-tools.json for the tool
|
||||
try {
|
||||
const toolConfig = getToolConfig(this.projectRoot, options.tool);
|
||||
if (options.tool === 'claude' && toolConfig.settingsFile) {
|
||||
endpointExtraArgs.push('--settings', toolConfig.settingsFile);
|
||||
}
|
||||
} catch (err) {
|
||||
// Non-fatal: continue without settings file
|
||||
}
|
||||
}
|
||||
|
||||
if (options.tool) {
|
||||
// Native CLI interactive session: spawn the CLI process directly
|
||||
const launchMode = options.launchMode ?? 'default';
|
||||
const config = getLaunchConfig(options.tool, launchMode);
|
||||
cliTool = options.tool;
|
||||
|
||||
// Append endpoint-specific extra args (e.g., --settings for claude)
|
||||
const allArgs = [...config.args, ...endpointExtraArgs];
|
||||
|
||||
// Build the full command string with arguments
|
||||
const fullCommand = config.args.length > 0
|
||||
? `${config.command} ${config.args.join(' ')}`
|
||||
const fullCommand = allArgs.length > 0
|
||||
? `${config.command} ${allArgs.join(' ')}`
|
||||
: config.command;
|
||||
|
||||
// On Windows, CLI tools installed via npm are typically .cmd files.
|
||||
@@ -275,7 +320,7 @@ export class CliSessionManager {
|
||||
// Unix: direct spawn works for most CLI tools
|
||||
shellKind = 'git-bash';
|
||||
file = config.command;
|
||||
args = config.args;
|
||||
args = allArgs;
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -289,6 +334,12 @@ export class CliSessionManager {
|
||||
args = picked.args;
|
||||
}
|
||||
|
||||
// Merge endpoint env vars with process.env (endpoint overrides process.env)
|
||||
const spawnEnv: Record<string, string> = {
|
||||
...(process.env as Record<string, string>),
|
||||
...endpointEnv,
|
||||
};
|
||||
|
||||
let pty: nodePty.IPty;
|
||||
try {
|
||||
pty = nodePty.spawn(file, args, {
|
||||
@@ -296,7 +347,7 @@ export class CliSessionManager {
|
||||
cols: options.cols ?? 120,
|
||||
rows: options.rows ?? 30,
|
||||
cwd: workingDir,
|
||||
env: process.env as Record<string, string>
|
||||
env: spawnEnv
|
||||
});
|
||||
} catch (spawnError: unknown) {
|
||||
const errorMsg = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
||||
|
||||
@@ -859,12 +859,12 @@ export function removeClaudeApiEndpoint(
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use addClaudeApiEndpoint instead
|
||||
* Adds tool to config based on tags:
|
||||
* Add a custom CLI settings endpoint tool to cli-tools.json
|
||||
* Adds tool based on tags:
|
||||
* - cli-wrapper tag -> type: 'cli-wrapper'
|
||||
* - others -> type: 'api-endpoint'
|
||||
*/
|
||||
export function addClaudeCustomEndpoint(
|
||||
export function addCustomEndpoint(
|
||||
projectDir: string,
|
||||
endpoint: { id: string; name: string; enabled: boolean; tags?: string[]; availableModels?: string[]; settingsFile?: string }
|
||||
): ClaudeCliToolsConfig {
|
||||
@@ -895,10 +895,13 @@ export function addClaudeCustomEndpoint(
|
||||
return config;
|
||||
}
|
||||
|
||||
/** @deprecated Use addCustomEndpoint instead */
|
||||
export const addClaudeCustomEndpoint = addCustomEndpoint;
|
||||
|
||||
/**
|
||||
* Remove endpoint tool (cli-wrapper or api-endpoint)
|
||||
*/
|
||||
export function removeClaudeCustomEndpoint(
|
||||
export function removeCustomEndpoint(
|
||||
projectDir: string,
|
||||
endpointId: string
|
||||
): ClaudeCliToolsConfig {
|
||||
@@ -918,6 +921,9 @@ export function removeClaudeCustomEndpoint(
|
||||
return config;
|
||||
}
|
||||
|
||||
/** @deprecated Use removeCustomEndpoint instead */
|
||||
export const removeClaudeCustomEndpoint = removeCustomEndpoint;
|
||||
|
||||
/**
|
||||
* Get config source info
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
/**
|
||||
* CLI Settings Type Definitions
|
||||
* Supports Claude CLI --settings parameter format
|
||||
* Supports multi-provider CLI settings: Claude, Codex, Gemini
|
||||
*/
|
||||
|
||||
/**
|
||||
* CLI Provider type discriminator
|
||||
*/
|
||||
export type CliProvider = 'claude' | 'codex' | 'gemini';
|
||||
|
||||
/**
|
||||
* Claude CLI Settings 文件格式
|
||||
* 对应 `claude --settings <file-or-json>` 参数
|
||||
@@ -21,8 +26,6 @@ export interface ClaudeCliSettings {
|
||||
};
|
||||
/** 模型选择 */
|
||||
model?: 'opus' | 'sonnet' | 'haiku' | string;
|
||||
/** 是否包含 co-authored-by */
|
||||
includeCoAuthoredBy?: boolean;
|
||||
/** CLI工具标签 (用于标签路由) */
|
||||
tags?: string[];
|
||||
/** 可用模型列表 (显示在下拉菜单中) */
|
||||
@@ -31,6 +34,60 @@ export interface ClaudeCliSettings {
|
||||
settingsFile?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex CLI Settings
|
||||
* Codex 使用 --profile 传递配置, auth.json / config.toml 管理凭证和设置
|
||||
*/
|
||||
export interface CodexCliSettings {
|
||||
/** 环境变量配置 */
|
||||
env: {
|
||||
/** OpenAI API Key */
|
||||
OPENAI_API_KEY?: string;
|
||||
/** OpenAI API Base URL */
|
||||
OPENAI_BASE_URL?: string;
|
||||
/** 其他自定义环境变量 */
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
/** Codex profile 名称 (传递为 --profile <name>) */
|
||||
profile?: string;
|
||||
/** 模型选择 */
|
||||
model?: string;
|
||||
/** auth.json 内容 (JSON 字符串) */
|
||||
authJson?: string;
|
||||
/** config.toml 内容 (TOML 字符串) */
|
||||
configToml?: string;
|
||||
/** CLI工具标签 */
|
||||
tags?: string[];
|
||||
/** 可用模型列表 */
|
||||
availableModels?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemini CLI Settings
|
||||
*/
|
||||
export interface GeminiCliSettings {
|
||||
/** 环境变量配置 */
|
||||
env: {
|
||||
/** Gemini API Key */
|
||||
GEMINI_API_KEY?: string;
|
||||
/** Google API Key (alternative) */
|
||||
GOOGLE_API_KEY?: string;
|
||||
/** 其他自定义环境变量 */
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
/** 模型选择 */
|
||||
model?: string;
|
||||
/** CLI工具标签 */
|
||||
tags?: string[];
|
||||
/** 可用模型列表 */
|
||||
availableModels?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Union type for all provider settings
|
||||
*/
|
||||
export type CliSettings = ClaudeCliSettings | CodexCliSettings | GeminiCliSettings;
|
||||
|
||||
/**
|
||||
* 端点 Settings 配置(带元数据)
|
||||
*/
|
||||
@@ -41,8 +98,10 @@ export interface EndpointSettings {
|
||||
name: string;
|
||||
/** 端点描述 */
|
||||
description?: string;
|
||||
/** Claude CLI Settings */
|
||||
settings: ClaudeCliSettings;
|
||||
/** CLI provider 类型 (默认 'claude' 兼容旧数据) */
|
||||
provider: CliProvider;
|
||||
/** CLI Settings (provider-specific) */
|
||||
settings: CliSettings;
|
||||
/** 是否启用 */
|
||||
enabled: boolean;
|
||||
/** 创建时间 */
|
||||
@@ -76,7 +135,9 @@ export interface SaveEndpointRequest {
|
||||
id?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
settings: ClaudeCliSettings;
|
||||
/** CLI provider 类型 */
|
||||
provider?: CliProvider;
|
||||
settings: CliSettings;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -104,22 +165,39 @@ export function mapProviderToClaudeEnv(provider: {
|
||||
/**
|
||||
* 创建默认 Settings
|
||||
*/
|
||||
export function createDefaultSettings(): ClaudeCliSettings {
|
||||
return {
|
||||
env: {
|
||||
DISABLE_AUTOUPDATER: '1'
|
||||
},
|
||||
model: 'sonnet',
|
||||
includeCoAuthoredBy: false,
|
||||
tags: [],
|
||||
availableModels: []
|
||||
};
|
||||
export function createDefaultSettings(provider: CliProvider = 'claude'): CliSettings {
|
||||
switch (provider) {
|
||||
case 'codex':
|
||||
return {
|
||||
env: {},
|
||||
model: '',
|
||||
tags: [],
|
||||
availableModels: []
|
||||
} satisfies CodexCliSettings;
|
||||
case 'gemini':
|
||||
return {
|
||||
env: {},
|
||||
model: '',
|
||||
tags: [],
|
||||
availableModels: []
|
||||
} satisfies GeminiCliSettings;
|
||||
case 'claude':
|
||||
default:
|
||||
return {
|
||||
env: {
|
||||
DISABLE_AUTOUPDATER: '1'
|
||||
},
|
||||
model: 'sonnet',
|
||||
tags: [],
|
||||
availableModels: []
|
||||
} satisfies ClaudeCliSettings;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Settings 格式
|
||||
* 验证 Settings 格式 (provider-aware)
|
||||
*/
|
||||
export function validateSettings(settings: unknown): settings is ClaudeCliSettings {
|
||||
export function validateSettings(settings: unknown, provider?: CliProvider): settings is CliSettings {
|
||||
if (!settings || typeof settings !== 'object') {
|
||||
return false;
|
||||
}
|
||||
@@ -136,7 +214,6 @@ export function validateSettings(settings: unknown): settings is ClaudeCliSettin
|
||||
for (const key in envObj) {
|
||||
if (Object.prototype.hasOwnProperty.call(envObj, key)) {
|
||||
const value = envObj[key];
|
||||
// 允许 undefined 或 string,其他类型(包括 null)都拒绝
|
||||
if (value !== undefined && typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
@@ -148,11 +225,6 @@ export function validateSettings(settings: unknown): settings is ClaudeCliSettin
|
||||
return false;
|
||||
}
|
||||
|
||||
// includeCoAuthoredBy 可选,但如果存在必须是布尔值
|
||||
if (s.includeCoAuthoredBy !== undefined && typeof s.includeCoAuthoredBy !== 'boolean') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// tags 可选,但如果存在必须是数组
|
||||
if (s.tags !== undefined && !Array.isArray(s.tags)) {
|
||||
return false;
|
||||
@@ -163,9 +235,25 @@ export function validateSettings(settings: unknown): settings is ClaudeCliSettin
|
||||
return false;
|
||||
}
|
||||
|
||||
// settingsFile 可选,但如果存在必须是字符串
|
||||
if (s.settingsFile !== undefined && typeof s.settingsFile !== 'string') {
|
||||
return false;
|
||||
// Provider-specific validation
|
||||
if (provider === 'codex') {
|
||||
// profile 可选,但如果存在必须是字符串
|
||||
if (s.profile !== undefined && typeof s.profile !== 'string') {
|
||||
return false;
|
||||
}
|
||||
// authJson 可选,但如果存在必须是字符串
|
||||
if (s.authJson !== undefined && typeof s.authJson !== 'string') {
|
||||
return false;
|
||||
}
|
||||
// configToml 可选,但如果存在必须是字符串
|
||||
if (s.configToml !== undefined && typeof s.configToml !== 'string') {
|
||||
return false;
|
||||
}
|
||||
} else if (provider === 'claude' || !provider) {
|
||||
// settingsFile 可选,但如果存在必须是字符串
|
||||
if (s.settingsFile !== undefined && typeof s.settingsFile !== 'string') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user