@timo972/cc-router 0.12.4 → 0.13.0-rc.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/CHANGELOG.md +35 -0
- package/README.md +2 -2
- package/dist/cli/account-flows.js +422 -0
- package/dist/cli/cli-errors.js +37 -0
- package/dist/cli/cmd-accounts.js +215 -203
- package/dist/cli/cmd-setup.js +5 -186
- package/dist/cli/cmd-status.js +85 -24
- package/dist/cli/index.js +15 -3
- package/dist/config/manager.js +16 -7
- package/dist/providers/anthropic/claude-cli.js +104 -0
- package/dist/providers/anthropic/scopes.js +5 -0
- package/dist/providers/anthropic/usage-refresher.js +38 -31
- package/dist/providers/openai/device-oauth.js +20 -1
- package/dist/providers/xai/device-oauth.js +9 -0
- package/dist/proxy/account-info-cache.js +69 -20
- package/dist/proxy/account-post-validation.js +23 -0
- package/dist/proxy/account-refresh.js +59 -0
- package/dist/proxy/account-replace.js +12 -12
- package/dist/proxy/server.js +66 -14
- package/dist/proxy/token-refresher.js +36 -1
- package/dist/proxy/types.js +19 -0
- package/dist/telemetry/contracts.js +2 -0
- package/dist/ui/Dashboard.js +72 -6
- package/dist/ui/accountsApi.js +21 -0
- package/dist/utils/browser.js +36 -0
- package/dist/utils/expiry-input.js +21 -0
- package/dist/utils/token-extractor.js +9 -5
- package/package.json +1 -1
package/dist/cli/cmd-accounts.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { loadAccounts, loadOpenAIAccounts, loadXaiAccounts, accountsFileExists, upsertAccountRecord, removeAccountRecordById, renameAccountRecordById, readConfig
|
|
2
|
+
import { loadAccounts, loadOpenAIAccounts, loadXaiAccounts, accountsFileExists, upsertAccountRecord, removeAccountRecordById, renameAccountRecordById, readConfig } from "../config/manager.js";
|
|
3
3
|
import { saveAccounts } from "../proxy/token-refresher.js";
|
|
4
4
|
import { formatExpiry, redactToken } from "../utils/token-extractor.js";
|
|
5
5
|
import { PROXY_PORT } from "../config/paths.js";
|
|
6
|
-
import { createOpenAIAccountRecord } from "../providers/openai/account-record.js";
|
|
7
|
-
import { loginOpenAIWithDeviceCode } from "../providers/openai/device-oauth.js";
|
|
8
|
-
import { importGrokCliAuth } from "../providers/xai/import-auth.js";
|
|
9
|
-
import { loginXaiWithDeviceCode } from "../providers/xai/device-oauth.js";
|
|
10
6
|
import { isValidAccountId } from "../proxy/account-rename.js";
|
|
11
|
-
import {
|
|
7
|
+
import { failAttemptFromError, withSetupTelemetryFlush, } from "../telemetry/setup-diagnostics.js";
|
|
8
|
+
import { isTokenOnly } from "../proxy/types.js";
|
|
12
9
|
import { sanitizeAccountInfo, formatAccountInfo } from "../providers/account-info.js";
|
|
13
10
|
import { needsReauthentication } from "../providers/auth-state.js";
|
|
11
|
+
import { CliUsageError } from "./cli-errors.js";
|
|
14
12
|
export function registerAccounts(program) {
|
|
15
13
|
const accounts = program
|
|
16
14
|
.command("accounts")
|
|
@@ -45,10 +43,10 @@ export function registerAccounts(program) {
|
|
|
45
43
|
? `\n Accounts (${liveStats.length} in the running proxy)\n`
|
|
46
44
|
: `\n Accounts (${stored.length + openAIStored.length + xaiStored.length} configured)\n`));
|
|
47
45
|
/**
|
|
48
|
-
* Accounts only re-authentication can restore
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
46
|
+
* Accounts only re-authentication can restore. Just the ids: `accounts
|
|
47
|
+
* reauth <id>` resolves the provider itself, so the hint no longer has
|
|
48
|
+
* to pick a per-provider sign-in command — getting that wrong re-added
|
|
49
|
+
* the id under the wrong provider entirely.
|
|
52
50
|
*/
|
|
53
51
|
const reauthNeeded = [];
|
|
54
52
|
if (liveStats) {
|
|
@@ -63,19 +61,23 @@ export function registerAccounts(program) {
|
|
|
63
61
|
// permanently rejected refresh token. Only the latter needs the
|
|
64
62
|
// operator, so it gets its own label rather than hiding in the crowd.
|
|
65
63
|
if (needsReauthentication(s))
|
|
66
|
-
reauthNeeded.push(
|
|
64
|
+
reauthNeeded.push(s.id);
|
|
67
65
|
const status = needsReauthentication(s)
|
|
68
66
|
? chalk.red("✗ re-auth required")
|
|
69
67
|
: s.healthy
|
|
70
68
|
? chalk.green("✓ healthy")
|
|
71
69
|
: chalk.red("✗ unhealthy");
|
|
72
70
|
const busy = s.busy ? chalk.yellow(" [busy]") : "";
|
|
71
|
+
// A `claude setup-token` account has no refresh token, so it never
|
|
72
|
+
// refreshes and carries the inference scope only. Saying so here is
|
|
73
|
+
// what stops "expires" reading as a bug rather than a deadline.
|
|
74
|
+
const tokenOnly = s.tokenOnly ? chalk.gray(" token-only") : "";
|
|
73
75
|
const exp = s.expiresInMs > 0
|
|
74
76
|
? chalk.yellow(formatMs(s.expiresInMs))
|
|
75
77
|
: chalk.red("EXPIRED");
|
|
76
78
|
console.log(` ${chalk.bold(s.id.padEnd(24))}` +
|
|
77
79
|
` ${provider}` +
|
|
78
|
-
` ${status}${busy}` +
|
|
80
|
+
` ${status}${busy}${tokenOnly}` +
|
|
79
81
|
` requests: ${chalk.cyan(String(s.requestCount).padStart(5))}` +
|
|
80
82
|
` errors: ${chalk.red(String(s.errorCount).padStart(3))}` +
|
|
81
83
|
` expires: ${exp}`);
|
|
@@ -110,11 +112,13 @@ export function registerAccounts(program) {
|
|
|
110
112
|
// `authExpired` is persisted, so the dead state is knowable without
|
|
111
113
|
// the proxy running — and this is exactly when an operator looks.
|
|
112
114
|
if (needsReauthentication(a))
|
|
113
|
-
reauthNeeded.push(
|
|
115
|
+
reauthNeeded.push(a.id);
|
|
114
116
|
console.log(` ${chalk.bold(a.id.padEnd(24))}` +
|
|
115
117
|
` ${redactToken(a.tokens.accessToken).padEnd(26)}` +
|
|
116
118
|
` expires: ${expColor}` +
|
|
117
|
-
|
|
119
|
+
(isTokenOnly(a.tokens)
|
|
120
|
+
? ` ${chalk.gray("token-only")}`
|
|
121
|
+
: ` scopes: ${chalk.gray(a.tokens.scopes.join(" "))}`) +
|
|
118
122
|
(needsReauthentication(a) ? ` ${chalk.red("✗ re-auth required")}` : ""));
|
|
119
123
|
}
|
|
120
124
|
for (const a of openAIStored) {
|
|
@@ -122,7 +126,7 @@ export function registerAccounts(program) {
|
|
|
122
126
|
? chalk.yellow(formatExpiry(a.expiresAt))
|
|
123
127
|
: chalk.red("EXPIRED");
|
|
124
128
|
if (needsReauthentication(a))
|
|
125
|
-
reauthNeeded.push(
|
|
129
|
+
reauthNeeded.push(a.id);
|
|
126
130
|
console.log(` ${chalk.bold(a.id.padEnd(24))}` +
|
|
127
131
|
` ${chalk.magenta("openai".padEnd(10))}` +
|
|
128
132
|
` ${redactToken(a.accessToken).padEnd(26)}` +
|
|
@@ -143,204 +147,118 @@ export function registerAccounts(program) {
|
|
|
143
147
|
// out of: the refresh loop has deliberately stopped retrying, so nothing
|
|
144
148
|
// changes until someone re-authenticates. Say so, and say how.
|
|
145
149
|
if (reauthNeeded.length > 0) {
|
|
146
|
-
console.log(chalk.red(`\n ⚠ Needs re-authentication: ${reauthNeeded.
|
|
150
|
+
console.log(chalk.red(`\n ⚠ Needs re-authentication: ${reauthNeeded.join(", ")}`));
|
|
147
151
|
console.log(chalk.gray(" The provider rejected these refresh tokens permanently; they cannot\n"
|
|
148
152
|
+ " be recovered and the refresh loop has stopped retrying them. Sign in\n"
|
|
149
153
|
+ " again under the same account id to resume routing — the existing\n"
|
|
150
154
|
+ " account is replaced, so there is nothing to remove first:"));
|
|
151
|
-
for (const
|
|
152
|
-
console.log(chalk.gray(` ${reauthCommand(
|
|
155
|
+
for (const id of reauthNeeded) {
|
|
156
|
+
console.log(chalk.gray(` ${reauthCommand(id)}`));
|
|
153
157
|
}
|
|
154
158
|
}
|
|
155
159
|
console.log();
|
|
156
160
|
});
|
|
157
|
-
// ── accounts
|
|
158
|
-
accounts
|
|
159
|
-
.command("add")
|
|
160
|
-
.description("Add a new Claude Max account interactively")
|
|
161
|
-
.action(async () => {
|
|
162
|
-
const { setupSingleAccountWithAttempt } = await import("./cmd-setup.js");
|
|
163
|
-
const existing = accountsFileExists() ? loadAccounts() : [];
|
|
164
|
-
const { account, attempt } = await setupSingleAccountWithAttempt(existing.length + 1);
|
|
165
|
-
if (!account) {
|
|
166
|
-
console.log(chalk.yellow("\nNo account added.\n"));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
// Merge: replace by ID if already exists, otherwise append
|
|
170
|
-
const merged = [
|
|
171
|
-
...existing.filter(a => a.id !== account.id),
|
|
172
|
-
account,
|
|
173
|
-
];
|
|
174
|
-
let mode;
|
|
175
|
-
try {
|
|
176
|
-
// Only `addStored` is overridden: this flow merges by id across the
|
|
177
|
-
// whole Claude pool. `tryAddLive` must stay the default so the live
|
|
178
|
-
// pool is asked to replace an existing id rather than reject it.
|
|
179
|
-
({ mode } = await addAccountRuntimeAware(serialize([account])[0], {
|
|
180
|
-
addStored: () => saveAccounts(merged),
|
|
181
|
-
}));
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
endFailedAttempt(attempt, error, "persistence");
|
|
185
|
-
throw error;
|
|
186
|
-
}
|
|
187
|
-
attempt.stageCompleted("persistence");
|
|
188
|
-
attempt.succeeded();
|
|
189
|
-
console.log(chalk.green(`\n✓ Account "${account.id}" added (${merged.length} total).\n`));
|
|
190
|
-
printAddOutcome(mode);
|
|
191
|
-
});
|
|
192
|
-
// ── accounts add-openai ──────────────────────────────────────────────────
|
|
161
|
+
// ── accounts login ───────────────────────────────────────────────────────
|
|
193
162
|
accounts
|
|
194
|
-
.command("
|
|
195
|
-
.description("
|
|
196
|
-
.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
163
|
+
.command("login [provider]")
|
|
164
|
+
.description("Sign in to a Claude, OpenAI or Grok account in the browser (provider: claude | openai | grok)")
|
|
165
|
+
.option("--id <id>", "Account id to store the credentials under")
|
|
166
|
+
.option("--email <email>", "Email to prefill on the sign-in page")
|
|
167
|
+
.option("--long-lived", "Claude only: create a long-lived token with claude setup-token instead of a full sign-in")
|
|
168
|
+
.action(async (providerArg, opts) => withSetupTelemetryFlush(async () => {
|
|
169
|
+
const provider = await chooseProvider(providerArg);
|
|
170
|
+
const flows = await import("./account-flows.js");
|
|
171
|
+
if (provider === "claude") {
|
|
172
|
+
const { account, attempt } = await flows.collectClaudeAccount({
|
|
173
|
+
index: (accountsFileExists() ? loadAccounts().length : 0) + 1,
|
|
174
|
+
fixedId: opts.id,
|
|
175
|
+
email: opts.email,
|
|
176
|
+
offer: "login",
|
|
177
|
+
...(opts.longLived ? { method: "setup_token" } : {}),
|
|
206
178
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
message: "OpenAI refresh token:",
|
|
214
|
-
mask: "*",
|
|
215
|
-
validate: (v) => v.trim().length > 0 || "Refresh token is required",
|
|
216
|
-
});
|
|
217
|
-
const expiresAt = await input({
|
|
218
|
-
message: "Access token expiry (Unix ms):",
|
|
219
|
-
default: String(Date.now() + 60 * 60 * 1000),
|
|
220
|
-
validate: (v) => Number.isFinite(Number(v)) && Number(v) > 0 || "Enter a positive Unix timestamp in milliseconds",
|
|
221
|
-
});
|
|
222
|
-
const scopes = await input({
|
|
223
|
-
message: "Scopes:",
|
|
224
|
-
default: "openid profile email offline_access",
|
|
225
|
-
});
|
|
226
|
-
attempt.stageCompleted("credential_read");
|
|
227
|
-
reached = "credential_parse";
|
|
228
|
-
const record = createOpenAIAccountRecord({
|
|
229
|
-
id,
|
|
230
|
-
accessToken,
|
|
231
|
-
refreshToken,
|
|
232
|
-
expiresAt,
|
|
233
|
-
scopes,
|
|
234
|
-
});
|
|
235
|
-
attempt.stageCompleted("credential_parse");
|
|
236
|
-
reached = "persistence";
|
|
237
|
-
const { mode } = await addAccountRuntimeAware(record);
|
|
238
|
-
attempt.stageCompleted("persistence");
|
|
239
|
-
attempt.succeeded();
|
|
240
|
-
console.log(chalk.green(`\n✓ OpenAI account "${record.id}" saved.\n`));
|
|
241
|
-
printAddOutcome(mode);
|
|
242
|
-
console.log(chalk.yellow(" Treat this as experimental until the OAuth login wizard lands.\n"));
|
|
179
|
+
if (!account) {
|
|
180
|
+
console.log(chalk.yellow("\nNo account added.\n"));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await persistClaude(account, attempt);
|
|
184
|
+
return;
|
|
243
185
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
186
|
+
if (provider === "openai") {
|
|
187
|
+
// The id prompt (with its `openai-account-N` default) lives in the
|
|
188
|
+
// flow, so an absent --id is a prompt rather than a guessed name.
|
|
189
|
+
const { record, attempt } = await flows.loginOpenAIAccount({ accountId: opts.id, email: opts.email });
|
|
190
|
+
await persistRecord(record, attempt, "OpenAI account");
|
|
191
|
+
return;
|
|
247
192
|
}
|
|
193
|
+
const record = await flows.loginGrokAccount({ accountId: opts.id });
|
|
194
|
+
upsertAccountRecord(record);
|
|
195
|
+
console.log(chalk.green(`\n✓ Grok account "${record.id}" saved via device login.\n`));
|
|
196
|
+
printAddOutcome("stored");
|
|
248
197
|
}));
|
|
249
|
-
// ── accounts
|
|
198
|
+
// ── accounts add ─────────────────────────────────────────────────────────
|
|
250
199
|
accounts
|
|
251
|
-
.command("
|
|
252
|
-
.description("
|
|
253
|
-
.
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
});
|
|
263
|
-
console.log(chalk.cyan("\nOpenAI Codex device login"));
|
|
264
|
-
console.log(chalk.gray("This will open no local callback server. You will approve the login in your browser.\n"));
|
|
265
|
-
const record = await loginOpenAIWithDeviceCode({
|
|
266
|
-
accountId,
|
|
267
|
-
onDeviceCode: (code) => {
|
|
268
|
-
console.log(chalk.bold("1. Open this URL:"));
|
|
269
|
-
console.log(` ${chalk.cyan(code.verificationUrl)}`);
|
|
270
|
-
console.log(chalk.bold("2. Enter this code:"));
|
|
271
|
-
console.log(` ${chalk.cyan(code.userCode)}\n`);
|
|
272
|
-
console.log(chalk.gray("Waiting for authorization..."));
|
|
273
|
-
},
|
|
274
|
-
onStageCompleted: (stage) => {
|
|
275
|
-
attempt.stageCompleted(stage);
|
|
276
|
-
reached = stage;
|
|
277
|
-
},
|
|
200
|
+
.command("add [provider]")
|
|
201
|
+
.description("Import credentials that already exist: Claude Keychain / credentials file / pasted tokens, OpenAI tokens, or ~/.grok")
|
|
202
|
+
.option("--id <id>", "Account id to store the credentials under")
|
|
203
|
+
.action(async (providerArg, opts) => withSetupTelemetryFlush(async () => {
|
|
204
|
+
const provider = await chooseProvider(providerArg);
|
|
205
|
+
const flows = await import("./account-flows.js");
|
|
206
|
+
if (provider === "claude") {
|
|
207
|
+
const { account, attempt } = await flows.collectClaudeAccount({
|
|
208
|
+
index: (accountsFileExists() ? loadAccounts().length : 0) + 1,
|
|
209
|
+
fixedId: opts.id,
|
|
210
|
+
offer: "import",
|
|
278
211
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
catch (error) {
|
|
287
|
-
endFailedAttempt(attempt, error, reached);
|
|
288
|
-
throw error;
|
|
289
|
-
}
|
|
290
|
-
}));
|
|
291
|
-
// ── accounts add-grok ────────────────────────────────────────────────────
|
|
292
|
-
accounts
|
|
293
|
-
.command("add-grok")
|
|
294
|
-
.description("Import the Grok CLI login from ~/.grok/auth.json")
|
|
295
|
-
.action(async () => {
|
|
296
|
-
const { input } = await import("@inquirer/prompts");
|
|
297
|
-
let imported;
|
|
298
|
-
try {
|
|
299
|
-
imported = importGrokCliAuth();
|
|
212
|
+
if (!account) {
|
|
213
|
+
console.log(chalk.yellow("\nNo account added.\n"));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
await persistClaude(account, attempt);
|
|
217
|
+
return;
|
|
300
218
|
}
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
process.exit(1);
|
|
219
|
+
if (provider === "openai") {
|
|
220
|
+
const { record, attempt } = await flows.importOpenAIAccount({ accountId: opts.id });
|
|
221
|
+
await persistRecord(record, attempt, "OpenAI account");
|
|
222
|
+
return;
|
|
306
223
|
}
|
|
307
|
-
const
|
|
308
|
-
message: "Grok account ID:",
|
|
309
|
-
default: imported.id,
|
|
310
|
-
validate: (v) => /^[a-zA-Z0-9_-]+$/.test(v) || "Only letters, numbers, _ and - allowed",
|
|
311
|
-
});
|
|
312
|
-
const record = { ...imported, id };
|
|
224
|
+
const record = await flows.importGrokAccount({ accountId: opts.id });
|
|
313
225
|
upsertAccountRecord(record);
|
|
314
226
|
console.log(chalk.green(`\n✓ Grok account "${record.id}" imported from ~/.grok.\n`));
|
|
315
227
|
printAddOutcome("stored");
|
|
316
|
-
});
|
|
317
|
-
// ── accounts
|
|
228
|
+
}));
|
|
229
|
+
// ── accounts reauth ──────────────────────────────────────────────────────
|
|
318
230
|
accounts
|
|
319
|
-
.command("
|
|
320
|
-
.description("Sign in
|
|
321
|
-
.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
231
|
+
.command("reauth <id>")
|
|
232
|
+
.description("Sign an existing account in again under the same id (its provider and email are looked up for you)")
|
|
233
|
+
.option("--email <email>", "Override the email to prefill on the sign-in page")
|
|
234
|
+
.option("--long-lived", "Claude only: use claude setup-token")
|
|
235
|
+
.action(async (id, opts) => withSetupTelemetryFlush(async () => {
|
|
236
|
+
const live = await fetchLiveStats();
|
|
237
|
+
const target = resolveReauthTarget(id, live, {
|
|
238
|
+
anthropic: accountsFileExists() ? loadAccounts() : [],
|
|
239
|
+
openai: loadOpenAIAccounts(),
|
|
240
|
+
xai: loadXaiAccounts(),
|
|
327
241
|
});
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
242
|
+
if (!target) {
|
|
243
|
+
const { ids } = mergeAccountInventory(loadAccounts().map(a => a.id), loadOpenAIAccounts().map(a => a.id), live, loadXaiAccounts().map(a => a.id));
|
|
244
|
+
console.log(chalk.red(`✗ Account "${id}" not found.`));
|
|
245
|
+
console.log(chalk.gray(` Available: ${ids.join(", ")}`));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
// Grok credentials are minted by the Grok CLI and read back out of
|
|
249
|
+
// ~/.grok; there is no id-preserving sign-in to re-run here.
|
|
250
|
+
if ("grok" in target) {
|
|
251
|
+
console.log(chalk.yellow(`Grok credentials live in ~/.grok. Run ${chalk.white("grok login")}, then ${chalk.white("cc-router accounts add grok")}.`));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
const flows = await import("./account-flows.js");
|
|
255
|
+
const result = await flows.collectReauthRecord({ ...target, ...(opts.email ? { email: opts.email } : {}) }, { longLived: opts.longLived });
|
|
256
|
+
if (!result) {
|
|
257
|
+
console.log(chalk.yellow("\nNo account re-authenticated.\n"));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
await persistRecord(result.record, result.attempt, target.provider === "openai_subscription" ? "OpenAI account" : "Account");
|
|
261
|
+
}));
|
|
344
262
|
// ── accounts remove ───────────────────────────────────────────────────────
|
|
345
263
|
accounts
|
|
346
264
|
.command("remove <id>")
|
|
@@ -426,7 +344,102 @@ export function registerAccounts(program) {
|
|
|
426
344
|
: chalk.gray(" Saved to accounts.json — loads on next start: cc-router start"));
|
|
427
345
|
});
|
|
428
346
|
}
|
|
429
|
-
|
|
347
|
+
/**
|
|
348
|
+
* The provider named on the command line, if one was.
|
|
349
|
+
*
|
|
350
|
+
* Unknown values throw rather than falling back to a picker: silently asking
|
|
351
|
+
* again would let `accounts login clade` sign the operator into whatever they
|
|
352
|
+
* then clicked, under a command they did not mean to run.
|
|
353
|
+
*/
|
|
354
|
+
export function parseProviderArg(value) {
|
|
355
|
+
if (value === undefined)
|
|
356
|
+
return undefined;
|
|
357
|
+
if (value === "claude" || value === "openai" || value === "grok")
|
|
358
|
+
return value;
|
|
359
|
+
throw new CliUsageError(`Unknown provider "${value}" — use claude, openai or grok`);
|
|
360
|
+
}
|
|
361
|
+
async function chooseProvider(given) {
|
|
362
|
+
const parsed = parseProviderArg(given);
|
|
363
|
+
if (parsed)
|
|
364
|
+
return parsed;
|
|
365
|
+
const { select } = await import("@inquirer/prompts");
|
|
366
|
+
return select({
|
|
367
|
+
message: "Which provider?",
|
|
368
|
+
choices: [
|
|
369
|
+
{ name: "Claude (Claude Max / Pro subscription)", value: "claude" },
|
|
370
|
+
{ name: "OpenAI (ChatGPT / Codex subscription)", value: "openai" },
|
|
371
|
+
{ name: "Grok (xAI)", value: "grok" },
|
|
372
|
+
],
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Persist a collected Claude account, replacing any account already holding
|
|
377
|
+
* its id. The merge is by id across the whole Claude pool, so only `addStored`
|
|
378
|
+
* is overridden — `tryAddLive` must stay the default, which asks the running
|
|
379
|
+
* proxy to replace rather than reject the id.
|
|
380
|
+
*/
|
|
381
|
+
async function persistClaude(account, attempt) {
|
|
382
|
+
const { accountToRecord } = await import("./account-flows.js");
|
|
383
|
+
const existing = accountsFileExists() ? loadAccounts() : [];
|
|
384
|
+
const merged = [...existing.filter(a => a.id !== account.id), account];
|
|
385
|
+
let mode;
|
|
386
|
+
try {
|
|
387
|
+
({ mode } = await addAccountRuntimeAware(accountToRecord(account), {
|
|
388
|
+
addStored: () => saveAccounts(merged),
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
endFailedAttempt(attempt, error, "persistence");
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
attempt.stageCompleted("persistence");
|
|
396
|
+
attempt.succeeded();
|
|
397
|
+
console.log(chalk.green(`\n✓ Account "${account.id}" saved (${merged.length} Claude accounts).\n`));
|
|
398
|
+
printAddOutcome(mode);
|
|
399
|
+
}
|
|
400
|
+
/** Persist any already-serialized record (OpenAI, or a re-authenticated account). */
|
|
401
|
+
async function persistRecord(record, attempt, label) {
|
|
402
|
+
let mode;
|
|
403
|
+
try {
|
|
404
|
+
({ mode } = await addAccountRuntimeAware(record));
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
if (attempt)
|
|
408
|
+
endFailedAttempt(attempt, error, "persistence");
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
attempt?.stageCompleted("persistence");
|
|
412
|
+
attempt?.succeeded();
|
|
413
|
+
console.log(chalk.green(`\n✓ ${label} "${record.id}" saved.\n`));
|
|
414
|
+
printAddOutcome(mode);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Which account `accounts reauth <id>` should sign in again, and as whom.
|
|
418
|
+
*
|
|
419
|
+
* The live pool wins: it is the only source that carries the fetched account
|
|
420
|
+
* metadata, and its email is what makes the sign-in page land on the right
|
|
421
|
+
* account instead of whichever one the browser is already holding. Disk is the
|
|
422
|
+
* fallback for a proxy that is not running, and knows no email.
|
|
423
|
+
*/
|
|
424
|
+
export function resolveReauthTarget(id, live, stored) {
|
|
425
|
+
const liveMatch = live?.find(a => a.id === id);
|
|
426
|
+
if (liveMatch) {
|
|
427
|
+
if (liveMatch.provider === "xai_subscription")
|
|
428
|
+
return { grok: true };
|
|
429
|
+
return {
|
|
430
|
+
id,
|
|
431
|
+
provider: liveMatch.provider === "openai_subscription" ? "openai_subscription" : "anthropic_subscription",
|
|
432
|
+
...(liveMatch.accountInfo?.email ? { email: liveMatch.accountInfo.email } : {}),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (stored.xai.some(a => a.id === id))
|
|
436
|
+
return { grok: true };
|
|
437
|
+
if (stored.openai.some(a => a.id === id))
|
|
438
|
+
return { id, provider: "openai_subscription" };
|
|
439
|
+
if (stored.anthropic.some(a => a.id === id))
|
|
440
|
+
return { id, provider: "anthropic_subscription" };
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
430
443
|
/**
|
|
431
444
|
* Close a setup attempt that ended in a thrown error. A cancelled prompt is a
|
|
432
445
|
* user decision, not a failure, and only an unexpected failure gets a
|
|
@@ -494,6 +507,7 @@ export function buildStoredAccountsJson(anthropicAccounts, openAIAccounts, xaiAc
|
|
|
494
507
|
expiresAt: a.tokens.expiresAt,
|
|
495
508
|
scopes: a.tokens.scopes,
|
|
496
509
|
...(needsReauthentication(a) ? { authExpired: true } : {}),
|
|
510
|
+
...(isTokenOnly(a.tokens) ? { tokenOnly: true } : {}),
|
|
497
511
|
})),
|
|
498
512
|
...openAIAccounts.map(a => ({
|
|
499
513
|
id: a.id,
|
|
@@ -669,19 +683,17 @@ export async function addAccountRuntimeAware(record, dependencies = {}) {
|
|
|
669
683
|
return { mode: "stored" };
|
|
670
684
|
}
|
|
671
685
|
/**
|
|
672
|
-
* The
|
|
686
|
+
* The command that recovers this account, whatever its provider.
|
|
673
687
|
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
688
|
+
* One command for every provider, because it resolves the provider from the id
|
|
689
|
+
* itself — the previous per-provider hints had to guess, and guessing wrong
|
|
690
|
+
* re-added the id under the wrong provider. It re-registers under the id the
|
|
691
|
+
* operator already has, and the live pool replaces rather than rejects it, so
|
|
692
|
+
* no deletion step is needed — which also avoids the running proxy refusing to
|
|
693
|
+
* delete a lone Claude account.
|
|
678
694
|
*/
|
|
679
|
-
function reauthCommand(
|
|
680
|
-
|
|
681
|
-
return "cc-router accounts login-openai";
|
|
682
|
-
if (provider === "xai_subscription")
|
|
683
|
-
return "cc-router accounts login-grok";
|
|
684
|
-
return "cc-router accounts add";
|
|
695
|
+
function reauthCommand(id) {
|
|
696
|
+
return `cc-router accounts reauth ${id}`;
|
|
685
697
|
}
|
|
686
698
|
async function fetchLiveStats() {
|
|
687
699
|
try {
|