diff --git a/README.md b/README.md
index 290bea66..c6515a99 100644
--- a/README.md
+++ b/README.md
@@ -106,6 +106,29 @@ ccs agy --headless # Displays URL, paste in browser elsewhere
ccs gemini --logout
```
+### Multi-Account for OAuth Providers
+
+Use multiple accounts per provider (work + personal):
+
+```bash
+# First account (default)
+ccs gemini --auth
+
+# Add another account
+ccs gemini --auth --add
+
+# Add with nickname for easy identification
+ccs gemini --auth --add --nickname work
+
+# List all accounts
+ccs agy --accounts
+
+# Switch to a different account
+ccs agy --use work
+```
+
+Accounts are stored in `~/.ccs/cliproxy/accounts.json` and can be managed via web dashboard (`ccs config`).
+
### OAuth vs API Key Models
| Feature | OAuth Providers
(gemini, codex, agy) | API Key Models
(glm, kimi) |
diff --git a/src/cliproxy/account-manager.ts b/src/cliproxy/account-manager.ts
index 5ba923bd..39413cef 100644
--- a/src/cliproxy/account-manager.ts
+++ b/src/cliproxy/account-manager.ts
@@ -165,6 +165,32 @@ export function getAccount(provider: CLIProxyProvider, accountId: string): Accou
return accounts.find((a) => a.id === accountId) || null;
}
+/**
+ * Find account by query (nickname, email, or id)
+ * Supports partial matching for convenience
+ */
+export function findAccountByQuery(provider: CLIProxyProvider, query: string): AccountInfo | null {
+ const accounts = getProviderAccounts(provider);
+ const lowerQuery = query.toLowerCase();
+
+ // Exact match first (id, email, nickname)
+ const exactMatch = accounts.find(
+ (a) =>
+ a.id === query ||
+ a.email?.toLowerCase() === lowerQuery ||
+ a.nickname?.toLowerCase() === lowerQuery
+ );
+ if (exactMatch) return exactMatch;
+
+ // Partial match on nickname or email prefix
+ const partialMatch = accounts.find(
+ (a) =>
+ a.nickname?.toLowerCase().startsWith(lowerQuery) ||
+ a.email?.toLowerCase().startsWith(lowerQuery)
+ );
+ return partialMatch || null;
+}
+
/**
* Register a new account
* Called after successful OAuth to record the account
diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts
index 1f565c8e..af8536c3 100644
--- a/src/cliproxy/cliproxy-executor.ts
+++ b/src/cliproxy/cliproxy-executor.ts
@@ -28,6 +28,12 @@ import { isAuthenticated } from './auth-handler';
import { CLIProxyProvider, ExecutorConfig } from './types';
import { configureProviderModel, getCurrentModel } from './model-config';
import { supportsModelConfig, isModelBroken, getModelIssueUrl, findModel } from './model-catalog';
+import {
+ findAccountByQuery,
+ getProviderAccounts,
+ setDefaultAccount,
+ touchAccount,
+} from './account-manager';
/** Default executor configuration */
const DEFAULT_CONFIG: ExecutorConfig = {
@@ -124,6 +130,52 @@ export async function execClaudeWithCLIProxy(
const forceLogout = args.includes('--logout');
const forceConfig = args.includes('--config');
const addAccount = args.includes('--add');
+ const showAccounts = args.includes('--accounts');
+
+ // Parse --use flag
+ let useAccount: string | undefined;
+ const useIdx = args.indexOf('--use');
+ if (useIdx !== -1 && args[useIdx + 1] && !args[useIdx + 1].startsWith('-')) {
+ useAccount = args[useIdx + 1];
+ }
+
+ // Handle --accounts: list accounts and exit
+ if (showAccounts) {
+ const accounts = getProviderAccounts(provider);
+ if (accounts.length === 0) {
+ console.log(`[i] No accounts registered for ${providerConfig.displayName}`);
+ console.log(` Run "ccs ${provider} --auth" to add an account`);
+ } else {
+ console.log(`\n${providerConfig.displayName} Accounts:\n`);
+ for (const acct of accounts) {
+ const defaultMark = acct.isDefault ? ' (default)' : '';
+ const nickname = acct.nickname ? `[${acct.nickname}]` : '';
+ console.log(` ${nickname.padEnd(12)} ${acct.email || acct.id}${defaultMark}`);
+ }
+ console.log(`\n Use "ccs ${provider} --use " to switch accounts`);
+ }
+ process.exit(0);
+ }
+
+ // Handle --use: switch to specified account
+ if (useAccount) {
+ const account = findAccountByQuery(provider, useAccount);
+ if (!account) {
+ console.error(`[X] Account not found: "${useAccount}"`);
+ const accounts = getProviderAccounts(provider);
+ if (accounts.length > 0) {
+ console.error(` Available accounts:`);
+ for (const acct of accounts) {
+ console.error(` - ${acct.nickname || acct.id} (${acct.email || 'no email'})`);
+ }
+ }
+ process.exit(1);
+ }
+ // Set as default for this and future sessions
+ setDefaultAccount(provider, account.id);
+ touchAccount(provider, account.id);
+ console.log(`[OK] Switched to account: ${account.nickname || account.email || account.id}`);
+ }
// Handle --config: configure model selection and exit
// Pass customSettingsPath for CLIProxy variants to save to correct file
@@ -262,8 +314,14 @@ 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'];
- const claudeArgs = args.filter((arg) => !ccsFlags.includes(arg));
+ const ccsFlags = ['--auth', '--headless', '--logout', '--config', '--add', '--accounts', '--use'];
+ 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;
+ return true;
+ });
const isWindows = process.platform === 'win32';
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
diff --git a/src/commands/cliproxy-command.ts b/src/commands/cliproxy-command.ts
index 31845543..76f2d2b7 100644
--- a/src/commands/cliproxy-command.ts
+++ b/src/commands/cliproxy-command.ts
@@ -656,6 +656,21 @@ async function showHelp(): Promise {
}
console.log('');
+ // Multi-Account Commands
+ console.log(subheader('Multi-Account Commands:'));
+ const multiAcctCmds: [string, string][] = [
+ ['--auth', 'Authenticate with a provider (first account)'],
+ ['--auth --add', 'Add another account to a provider'],
+ ['--nickname ', 'Set friendly name for account'],
+ ['--accounts', 'List all accounts for a provider'],
+ ['--use ', 'Switch to account by nickname/email'],
+ ];
+ const maxMultiLen = Math.max(...multiAcctCmds.map(([cmd]) => cmd.length));
+ for (const [cmd, desc] of multiAcctCmds) {
+ console.log(` ${color(cmd.padEnd(maxMultiLen + 2), 'command')} ${desc}`);
+ }
+ console.log('');
+
// Create Options
console.log(subheader('Create Options:'));
const createOpts: [string, string][] = [
@@ -690,6 +705,23 @@ async function showHelp(): Promise {
` $ ${color('ccs cliproxy --latest', 'command')} ${dim('# Update binary')}`
);
console.log('');
+ console.log(subheader('Multi-Account Examples:'));
+ console.log(
+ ` $ ${color('ccs gemini --auth', 'command')} ${dim('# First account')}`
+ );
+ console.log(
+ ` $ ${color('ccs gemini --auth --add', 'command')} ${dim('# Add second account')}`
+ );
+ console.log(
+ ` $ ${color('ccs gemini --auth --add --nickname work', 'command')} ${dim('# With nickname')}`
+ );
+ console.log(
+ ` $ ${color('ccs agy --accounts', 'command')} ${dim('# List accounts')}`
+ );
+ console.log(
+ ` $ ${color('ccs agy --use work', 'command')} ${dim('# Switch account')}`
+ );
+ console.log('');
// Notes
console.log(subheader('Notes:'));
diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts
index 9a266347..23c2af18 100644
--- a/src/commands/help-command.ts
+++ b/src/commands/help-command.ts
@@ -162,6 +162,8 @@ Claude Code Profile & Model Switcher`.trim();
['', ''], // Spacer
['ccs --auth', 'Authenticate only'],
['ccs --auth --add', 'Add another account'],
+ ['ccs --accounts', 'List all accounts'],
+ ['ccs --use ', 'Switch to account'],
['ccs --config', 'Change model (agy, gemini)'],
['ccs --logout', 'Clear authentication'],
['ccs --headless', 'Headless auth (for SSH)'],