Skip to content

Commit 8f6684f

Browse files
committed
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
1 parent 8f5c006 commit 8f6684f

File tree

5 files changed

+143
-2
lines changed

5 files changed

+143
-2
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,29 @@ ccs agy --headless # Displays URL, paste in browser elsewhere
106106
ccs gemini --logout
107107
```
108108

109+
### Multi-Account for OAuth Providers
110+
111+
Use multiple accounts per provider (work + personal):
112+
113+
```bash
114+
# First account (default)
115+
ccs gemini --auth
116+
117+
# Add another account
118+
ccs gemini --auth --add
119+
120+
# Add with nickname for easy identification
121+
ccs gemini --auth --add --nickname work
122+
123+
# List all accounts
124+
ccs agy --accounts
125+
126+
# Switch to a different account
127+
ccs agy --use work
128+
```
129+
130+
Accounts are stored in `~/.ccs/cliproxy/accounts.json` and can be managed via web dashboard (`ccs config`).
131+
109132
### OAuth vs API Key Models
110133

111134
| Feature | OAuth Providers<br>(gemini, codex, agy) | API Key Models<br>(glm, kimi) |

src/cliproxy/account-manager.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,32 @@ export function getAccount(provider: CLIProxyProvider, accountId: string): Accou
165165
return accounts.find((a) => a.id === accountId) || null;
166166
}
167167

168+
/**
169+
* Find account by query (nickname, email, or id)
170+
* Supports partial matching for convenience
171+
*/
172+
export function findAccountByQuery(provider: CLIProxyProvider, query: string): AccountInfo | null {
173+
const accounts = getProviderAccounts(provider);
174+
const lowerQuery = query.toLowerCase();
175+
176+
// Exact match first (id, email, nickname)
177+
const exactMatch = accounts.find(
178+
(a) =>
179+
a.id === query ||
180+
a.email?.toLowerCase() === lowerQuery ||
181+
a.nickname?.toLowerCase() === lowerQuery
182+
);
183+
if (exactMatch) return exactMatch;
184+
185+
// Partial match on nickname or email prefix
186+
const partialMatch = accounts.find(
187+
(a) =>
188+
a.nickname?.toLowerCase().startsWith(lowerQuery) ||
189+
a.email?.toLowerCase().startsWith(lowerQuery)
190+
);
191+
return partialMatch || null;
192+
}
193+
168194
/**
169195
* Register a new account
170196
* Called after successful OAuth to record the account

src/cliproxy/cliproxy-executor.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ import { isAuthenticated } from './auth-handler';
2828
import { CLIProxyProvider, ExecutorConfig } from './types';
2929
import { configureProviderModel, getCurrentModel } from './model-config';
3030
import { supportsModelConfig, isModelBroken, getModelIssueUrl, findModel } from './model-catalog';
31+
import {
32+
findAccountByQuery,
33+
getProviderAccounts,
34+
setDefaultAccount,
35+
touchAccount,
36+
} from './account-manager';
3137

3238
/** Default executor configuration */
3339
const DEFAULT_CONFIG: ExecutorConfig = {
@@ -124,6 +130,52 @@ export async function execClaudeWithCLIProxy(
124130
const forceLogout = args.includes('--logout');
125131
const forceConfig = args.includes('--config');
126132
const addAccount = args.includes('--add');
133+
const showAccounts = args.includes('--accounts');
134+
135+
// Parse --use <account> flag
136+
let useAccount: string | undefined;
137+
const useIdx = args.indexOf('--use');
138+
if (useIdx !== -1 && args[useIdx + 1] && !args[useIdx + 1].startsWith('-')) {
139+
useAccount = args[useIdx + 1];
140+
}
141+
142+
// Handle --accounts: list accounts and exit
143+
if (showAccounts) {
144+
const accounts = getProviderAccounts(provider);
145+
if (accounts.length === 0) {
146+
console.log(`[i] No accounts registered for ${providerConfig.displayName}`);
147+
console.log(` Run "ccs ${provider} --auth" to add an account`);
148+
} else {
149+
console.log(`\n${providerConfig.displayName} Accounts:\n`);
150+
for (const acct of accounts) {
151+
const defaultMark = acct.isDefault ? ' (default)' : '';
152+
const nickname = acct.nickname ? `[${acct.nickname}]` : '';
153+
console.log(` ${nickname.padEnd(12)} ${acct.email || acct.id}${defaultMark}`);
154+
}
155+
console.log(`\n Use "ccs ${provider} --use <nickname>" to switch accounts`);
156+
}
157+
process.exit(0);
158+
}
159+
160+
// Handle --use: switch to specified account
161+
if (useAccount) {
162+
const account = findAccountByQuery(provider, useAccount);
163+
if (!account) {
164+
console.error(`[X] Account not found: "${useAccount}"`);
165+
const accounts = getProviderAccounts(provider);
166+
if (accounts.length > 0) {
167+
console.error(` Available accounts:`);
168+
for (const acct of accounts) {
169+
console.error(` - ${acct.nickname || acct.id} (${acct.email || 'no email'})`);
170+
}
171+
}
172+
process.exit(1);
173+
}
174+
// Set as default for this and future sessions
175+
setDefaultAccount(provider, account.id);
176+
touchAccount(provider, account.id);
177+
console.log(`[OK] Switched to account: ${account.nickname || account.email || account.id}`);
178+
}
127179

128180
// Handle --config: configure model selection and exit
129181
// Pass customSettingsPath for CLIProxy variants to save to correct file
@@ -262,8 +314,14 @@ export async function execClaudeWithCLIProxy(
262314
log(`Claude env: ANTHROPIC_MODEL=${envVars.ANTHROPIC_MODEL}`);
263315

264316
// Filter out CCS-specific flags before passing to Claude CLI
265-
const ccsFlags = ['--auth', '--headless', '--logout', '--config', '--add'];
266-
const claudeArgs = args.filter((arg) => !ccsFlags.includes(arg));
317+
const ccsFlags = ['--auth', '--headless', '--logout', '--config', '--add', '--accounts', '--use'];
318+
const claudeArgs = args.filter((arg, idx) => {
319+
// Filter out CCS flags
320+
if (ccsFlags.includes(arg)) return false;
321+
// Filter out value after --use
322+
if (args[idx - 1] === '--use') return false;
323+
return true;
324+
});
267325

268326
const isWindows = process.platform === 'win32';
269327
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);

src/commands/cliproxy-command.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,21 @@ async function showHelp(): Promise<void> {
656656
}
657657
console.log('');
658658

659+
// Multi-Account Commands
660+
console.log(subheader('Multi-Account Commands:'));
661+
const multiAcctCmds: [string, string][] = [
662+
['--auth', 'Authenticate with a provider (first account)'],
663+
['--auth --add', 'Add another account to a provider'],
664+
['--nickname <name>', 'Set friendly name for account'],
665+
['--accounts', 'List all accounts for a provider'],
666+
['--use <name>', 'Switch to account by nickname/email'],
667+
];
668+
const maxMultiLen = Math.max(...multiAcctCmds.map(([cmd]) => cmd.length));
669+
for (const [cmd, desc] of multiAcctCmds) {
670+
console.log(` ${color(cmd.padEnd(maxMultiLen + 2), 'command')} ${desc}`);
671+
}
672+
console.log('');
673+
659674
// Create Options
660675
console.log(subheader('Create Options:'));
661676
const createOpts: [string, string][] = [
@@ -690,6 +705,23 @@ async function showHelp(): Promise<void> {
690705
` $ ${color('ccs cliproxy --latest', 'command')} ${dim('# Update binary')}`
691706
);
692707
console.log('');
708+
console.log(subheader('Multi-Account Examples:'));
709+
console.log(
710+
` $ ${color('ccs gemini --auth', 'command')} ${dim('# First account')}`
711+
);
712+
console.log(
713+
` $ ${color('ccs gemini --auth --add', 'command')} ${dim('# Add second account')}`
714+
);
715+
console.log(
716+
` $ ${color('ccs gemini --auth --add --nickname work', 'command')} ${dim('# With nickname')}`
717+
);
718+
console.log(
719+
` $ ${color('ccs agy --accounts', 'command')} ${dim('# List accounts')}`
720+
);
721+
console.log(
722+
` $ ${color('ccs agy --use work', 'command')} ${dim('# Switch account')}`
723+
);
724+
console.log('');
693725

694726
// Notes
695727
console.log(subheader('Notes:'));

src/commands/help-command.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ Claude Code Profile & Model Switcher`.trim();
162162
['', ''], // Spacer
163163
['ccs <provider> --auth', 'Authenticate only'],
164164
['ccs <provider> --auth --add', 'Add another account'],
165+
['ccs <provider> --accounts', 'List all accounts'],
166+
['ccs <provider> --use <name>', 'Switch to account'],
165167
['ccs <provider> --config', 'Change model (agy, gemini)'],
166168
['ccs <provider> --logout', 'Clear authentication'],
167169
['ccs <provider> --headless', 'Headless auth (for SSH)'],

0 commit comments

Comments
 (0)