Skip to content

Commit 70e8675

Browse files
committed
chore(cliproxy): improve config and token management
1 parent 92b7065 commit 70e8675

File tree

9 files changed

+702
-219
lines changed

9 files changed

+702
-219
lines changed

logs/main.log

Lines changed: 175 additions & 0 deletions
Large diffs are not rendered by default.

src/cliproxy/account-manager.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,54 @@ export function saveAccountsRegistry(registry: AccountsRegistry): void {
130130
});
131131
}
132132

133+
/**
134+
* Sync registry with actual token files
135+
* Removes stale entries where token file no longer exists
136+
* Called automatically when loading accounts
137+
*/
138+
function syncRegistryWithTokenFiles(registry: AccountsRegistry): boolean {
139+
const authDir = getAuthDir();
140+
let modified = false;
141+
142+
for (const [_providerName, providerAccounts] of Object.entries(registry.providers)) {
143+
if (!providerAccounts) continue;
144+
145+
const staleIds: string[] = [];
146+
147+
for (const [accountId, meta] of Object.entries(providerAccounts.accounts)) {
148+
const tokenPath = path.join(authDir, meta.tokenFile);
149+
if (!fs.existsSync(tokenPath)) {
150+
staleIds.push(accountId);
151+
}
152+
}
153+
154+
// Remove stale accounts
155+
for (const id of staleIds) {
156+
delete providerAccounts.accounts[id];
157+
modified = true;
158+
159+
// Update default if deleted
160+
if (providerAccounts.default === id) {
161+
const remainingIds = Object.keys(providerAccounts.accounts);
162+
providerAccounts.default = remainingIds[0] || 'default';
163+
}
164+
}
165+
}
166+
167+
return modified;
168+
}
169+
133170
/**
134171
* Get all accounts for a provider
135172
*/
136173
export function getProviderAccounts(provider: CLIProxyProvider): AccountInfo[] {
137174
const registry = loadAccountsRegistry();
175+
176+
// Sync with actual token files (removes stale entries)
177+
if (syncRegistryWithTokenFiles(registry)) {
178+
saveAccountsRegistry(registry);
179+
}
180+
138181
const providerAccounts = registry.providers[provider];
139182

140183
if (!providerAccounts) {

src/web-server/routes.ts

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import { getCcsDir, getConfigPath, loadConfig, loadSettings } from '../utils/con
1111
import { Config, Settings } from '../types/config';
1212
import { expandPath } from '../utils/helpers';
1313
import { runHealthChecks, fixHealthIssue } from './health-service';
14-
import { getAllAuthStatus, getOAuthConfig, initializeAccounts } from '../cliproxy/auth-handler';
14+
import {
15+
getAllAuthStatus,
16+
getOAuthConfig,
17+
initializeAccounts,
18+
triggerOAuth,
19+
} from '../cliproxy/auth-handler';
1520
import {
1621
fetchCliproxyStats,
1722
fetchCliproxyModels,
@@ -33,6 +38,7 @@ import {
3338
removeAccount as removeAccountFn,
3439
} from '../cliproxy/account-manager';
3540
import type { CLIProxyProvider } from '../cliproxy/types';
41+
import { getClaudeEnvVars } from '../cliproxy/config-generator';
3642
// Unified config imports
3743
import {
3844
hasUnifiedConfig,
@@ -200,12 +206,25 @@ function updateSettingsFile(
200206

201207
/**
202208
* Helper: Create cliproxy variant settings
209+
* Includes base URL and auth token for proper Claude CLI integration
203210
*/
204-
function createCliproxySettings(name: string, model?: string): string {
211+
function createCliproxySettings(name: string, provider: CLIProxyProvider, model?: string): string {
205212
const settingsPath = path.join(getCcsDir(), `${name}.settings.json`);
206213

214+
// Get base env vars from provider config (includes BASE_URL, AUTH_TOKEN)
215+
const baseEnv = getClaudeEnvVars(provider);
216+
207217
const settings: Settings = {
208-
env: model ? { ANTHROPIC_MODEL: model } : {},
218+
env: {
219+
ANTHROPIC_BASE_URL: baseEnv.ANTHROPIC_BASE_URL || '',
220+
ANTHROPIC_AUTH_TOKEN: baseEnv.ANTHROPIC_AUTH_TOKEN || '',
221+
ANTHROPIC_MODEL: model || (baseEnv.ANTHROPIC_MODEL as string) || '',
222+
ANTHROPIC_DEFAULT_OPUS_MODEL: model || (baseEnv.ANTHROPIC_DEFAULT_OPUS_MODEL as string) || '',
223+
ANTHROPIC_DEFAULT_SONNET_MODEL:
224+
model || (baseEnv.ANTHROPIC_DEFAULT_SONNET_MODEL as string) || '',
225+
ANTHROPIC_DEFAULT_HAIKU_MODEL:
226+
(baseEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL as string) || model || '',
227+
},
209228
};
210229

211230
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
@@ -356,7 +375,7 @@ apiRoutes.post('/cliproxy', (req: Request, res: Response): void => {
356375
}
357376

358377
// Create settings file for variant
359-
const settingsPath = createCliproxySettings(name, model);
378+
const settingsPath = createCliproxySettings(name, provider as CLIProxyProvider, model);
360379

361380
// Include account if specified (defaults to 'default' if not provided)
362381
config.cliproxy[name] = {
@@ -534,10 +553,34 @@ apiRoutes.post('/cliproxy/accounts/:provider/default', (req: Request, res: Respo
534553
/**
535554
* DELETE /api/cliproxy/accounts/:provider/:accountId - Remove an account
536555
*/
537-
apiRoutes.delete(
538-
'/api/cliproxy/accounts/:provider/:accountId',
539-
(req: Request, res: Response): void => {
540-
const { provider, accountId } = req.params;
556+
apiRoutes.delete('/cliproxy/accounts/:provider/:accountId', (req: Request, res: Response): void => {
557+
const { provider, accountId } = req.params;
558+
559+
// Validate provider
560+
const validProviders: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow'];
561+
if (!validProviders.includes(provider as CLIProxyProvider)) {
562+
res.status(400).json({ error: `Invalid provider: ${provider}` });
563+
return;
564+
}
565+
566+
const success = removeAccountFn(provider as CLIProxyProvider, accountId);
567+
568+
if (success) {
569+
res.json({ provider, accountId, deleted: true });
570+
} else {
571+
res.status(404).json({ error: 'Account not found' });
572+
}
573+
});
574+
575+
/**
576+
* POST /api/cliproxy/auth/:provider/start - Start OAuth flow for a provider
577+
* Opens browser for authentication and returns account info when complete
578+
*/
579+
apiRoutes.post(
580+
'/cliproxy/auth/:provider/start',
581+
async (req: Request, res: Response): Promise<void> => {
582+
const { provider } = req.params;
583+
const { nickname } = req.body;
541584

542585
// Validate provider
543586
const validProviders: CLIProxyProvider[] = ['gemini', 'codex', 'agy', 'qwen', 'iflow'];
@@ -546,12 +589,30 @@ apiRoutes.delete(
546589
return;
547590
}
548591

549-
const success = removeAccountFn(provider as CLIProxyProvider, accountId);
592+
try {
593+
// Trigger OAuth flow - this opens browser and waits for completion
594+
const account = await triggerOAuth(provider as CLIProxyProvider, {
595+
add: true, // Always add mode from UI
596+
headless: false, // Force interactive mode
597+
nickname: nickname || undefined,
598+
});
550599

551-
if (success) {
552-
res.json({ provider, accountId, deleted: true });
553-
} else {
554-
res.status(404).json({ error: 'Account not found' });
600+
if (account) {
601+
res.json({
602+
success: true,
603+
account: {
604+
id: account.id,
605+
email: account.email,
606+
nickname: account.nickname,
607+
provider: account.provider,
608+
isDefault: account.isDefault,
609+
},
610+
});
611+
} else {
612+
res.status(400).json({ error: 'Authentication failed or was cancelled' });
613+
}
614+
} catch (error) {
615+
res.status(500).json({ error: (error as Error).message });
555616
}
556617
}
557618
);

ui/src/components/add-account-dialog.tsx

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
/**
22
* Add Account Dialog Component
3-
* Simple dialog to add another OAuth account to a provider
4-
*
5-
* Shows auth command + refresh button (no variant creation)
3+
* Triggers OAuth flow server-side to add another account to a provider
64
*/
75

86
import { useState } from 'react';
@@ -14,9 +12,10 @@ import {
1412
DialogDescription,
1513
} from '@/components/ui/dialog';
1614
import { Button } from '@/components/ui/button';
17-
import { Card, CardContent } from '@/components/ui/card';
18-
import { Copy, Check, RefreshCw, Terminal } from 'lucide-react';
19-
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
15+
import { Input } from '@/components/ui/input';
16+
import { Label } from '@/components/ui/label';
17+
import { Loader2, ExternalLink, User } from 'lucide-react';
18+
import { useStartAuth } from '@/hooks/use-cliproxy';
2019

2120
interface AddAccountDialogProps {
2221
open: boolean;
@@ -26,69 +25,82 @@ interface AddAccountDialogProps {
2625
}
2726

2827
export function AddAccountDialog({ open, onClose, provider, displayName }: AddAccountDialogProps) {
29-
const [copied, setCopied] = useState(false);
30-
const [isRefreshing, setIsRefreshing] = useState(false);
31-
const { refetch } = useCliproxyAuth();
28+
const [nickname, setNickname] = useState('');
29+
const startAuthMutation = useStartAuth();
3230

33-
const authCommand = `ccs ${provider} --auth --add`;
34-
35-
const copyCommand = async () => {
36-
await navigator.clipboard.writeText(authCommand);
37-
setCopied(true);
38-
setTimeout(() => setCopied(false), 2000);
31+
const handleStartAuth = () => {
32+
startAuthMutation.mutate(
33+
{ provider, nickname: nickname.trim() || undefined },
34+
{
35+
onSuccess: () => {
36+
setNickname('');
37+
onClose();
38+
},
39+
}
40+
);
3941
};
4042

41-
const handleRefresh = async () => {
42-
setIsRefreshing(true);
43-
await refetch();
44-
setIsRefreshing(false);
45-
onClose();
43+
const handleOpenChange = (isOpen: boolean) => {
44+
if (!isOpen && !startAuthMutation.isPending) {
45+
setNickname('');
46+
onClose();
47+
}
4648
};
4749

4850
return (
49-
<Dialog open={open} onOpenChange={onClose}>
51+
<Dialog open={open} onOpenChange={handleOpenChange}>
5052
<DialogContent className="sm:max-w-md">
5153
<DialogHeader>
5254
<DialogTitle>Add {displayName} Account</DialogTitle>
5355
<DialogDescription>
54-
Run the command below in your terminal to authenticate a new account
56+
Click the button below to authenticate a new account. A browser window will open for
57+
OAuth.
5558
</DialogDescription>
5659
</DialogHeader>
5760

5861
<div className="space-y-4 py-4">
59-
<Card>
60-
<CardContent className="p-4 space-y-3">
61-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
62-
<Terminal className="w-4 h-4" />
63-
Run this command:
64-
</div>
65-
<div className="flex items-center gap-2">
66-
<code className="flex-1 px-3 py-2 bg-muted rounded-md font-mono text-sm">
67-
{authCommand}
68-
</code>
69-
<Button variant="outline" size="icon" onClick={copyCommand}>
70-
{copied ? (
71-
<Check className="w-4 h-4 text-green-500" />
72-
) : (
73-
<Copy className="w-4 h-4" />
74-
)}
75-
</Button>
76-
</div>
77-
<div className="text-xs text-muted-foreground">
78-
This will open your browser to authenticate with {displayName}
79-
</div>
80-
</CardContent>
81-
</Card>
62+
<div className="space-y-2">
63+
<Label htmlFor="nickname">Nickname (optional)</Label>
64+
<div className="flex items-center gap-2">
65+
<User className="w-4 h-4 text-muted-foreground" />
66+
<Input
67+
id="nickname"
68+
value={nickname}
69+
onChange={(e) => setNickname(e.target.value)}
70+
placeholder="e.g., work, personal"
71+
disabled={startAuthMutation.isPending}
72+
className="flex-1"
73+
/>
74+
</div>
75+
<p className="text-xs text-muted-foreground">
76+
A friendly name to identify this account. Auto-generated from email if left empty.
77+
</p>
78+
</div>
8279

83-
<div className="flex items-center justify-end gap-2">
84-
<Button variant="ghost" onClick={onClose}>
80+
<div className="flex items-center justify-end gap-2 pt-2">
81+
<Button variant="ghost" onClick={onClose} disabled={startAuthMutation.isPending}>
8582
Cancel
8683
</Button>
87-
<Button onClick={handleRefresh} disabled={isRefreshing}>
88-
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
89-
{isRefreshing ? 'Checking...' : 'I ran the command'}
84+
<Button onClick={handleStartAuth} disabled={startAuthMutation.isPending}>
85+
{startAuthMutation.isPending ? (
86+
<>
87+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
88+
Authenticating...
89+
</>
90+
) : (
91+
<>
92+
<ExternalLink className="w-4 h-4 mr-2" />
93+
Authenticate
94+
</>
95+
)}
9096
</Button>
9197
</div>
98+
99+
{startAuthMutation.isPending && (
100+
<p className="text-sm text-center text-muted-foreground">
101+
Complete the OAuth flow in your browser...
102+
</p>
103+
)}
92104
</div>
93105
</DialogContent>
94106
</Dialog>

0 commit comments

Comments
 (0)