diff --git a/src/api/services/index.ts b/src/api/services/index.ts index bdb07b20..57cceba4 100644 --- a/src/api/services/index.ts +++ b/src/api/services/index.ts @@ -15,6 +15,7 @@ export { type ApiListResult, type CreateApiProfileResult, type RemoveApiProfileResult, + type UpdateApiProfileTargetResult, } from './profile-types'; // Profile read operations @@ -27,7 +28,7 @@ export { } from './profile-reader'; // Profile write operations -export { createApiProfile, removeApiProfile } from './profile-writer'; +export { createApiProfile, removeApiProfile, updateApiProfileTarget } from './profile-writer'; // OpenRouter catalog and picker export { isOpenRouterUrl, fetchOpenRouterModels, type OpenRouterModel } from './openrouter-catalog'; diff --git a/src/api/services/profile-reader.ts b/src/api/services/profile-reader.ts index f31d4986..028931c7 100644 --- a/src/api/services/profile-reader.ts +++ b/src/api/services/profile-reader.ts @@ -9,6 +9,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { getCcsDir, loadConfigSafe } from '../../utils/config-manager'; import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader'; +import type { TargetType } from '../../targets/target-adapter'; import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types'; /** @@ -68,6 +69,7 @@ export function listApiProfiles(): ApiListResult { settingsPath: profile.settings || 'config.yaml', isConfigured: isApiProfileConfigured(name), configSource: 'unified', + target: profile.target || 'claude', }); } // CLIProxy variants @@ -80,10 +82,13 @@ export function listApiProfiles(): ApiListResult { name, provider, settings: variant?.settings || '-', + target: variant?.target || 'claude', }); } } else { const config = loadConfigSafe(); + const legacyTargetMap = (config as { profile_targets?: Record }) + .profile_targets; for (const [name, settingsPath] of Object.entries(config.profiles)) { // Skip 'default' profile - it's the user's native Claude settings if (name === 'default' && (settingsPath as string).includes('.claude/settings.json')) { @@ -94,16 +99,18 @@ export function listApiProfiles(): ApiListResult { settingsPath: settingsPath as string, isConfigured: isApiProfileConfigured(name), configSource: 'legacy', + target: legacyTargetMap?.[name] || 'claude', }); } // CLIProxy variants if (config.cliproxy) { for (const [name, v] of Object.entries(config.cliproxy)) { - const variant = v as { provider: string; settings: string }; + const variant = v as { provider: string; settings: string; target?: TargetType }; variants.push({ name, provider: variant.provider, settings: variant.settings, + target: variant.target || 'claude', }); } } diff --git a/src/api/services/profile-types.ts b/src/api/services/profile-types.ts index 75a0b76b..62da4667 100644 --- a/src/api/services/profile-types.ts +++ b/src/api/services/profile-types.ts @@ -4,6 +4,8 @@ * Shared type definitions for API profile services. */ +import type { TargetType } from '../../targets/target-adapter'; + /** Model mapping for API profiles */ export interface ModelMapping { default: string; @@ -18,6 +20,7 @@ export interface ApiProfileInfo { settingsPath: string; isConfigured: boolean; configSource: 'unified' | 'legacy'; + target: TargetType; } /** CLIProxy variant info */ @@ -25,6 +28,7 @@ export interface CliproxyVariantInfo { name: string; provider: string; settings: string; + target: TargetType; } /** Result from list operation */ @@ -45,3 +49,10 @@ export interface RemoveApiProfileResult { success: boolean; error?: string; } + +/** Result from updating API profile target */ +export interface UpdateApiProfileTargetResult { + success: boolean; + target?: TargetType; + error?: string; +} diff --git a/src/api/services/profile-writer.ts b/src/api/services/profile-writer.ts index ebac9446..fa8ff9c4 100644 --- a/src/api/services/profile-writer.ts +++ b/src/api/services/profile-writer.ts @@ -12,7 +12,13 @@ import { isUnifiedMode, } from '../../config/unified-config-loader'; import { ensureProfileHooks } from '../../utils/websearch/profile-hook-injector'; -import type { ModelMapping, CreateApiProfileResult, RemoveApiProfileResult } from './profile-types'; +import type { TargetType } from '../../targets/target-adapter'; +import type { + ModelMapping, + CreateApiProfileResult, + RemoveApiProfileResult, + UpdateApiProfileTargetResult, +} from './profile-types'; /** Check if URL is an OpenRouter endpoint */ function isOpenRouterUrl(baseUrl: string): boolean { @@ -51,11 +57,15 @@ function createSettingsFile( } /** Update config.json with new API profile (legacy format) */ -function updateLegacyConfig(name: string): void { +function updateLegacyConfig(name: string, target: TargetType = 'claude'): void { const configPath = getConfigPath(); const ccsDir = getCcsDir(); - let config: { profiles: Record; cliproxy?: Record }; + let config: { + profiles: Record; + cliproxy?: Record; + profile_targets?: Record; + }; try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { @@ -64,6 +74,12 @@ function updateLegacyConfig(name: string): void { const relativePath = `~/.ccs/${name}.settings.json`; config.profiles[name] = relativePath; + config.profile_targets = config.profile_targets || {}; + if (target === 'claude') { + delete config.profile_targets[name]; + } else { + config.profile_targets[name] = target; + } if (!fs.existsSync(ccsDir)) { fs.mkdirSync(ccsDir, { recursive: true }); @@ -80,7 +96,8 @@ function createApiProfileUnified( name: string, baseUrl: string, apiKey: string, - models: ModelMapping + models: ModelMapping, + target: TargetType = 'claude' ): void { const ccsDir = getCcsDir(); const settingsFile = `${name}.settings.json`; @@ -112,6 +129,7 @@ function createApiProfileUnified( config.profiles[name] = { type: 'api', settings: `~/.ccs/${settingsFile}`, + ...(target !== 'claude' && { target }), }; saveUnifiedConfig(config); } @@ -121,16 +139,17 @@ export function createApiProfile( name: string, baseUrl: string, apiKey: string, - models: ModelMapping + models: ModelMapping, + target: TargetType = 'claude' ): CreateApiProfileResult { try { const settingsFile = `~/.ccs/${name}.settings.json`; if (isUnifiedMode()) { - createApiProfileUnified(name, baseUrl, apiKey, models); + createApiProfileUnified(name, baseUrl, apiKey, models, target); } else { createSettingsFile(name, baseUrl, apiKey, models); - updateLegacyConfig(name); + updateLegacyConfig(name, target); } return { success: true, settingsFile }; @@ -143,6 +162,63 @@ export function createApiProfile( } } +/** + * Update API profile target (claude/droid). + * Persists to config.yaml in unified mode and config.json profile_targets in legacy mode. + */ +export function updateApiProfileTarget( + name: string, + target: TargetType +): UpdateApiProfileTargetResult { + try { + if (isUnifiedMode()) { + const config = loadOrCreateUnifiedConfig(); + if (!config.profiles[name]) { + return { success: false, error: `API profile not found: ${name}` }; + } + + if (target === 'claude') { + delete config.profiles[name].target; + } else { + config.profiles[name].target = target; + } + saveUnifiedConfig(config); + return { success: true, target }; + } + + const configPath = getConfigPath(); + let config: { + profiles: Record; + cliproxy?: Record; + profile_targets?: Record; + }; + try { + config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + } catch { + config = { profiles: {} }; + } + + if (!config.profiles[name]) { + return { success: false, error: `API profile not found: ${name}` }; + } + + config.profile_targets = config.profile_targets || {}; + if (target === 'claude') { + delete config.profile_targets[name]; + } else { + config.profile_targets[name] = target; + } + + const tempPath = configPath + '.tmp'; + fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n', 'utf8'); + fs.renameSync(tempPath, configPath); + + return { success: true, target }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } +} + /** Remove API profile from unified config */ function removeApiProfileUnified(name: string): void { const config = loadOrCreateUnifiedConfig();