feat(cliproxy): add --use and --accounts flags for multi-account switching

- Add findAccountByQuery() to search accounts by nickname/email/id
- Add --accounts flag to list all accounts for a provider
- Add --use <name> flag to switch between accounts
- Filter CCS-specific flags from Claude CLI args
- Update help documentation with new multi-account commands
This commit is contained in:
kaitranntt
2025-12-09 17:28:41 -05:00
parent 8f5c006f07
commit 8f6684f948
5 changed files with 143 additions and 2 deletions
+23
View File
@@ -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<br>(gemini, codex, agy) | API Key Models<br>(glm, kimi) |
+26
View File
@@ -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
+60 -2
View File
@@ -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 <account> 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 <nickname>" 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);
+32
View File
@@ -656,6 +656,21 @@ async function showHelp(): Promise<void> {
}
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 <name>', 'Set friendly name for account'],
['--accounts', 'List all accounts for a provider'],
['--use <name>', '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<void> {
` $ ${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:'));
+2
View File
@@ -162,6 +162,8 @@ Claude Code Profile & Model Switcher`.trim();
['', ''], // Spacer
['ccs <provider> --auth', 'Authenticate only'],
['ccs <provider> --auth --add', 'Add another account'],
['ccs <provider> --accounts', 'List all accounts'],
['ccs <provider> --use <name>', 'Switch to account'],
['ccs <provider> --config', 'Change model (agy, gemini)'],
['ccs <provider> --logout', 'Clear authentication'],
['ccs <provider> --headless', 'Headless auth (for SSH)'],