@karpeleslab/teamclaude 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/package.json +27 -0
- package/src/account-manager.js +323 -0
- package/src/config.js +48 -0
- package/src/index.js +577 -0
- package/src/oauth.js +220 -0
- package/src/server.js +351 -0
- package/src/tui.js +388 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { loadOrCreateConfig, saveConfig, getConfigPath } from './config.js';
|
|
6
|
+
import { AccountManager } from './account-manager.js';
|
|
7
|
+
import { createProxyServer } from './server.js';
|
|
8
|
+
import { importCredentials, loginOAuth, fetchProfile } from './oauth.js';
|
|
9
|
+
import { TUI } from './tui.js';
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const command = args[0];
|
|
13
|
+
|
|
14
|
+
switch (command) {
|
|
15
|
+
case 'server':
|
|
16
|
+
await serverCommand();
|
|
17
|
+
break;
|
|
18
|
+
case 'import':
|
|
19
|
+
await importCommand();
|
|
20
|
+
break;
|
|
21
|
+
case 'login':
|
|
22
|
+
await loginCommand();
|
|
23
|
+
break;
|
|
24
|
+
case 'env':
|
|
25
|
+
await envCommand();
|
|
26
|
+
break;
|
|
27
|
+
case 'run':
|
|
28
|
+
await runCommand();
|
|
29
|
+
break;
|
|
30
|
+
case 'status':
|
|
31
|
+
await statusCommand();
|
|
32
|
+
break;
|
|
33
|
+
case 'accounts':
|
|
34
|
+
await accountsCommand();
|
|
35
|
+
break;
|
|
36
|
+
case 'remove':
|
|
37
|
+
await removeCommand();
|
|
38
|
+
break;
|
|
39
|
+
case 'api':
|
|
40
|
+
await apiCommand();
|
|
41
|
+
break;
|
|
42
|
+
case 'help':
|
|
43
|
+
case '--help':
|
|
44
|
+
case '-h':
|
|
45
|
+
showHelp();
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
// No command or unknown command → start server
|
|
49
|
+
if (command && !command.startsWith('-')) {
|
|
50
|
+
console.error(`Unknown command: ${command}\n`);
|
|
51
|
+
showHelp();
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
await serverCommand();
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── server ──────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
async function serverCommand() {
|
|
61
|
+
const config = await loadOrCreateConfig();
|
|
62
|
+
|
|
63
|
+
// --log-to <dir>
|
|
64
|
+
const logTo = argValue('--log-to');
|
|
65
|
+
if (logTo) config.logDir = logTo;
|
|
66
|
+
|
|
67
|
+
if (config.accounts.length === 0) {
|
|
68
|
+
console.error('No accounts configured.\n');
|
|
69
|
+
console.error('Add an account first:');
|
|
70
|
+
console.error(' teamclaude import Import from Claude Code');
|
|
71
|
+
console.error(' teamclaude login OAuth login via browser');
|
|
72
|
+
console.error(' teamclaude login --api Add an API key');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const accounts = await resolveAccounts(config);
|
|
77
|
+
if (accounts.length === 0) {
|
|
78
|
+
console.error('No valid accounts after initialization');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const threshold = config.switchThreshold || 0.98;
|
|
83
|
+
const accountManager = new AccountManager(accounts, threshold);
|
|
84
|
+
|
|
85
|
+
// Persist refreshed tokens back to config
|
|
86
|
+
accountManager.onTokenRefresh((idx, newTokens) => {
|
|
87
|
+
const cfgAcct = config.accounts[idx];
|
|
88
|
+
if (cfgAcct) {
|
|
89
|
+
cfgAcct.accessToken = newTokens.accessToken;
|
|
90
|
+
cfgAcct.refreshToken = newTokens.refreshToken;
|
|
91
|
+
cfgAcct.expiresAt = newTokens.expiresAt;
|
|
92
|
+
saveConfig(config).catch(err => console.error(`[TeamClaude] Failed to save refreshed token: ${err.message}`));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const port = config.proxy.port;
|
|
96
|
+
const useTUI = process.stdout.isTTY && process.stdin.isTTY;
|
|
97
|
+
|
|
98
|
+
let tui = null;
|
|
99
|
+
let hooks = {};
|
|
100
|
+
|
|
101
|
+
if (useTUI) {
|
|
102
|
+
tui = new TUI({
|
|
103
|
+
accountManager, config, saveConfig,
|
|
104
|
+
onQuit: () => { server.close(() => process.exit(0)); },
|
|
105
|
+
});
|
|
106
|
+
hooks = {
|
|
107
|
+
onRequestStart: (id, info) => tui.onRequestStart(id, info),
|
|
108
|
+
onRequestRouted: (id, info) => tui.onRequestRouted(id, info),
|
|
109
|
+
onRequestEnd: (id, info) => tui.onRequestEnd(id, info),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const server = createProxyServer(accountManager, config, hooks);
|
|
114
|
+
|
|
115
|
+
server.listen(port, () => {
|
|
116
|
+
if (tui) {
|
|
117
|
+
tui.start();
|
|
118
|
+
console.log(`Listening on port ${port} with ${accounts.length} account(s)`);
|
|
119
|
+
} else {
|
|
120
|
+
const sep = '='.repeat(60);
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(sep);
|
|
123
|
+
console.log(' TeamClaude Proxy');
|
|
124
|
+
console.log(sep);
|
|
125
|
+
console.log(` Port: ${port}`);
|
|
126
|
+
console.log(` Accounts: ${accounts.length}`);
|
|
127
|
+
console.log(` Threshold: ${(threshold * 100).toFixed(0)}%`);
|
|
128
|
+
console.log(` Upstream: ${config.upstream || 'https://api.anthropic.com'}`);
|
|
129
|
+
console.log('');
|
|
130
|
+
accounts.forEach((a, i) => {
|
|
131
|
+
console.log(` [${i + 1}] ${a.name} (${a.type})`);
|
|
132
|
+
});
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(' Run Claude through proxy: teamclaude run');
|
|
135
|
+
console.log(' Show env vars: teamclaude env');
|
|
136
|
+
console.log(sep);
|
|
137
|
+
console.log('');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!tui) {
|
|
142
|
+
process.on('SIGINT', () => {
|
|
143
|
+
console.log('\n[TeamClaude] Shutting down...');
|
|
144
|
+
server.close(() => process.exit(0));
|
|
145
|
+
});
|
|
146
|
+
process.on('SIGTERM', () => {
|
|
147
|
+
console.log('\n[TeamClaude] Shutting down...');
|
|
148
|
+
server.close(() => process.exit(0));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── import ──────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
async function importCommand() {
|
|
156
|
+
const config = await loadOrCreateConfig();
|
|
157
|
+
|
|
158
|
+
let name = argValue('--name');
|
|
159
|
+
const fromPath = argValue('--from') || '~/.claude/.credentials.json';
|
|
160
|
+
|
|
161
|
+
let creds;
|
|
162
|
+
try {
|
|
163
|
+
creds = await importCredentials(fromPath);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(`Failed to import from ${fromPath}: ${err.message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await upsertOAuthAccount(config, name, creds, 'import');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── login ───────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
async function loginCommand() {
|
|
175
|
+
if (args.includes('--api')) {
|
|
176
|
+
await loginApiCommand();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (args.includes('--oauth')) {
|
|
180
|
+
await loginOAuthCommand();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Default to OAuth if not a TTY
|
|
185
|
+
if (!process.stdout.isTTY) {
|
|
186
|
+
await loginOAuthCommand();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Interactive menu
|
|
191
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
192
|
+
console.log('Select login method:\n');
|
|
193
|
+
console.log(' 1. Claude subscription (Pro, Max, Team, Enterprise)');
|
|
194
|
+
console.log(' 2. Anthropic API key (Console API billing)');
|
|
195
|
+
console.log('');
|
|
196
|
+
const choice = await new Promise(resolve => rl.question('Choice [1]: ', resolve));
|
|
197
|
+
rl.close();
|
|
198
|
+
|
|
199
|
+
switch (choice.trim() || '1') {
|
|
200
|
+
case '1': await loginOAuthCommand(); break;
|
|
201
|
+
case '2': await loginApiCommand(); break;
|
|
202
|
+
default:
|
|
203
|
+
console.error(`Invalid choice: ${choice.trim()}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function loginApiCommand() {
|
|
209
|
+
const config = await loadOrCreateConfig();
|
|
210
|
+
let name = argValue('--name');
|
|
211
|
+
|
|
212
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
213
|
+
const apiKey = await new Promise(resolve => rl.question('Anthropic API key: ', resolve));
|
|
214
|
+
rl.close();
|
|
215
|
+
|
|
216
|
+
if (!apiKey.trim()) {
|
|
217
|
+
console.error('No API key provided');
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!name) {
|
|
222
|
+
const n = config.accounts.filter(a => a.name.startsWith('api-')).length + 1;
|
|
223
|
+
name = `api-${n}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
config.accounts.push({ name, type: 'apikey', apiKey: apiKey.trim() });
|
|
227
|
+
await saveConfig(config);
|
|
228
|
+
console.log(`Added API key account "${name}"`);
|
|
229
|
+
console.log(`Saved to ${getConfigPath()}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function loginOAuthCommand() {
|
|
233
|
+
const config = await loadOrCreateConfig();
|
|
234
|
+
let name = argValue('--name');
|
|
235
|
+
|
|
236
|
+
console.log('Starting OAuth login...');
|
|
237
|
+
let creds;
|
|
238
|
+
try {
|
|
239
|
+
creds = await loginOAuth();
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error(`OAuth login failed: ${err.message}`);
|
|
242
|
+
console.error('');
|
|
243
|
+
console.error('Alternatives:');
|
|
244
|
+
console.error(' teamclaude import Import from existing Claude Code credentials');
|
|
245
|
+
console.error(' teamclaude login --api Add an API key instead');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await upsertOAuthAccount(config, name, creds, 'login');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── env ─────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
async function envCommand() {
|
|
255
|
+
const config = await loadOrCreateConfig();
|
|
256
|
+
console.log(`export ANTHROPIC_BASE_URL=http://localhost:${config.proxy.port}`);
|
|
257
|
+
console.log(`export ANTHROPIC_API_KEY=${config.proxy.apiKey}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── run ─────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
async function runCommand() {
|
|
263
|
+
const config = await loadOrCreateConfig();
|
|
264
|
+
|
|
265
|
+
// Everything after 'run' (skip -- separator if present)
|
|
266
|
+
const claudeArgs = args.slice(1);
|
|
267
|
+
if (claudeArgs[0] === '--') claudeArgs.shift();
|
|
268
|
+
|
|
269
|
+
const child = spawn('claude', claudeArgs, {
|
|
270
|
+
stdio: 'inherit',
|
|
271
|
+
env: {
|
|
272
|
+
...process.env,
|
|
273
|
+
ANTHROPIC_BASE_URL: `http://localhost:${config.proxy.port}`,
|
|
274
|
+
ANTHROPIC_API_KEY: config.proxy.apiKey,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
child.on('error', (err) => {
|
|
279
|
+
if (err.code === 'ENOENT') {
|
|
280
|
+
console.error('Claude Code not found in PATH. Install it first.');
|
|
281
|
+
} else {
|
|
282
|
+
console.error(`Failed to start claude: ${err.message}`);
|
|
283
|
+
}
|
|
284
|
+
process.exit(1);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
child.on('exit', (code) => process.exit(code ?? 1));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── status ──────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
async function statusCommand() {
|
|
293
|
+
const config = await loadOrCreateConfig();
|
|
294
|
+
const url = `http://localhost:${config.proxy.port}/teamclaude/status`;
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const res = await fetch(url, { headers: { 'x-api-key': config.proxy.apiKey } });
|
|
298
|
+
const data = await res.json();
|
|
299
|
+
|
|
300
|
+
console.log(`Active account: ${data.currentAccount}`);
|
|
301
|
+
console.log(`Switch at: ${(data.switchThreshold * 100).toFixed(0)}% usage\n`);
|
|
302
|
+
|
|
303
|
+
for (const acct of data.accounts) {
|
|
304
|
+
const q = acct.quota;
|
|
305
|
+
const current = acct.name === data.currentAccount ? ' *' : '';
|
|
306
|
+
|
|
307
|
+
console.log(` ${acct.name} (${acct.type})${current}`);
|
|
308
|
+
console.log(` Status: ${acct.status}`);
|
|
309
|
+
|
|
310
|
+
if (q.unified5h != null || q.unified7d != null) {
|
|
311
|
+
const ses = q.unified5h != null ? (q.unified5h * 100).toFixed(1) + '%' : '-';
|
|
312
|
+
const wk = q.unified7d != null ? (q.unified7d * 100).toFixed(1) + '%' : '-';
|
|
313
|
+
console.log(` Session: ${ses} used Weekly: ${wk} used`);
|
|
314
|
+
} else {
|
|
315
|
+
const tok = q.tokensLimit ? ((1 - q.tokensRemaining / q.tokensLimit) * 100).toFixed(1) + '%' : '-';
|
|
316
|
+
const req = q.requestsLimit ? ((1 - q.requestsRemaining / q.requestsLimit) * 100).toFixed(1) + '%' : '-';
|
|
317
|
+
console.log(` Tokens: ${tok} used Requests: ${req} used`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
console.log(` Total: ${acct.usage.totalInputTokens + acct.usage.totalOutputTokens} tokens, ${acct.usage.totalRequests} requests`);
|
|
321
|
+
if (acct.rateLimitedUntil) console.log(` Throttled until: ${acct.rateLimitedUntil}`);
|
|
322
|
+
console.log('');
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
console.error(`Cannot connect to proxy at localhost:${config.proxy.port}`);
|
|
326
|
+
console.error('Is the server running? Start with: teamclaude server');
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── accounts ────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
async function accountsCommand() {
|
|
334
|
+
const config = await loadOrCreateConfig();
|
|
335
|
+
|
|
336
|
+
if (config.accounts.length === 0) {
|
|
337
|
+
console.log('No accounts configured.');
|
|
338
|
+
console.log('Add one with: teamclaude import, teamclaude login, or teamclaude login --api');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Fetch profiles in parallel for all OAuth accounts
|
|
343
|
+
const profiles = await Promise.all(
|
|
344
|
+
config.accounts.map(a =>
|
|
345
|
+
a.type === 'oauth' && a.accessToken ? fetchProfile(a.accessToken) : null
|
|
346
|
+
)
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Deduplicate by accountUuid — keep the last (most recently added) entry
|
|
350
|
+
const seen = new Map();
|
|
351
|
+
let removed = 0;
|
|
352
|
+
for (let i = config.accounts.length - 1; i >= 0; i--) {
|
|
353
|
+
const a = config.accounts[i];
|
|
354
|
+
const uuid = profiles[i]?.accountUuid || a.accountUuid;
|
|
355
|
+
if (uuid) {
|
|
356
|
+
if (seen.has(uuid)) {
|
|
357
|
+
config.accounts.splice(i, 1);
|
|
358
|
+
profiles.splice(i, 1);
|
|
359
|
+
removed++;
|
|
360
|
+
} else {
|
|
361
|
+
seen.set(uuid, i);
|
|
362
|
+
// Update stored UUID and name from profile
|
|
363
|
+
if (profiles[i]) {
|
|
364
|
+
a.accountUuid = profiles[i].accountUuid;
|
|
365
|
+
if (profiles[i].email) a.name = profiles[i].email;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (removed > 0) {
|
|
371
|
+
await saveConfig(config);
|
|
372
|
+
console.log(`Removed ${removed} duplicate account(s)\n`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const [i, a] of config.accounts.entries()) {
|
|
376
|
+
const p = profiles[i];
|
|
377
|
+
|
|
378
|
+
if (a.type === 'apikey') {
|
|
379
|
+
console.log(` [${i + 1}] ${a.name} (apikey) ${a.apiKey?.slice(0, 15)}...`);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// OAuth account
|
|
384
|
+
const tier = p?.hasClaudeMax ? 'Max' : p?.hasClaudePro ? 'Pro' : 'subscription';
|
|
385
|
+
const status = p ? `Claude ${tier}` : 'unknown (profile fetch failed)';
|
|
386
|
+
const src = a.source ? `, ${a.source}` : '';
|
|
387
|
+
console.log(` [${i + 1}] ${a.name} (${status}${src})`);
|
|
388
|
+
if (p?.email && p.email !== a.name) console.log(` Email: ${p.email}`);
|
|
389
|
+
if (p?.orgName) console.log(` Org: ${p.orgName}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── api ─────────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
async function apiCommand() {
|
|
396
|
+
const config = await loadOrCreateConfig();
|
|
397
|
+
const path = args[1];
|
|
398
|
+
|
|
399
|
+
if (!path) {
|
|
400
|
+
console.error('Usage: teamclaude api <path> [--account NAME] [--method POST] [--data JSON]');
|
|
401
|
+
console.error('Example: teamclaude api /api/oauth/claude_cli/roles');
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Find account to use
|
|
406
|
+
const accountName = argValue('--account');
|
|
407
|
+
const method = (argValue('--method') || 'GET').toUpperCase();
|
|
408
|
+
const data = argValue('--data');
|
|
409
|
+
|
|
410
|
+
const accounts = await resolveAccounts(config);
|
|
411
|
+
let account;
|
|
412
|
+
if (accountName) {
|
|
413
|
+
account = accounts.find(a => a.name === accountName);
|
|
414
|
+
if (!account) { console.error(`Account "${accountName}" not found`); process.exit(1); }
|
|
415
|
+
} else {
|
|
416
|
+
account = accounts.find(a => a.type === 'oauth') || accounts[0];
|
|
417
|
+
if (!account) { console.error('No accounts configured'); process.exit(1); }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const credential = account.accessToken || account.apiKey;
|
|
421
|
+
const isOAuth = account.type === 'oauth';
|
|
422
|
+
const upstream = config.upstream || 'https://api.anthropic.com';
|
|
423
|
+
const url = path.startsWith('http') ? path : `${upstream}${path}`;
|
|
424
|
+
|
|
425
|
+
const headers = isOAuth
|
|
426
|
+
? { 'Authorization': `Bearer ${credential}` }
|
|
427
|
+
: { 'x-api-key': credential };
|
|
428
|
+
|
|
429
|
+
const fetchOpts = { method, headers };
|
|
430
|
+
if (data) {
|
|
431
|
+
headers['Content-Type'] = 'application/json';
|
|
432
|
+
fetchOpts.body = data;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const res = await fetch(url, fetchOpts);
|
|
436
|
+
|
|
437
|
+
// Print response headers to stderr
|
|
438
|
+
console.error(`${res.status} ${res.statusText}`);
|
|
439
|
+
for (const [k, v] of res.headers.entries()) {
|
|
440
|
+
console.error(` ${k}: ${v}`);
|
|
441
|
+
}
|
|
442
|
+
console.error('');
|
|
443
|
+
|
|
444
|
+
// Print body to stdout
|
|
445
|
+
const body = await res.text();
|
|
446
|
+
try {
|
|
447
|
+
console.log(JSON.stringify(JSON.parse(body), null, 2));
|
|
448
|
+
} catch {
|
|
449
|
+
console.log(body);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── remove ──────────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
async function removeCommand() {
|
|
456
|
+
const config = await loadOrCreateConfig();
|
|
457
|
+
const name = args[1];
|
|
458
|
+
|
|
459
|
+
if (!name) {
|
|
460
|
+
console.error('Usage: teamclaude remove <account-name>');
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const idx = config.accounts.findIndex(a => a.name === name);
|
|
465
|
+
if (idx < 0) {
|
|
466
|
+
console.error(`Account "${name}" not found`);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
config.accounts.splice(idx, 1);
|
|
471
|
+
await saveConfig(config);
|
|
472
|
+
console.log(`Removed account "${name}"`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── help ────────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
function showHelp() {
|
|
478
|
+
console.log(`TeamClaude - Multi-account Claude proxy
|
|
479
|
+
|
|
480
|
+
Usage: teamclaude [command] [options]
|
|
481
|
+
|
|
482
|
+
Commands:
|
|
483
|
+
server Start the proxy server (default)
|
|
484
|
+
import Import credentials from Claude Code
|
|
485
|
+
login OAuth login via browser
|
|
486
|
+
login --api Add an API key account
|
|
487
|
+
env Print env vars to use with Claude
|
|
488
|
+
run [-- args...] Run Claude Code through the proxy
|
|
489
|
+
status Show proxy & account status (live)
|
|
490
|
+
accounts List configured accounts
|
|
491
|
+
remove <name> Remove an account
|
|
492
|
+
api <path> Call an API endpoint with account credentials
|
|
493
|
+
help Show this help
|
|
494
|
+
|
|
495
|
+
Options:
|
|
496
|
+
--name NAME Set account name (import/login)
|
|
497
|
+
--from PATH Credentials path (import, default: ~/.claude/.credentials.json)
|
|
498
|
+
--log-to DIR Log full requests/responses to DIR (server, one file per request)
|
|
499
|
+
|
|
500
|
+
Config: ${getConfigPath()}
|
|
501
|
+
`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ── shared account upsert ────────────────────────────────────
|
|
505
|
+
|
|
506
|
+
async function upsertOAuthAccount(config, name, creds, source = 'unknown') {
|
|
507
|
+
// Fetch profile to auto-name and deduplicate by account UUID
|
|
508
|
+
const profile = await fetchProfile(creds.accessToken);
|
|
509
|
+
|
|
510
|
+
if (!name && profile?.email) {
|
|
511
|
+
name = profile.email;
|
|
512
|
+
const tier = profile.hasClaudeMax ? 'Max' : profile.hasClaudePro ? 'Pro' : null;
|
|
513
|
+
if (tier) console.log(`Detected Claude ${tier} account: ${profile.email}`);
|
|
514
|
+
}
|
|
515
|
+
if (!name) {
|
|
516
|
+
const n = config.accounts.filter(a => a.name.startsWith('account-')).length + 1;
|
|
517
|
+
name = `account-${n}`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const account = {
|
|
521
|
+
name,
|
|
522
|
+
type: 'oauth',
|
|
523
|
+
source,
|
|
524
|
+
accountUuid: profile?.accountUuid || null,
|
|
525
|
+
accessToken: creds.accessToken,
|
|
526
|
+
refreshToken: creds.refreshToken,
|
|
527
|
+
expiresAt: creds.expiresAt,
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// Deduplicate: match by UUID first, then by name
|
|
531
|
+
let idx = profile?.accountUuid
|
|
532
|
+
? config.accounts.findIndex(a => a.accountUuid === profile.accountUuid)
|
|
533
|
+
: -1;
|
|
534
|
+
if (idx < 0) idx = config.accounts.findIndex(a => a.name === name);
|
|
535
|
+
|
|
536
|
+
if (idx >= 0) {
|
|
537
|
+
config.accounts[idx] = account;
|
|
538
|
+
console.log(`Updated account "${name}"`);
|
|
539
|
+
} else {
|
|
540
|
+
config.accounts.push(account);
|
|
541
|
+
console.log(`Added account "${name}"`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
await saveConfig(config);
|
|
545
|
+
console.log(`Saved to ${getConfigPath()}`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ── helpers ─────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
async function resolveAccounts(config) {
|
|
551
|
+
const accounts = [];
|
|
552
|
+
for (const acct of config.accounts) {
|
|
553
|
+
if (acct.type === 'oauth') {
|
|
554
|
+
if (acct.importFrom) {
|
|
555
|
+
try {
|
|
556
|
+
const creds = await importCredentials(acct.importFrom);
|
|
557
|
+
accounts.push({ name: acct.name, type: 'oauth', ...creds });
|
|
558
|
+
console.log(`Imported "${acct.name}" from ${acct.importFrom}`);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
console.error(`Failed to import "${acct.name}": ${err.message}`);
|
|
561
|
+
}
|
|
562
|
+
} else if (acct.accessToken) {
|
|
563
|
+
accounts.push(acct);
|
|
564
|
+
} else {
|
|
565
|
+
console.error(`No token for "${acct.name}", skipping`);
|
|
566
|
+
}
|
|
567
|
+
} else if (acct.type === 'apikey' && acct.apiKey) {
|
|
568
|
+
accounts.push(acct);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return accounts;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function argValue(flag) {
|
|
575
|
+
const i = args.indexOf(flag);
|
|
576
|
+
return (i >= 0 && args[i + 1]) ? args[i + 1] : null;
|
|
577
|
+
}
|