diff --git a/src/cliproxy/account-manager.ts b/src/cliproxy/account-manager.ts index 39413cef..f1b53d71 100644 --- a/src/cliproxy/account-manager.ts +++ b/src/cliproxy/account-manager.ts @@ -304,6 +304,38 @@ export function removeAccount(provider: CLIProxyProvider, accountId: string): bo return true; } +/** + * Rename an account's nickname + */ +export function renameAccount( + provider: CLIProxyProvider, + accountId: string, + newNickname: string +): boolean { + const validationError = validateNickname(newNickname); + if (validationError) { + throw new Error(validationError); + } + + const registry = loadAccountsRegistry(); + const providerAccounts = registry.providers[provider]; + + if (!providerAccounts?.accounts[accountId]) { + return false; + } + + // Check if nickname is already used by another account + for (const [id, account] of Object.entries(providerAccounts.accounts)) { + if (id !== accountId && account.nickname?.toLowerCase() === newNickname.toLowerCase()) { + throw new Error(`Nickname "${newNickname}" is already used by another account`); + } + } + + providerAccounts.accounts[accountId].nickname = newNickname; + saveAccountsRegistry(registry); + return true; +} + /** * Update last used timestamp for an account */ diff --git a/src/cliproxy/auth-handler.ts b/src/cliproxy/auth-handler.ts index de355dd0..1f8b057f 100644 --- a/src/cliproxy/auth-handler.ts +++ b/src/cliproxy/auth-handler.ts @@ -435,10 +435,16 @@ export function clearAuth(provider: CLIProxyProvider): boolean { */ export async function triggerOAuth( provider: CLIProxyProvider, - options: { verbose?: boolean; headless?: boolean; account?: string; add?: boolean } = {} + options: { + verbose?: boolean; + headless?: boolean; + account?: string; + add?: boolean; + nickname?: string; + } = {} ): Promise { const oauthConfig = getOAuthConfig(provider); - const { verbose = false, add = false } = options; + const { verbose = false, add = false, nickname } = options; // Auto-detect headless if not explicitly set const headless = options.headless ?? isHeadlessEnvironment(); @@ -615,7 +621,7 @@ export async function triggerOAuth( console.log('[OK] Authentication successful'); // Register the account in accounts registry - const account = registerAccountFromToken(provider, tokenDir); + const account = registerAccountFromToken(provider, tokenDir, nickname); resolve(account); } else { if (!headless) spinner.fail('Authentication incomplete'); @@ -658,10 +664,14 @@ export async function triggerOAuth( /** * Register account from newly created token file * Scans auth directory for new token and extracts email + * @param provider - The CLIProxy provider + * @param tokenDir - Directory containing token files + * @param nickname - Optional nickname (uses auto-generated from email if not provided) */ function registerAccountFromToken( provider: CLIProxyProvider, - tokenDir: string + tokenDir: string, + nickname?: string ): AccountInfo | null { try { const files = fs.readdirSync(tokenDir); @@ -692,8 +702,8 @@ function registerAccountFromToken( const data = JSON.parse(content); const email = data.email || undefined; - // Register the account with auto-generated nickname - return registerAccount(provider, newestFile, email, generateNickname(email)); + // Register the account (use provided nickname or auto-generate from email) + return registerAccount(provider, newestFile, email, nickname || generateNickname(email)); } catch { return null; } diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index af8536c3..e21d1a27 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -33,6 +33,8 @@ import { getProviderAccounts, setDefaultAccount, touchAccount, + renameAccount, + getDefaultAccount, } from './account-manager'; /** Default executor configuration */ @@ -139,6 +141,13 @@ export async function execClaudeWithCLIProxy( useAccount = args[useIdx + 1]; } + // Parse --nickname flag + let setNickname: string | undefined; + const nicknameIdx = args.indexOf('--nickname'); + if (nicknameIdx !== -1 && args[nicknameIdx + 1] && !args[nicknameIdx + 1].startsWith('-')) { + setNickname = args[nicknameIdx + 1]; + } + // Handle --accounts: list accounts and exit if (showAccounts) { const accounts = getProviderAccounts(provider); @@ -177,6 +186,29 @@ export async function execClaudeWithCLIProxy( console.log(`[OK] Switched to account: ${account.nickname || account.email || account.id}`); } + // Handle --nickname: rename account and exit (unless used with --auth --add) + if (setNickname && !addAccount) { + const defaultAccount = getDefaultAccount(provider); + if (!defaultAccount) { + console.error(`[X] No account found for ${providerConfig.displayName}`); + console.error(` Run "ccs ${provider} --auth" to add an account first`); + process.exit(1); + } + try { + const success = renameAccount(provider, defaultAccount.id, setNickname); + if (success) { + console.log(`[OK] Renamed account to: ${setNickname}`); + } else { + console.error(`[X] Failed to rename account`); + process.exit(1); + } + } catch (err) { + console.error(`[X] ${err instanceof Error ? err.message : 'Failed to rename account'}`); + process.exit(1); + } + process.exit(0); + } + // Handle --config: configure model selection and exit // Pass customSettingsPath for CLIProxy variants to save to correct file if (forceConfig && supportsModelConfig(provider)) { @@ -206,6 +238,7 @@ export async function execClaudeWithCLIProxy( verbose, add: addAccount, ...(forceHeadless ? { headless: true } : {}), + ...(setNickname ? { nickname: setNickname } : {}), }); if (!authSuccess) { throw new Error(`Authentication required for ${providerConfig.displayName}`); @@ -314,12 +347,21 @@ export async function execClaudeWithCLIProxy( log(`Claude env: ANTHROPIC_MODEL=${envVars.ANTHROPIC_MODEL}`); // Filter out CCS-specific flags before passing to Claude CLI - const ccsFlags = ['--auth', '--headless', '--logout', '--config', '--add', '--accounts', '--use']; + const ccsFlags = [ + '--auth', + '--headless', + '--logout', + '--config', + '--add', + '--accounts', + '--use', + '--nickname', + ]; const claudeArgs = args.filter((arg, idx) => { // Filter out CCS flags if (ccsFlags.includes(arg)) return false; - // Filter out value after --use - if (args[idx - 1] === '--use') return false; + // Filter out value after --use or --nickname + if (args[idx - 1] === '--use' || args[idx - 1] === '--nickname') return false; return true; });