@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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 +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { findByIdentity, resolveIdentityFromOAuthExchange } from "../../account-identity.js";
|
|
3
|
+
import { CLIENT_ID, loadConfig } from "../../config.js";
|
|
4
|
+
import { authorize, exchange, revoke } from "../../oauth.js";
|
|
5
|
+
import { createDefaultStats, getStoragePath, loadAccounts, saveAccounts, type AccountMetadata } from "../../storage.js";
|
|
6
|
+
import { loadAccountsWithRepair } from "../../accounts/repair.js";
|
|
7
|
+
import { confirm, intro, isCancel, log, spinner, text } from "@clack/prompts";
|
|
8
|
+
import {
|
|
9
|
+
c,
|
|
10
|
+
fmtTokens,
|
|
11
|
+
formatDuration,
|
|
12
|
+
formatTimeAgo,
|
|
13
|
+
pad,
|
|
14
|
+
renderUsageLines,
|
|
15
|
+
rpad,
|
|
16
|
+
shortPath,
|
|
17
|
+
USAGE_INDENT,
|
|
18
|
+
} from "../formatting.js";
|
|
19
|
+
|
|
20
|
+
type RefreshableAccount = Pick<AccountMetadata, "refreshToken" | "access" | "expires" | "token_updated_at">;
|
|
21
|
+
type UsageAccount = Pick<AccountMetadata, "refreshToken" | "access" | "expires" | "enabled" | "token_updated_at">;
|
|
22
|
+
type LogoutOptions = { force?: boolean; all?: boolean };
|
|
23
|
+
type RemoveOptions = { force?: boolean };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Refresh an account's OAuth access token.
|
|
27
|
+
* Mutates the account object in-place and returns the new access token.
|
|
28
|
+
*/
|
|
29
|
+
export async function refreshAccessToken(account: RefreshableAccount) {
|
|
30
|
+
try {
|
|
31
|
+
const resp = await fetch("https://platform.claude.com/v1/oauth/token", {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "Content-Type": "application/json" },
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
grant_type: "refresh_token",
|
|
36
|
+
refresh_token: account.refreshToken,
|
|
37
|
+
client_id: CLIENT_ID,
|
|
38
|
+
}),
|
|
39
|
+
signal: AbortSignal.timeout(5000),
|
|
40
|
+
});
|
|
41
|
+
if (!resp.ok) return null;
|
|
42
|
+
|
|
43
|
+
const json = (await resp.json()) as {
|
|
44
|
+
access_token: string;
|
|
45
|
+
expires_in: number;
|
|
46
|
+
refresh_token?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
account.access = json.access_token;
|
|
50
|
+
account.expires = Date.now() + json.expires_in * 1000;
|
|
51
|
+
if (json.refresh_token) account.refreshToken = json.refresh_token;
|
|
52
|
+
account.token_updated_at = Date.now();
|
|
53
|
+
return json.access_token;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Fetch usage quotas from the Anthropic OAuth usage endpoint.
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchUsage(accessToken: string) {
|
|
63
|
+
try {
|
|
64
|
+
const resp = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
65
|
+
headers: {
|
|
66
|
+
authorization: `Bearer ${accessToken}`,
|
|
67
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
68
|
+
accept: "application/json",
|
|
69
|
+
},
|
|
70
|
+
signal: AbortSignal.timeout(5000),
|
|
71
|
+
});
|
|
72
|
+
if (!resp.ok) return null;
|
|
73
|
+
return resp.json() as Promise<Record<string, unknown>>;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Ensure an account has a valid access token and fetch its usage data.
|
|
81
|
+
*/
|
|
82
|
+
export async function ensureTokenAndFetchUsage(account: UsageAccount) {
|
|
83
|
+
if (!account.enabled) return { usage: null, tokenRefreshed: false };
|
|
84
|
+
|
|
85
|
+
let token: string | undefined = account.access;
|
|
86
|
+
let tokenRefreshed = false;
|
|
87
|
+
|
|
88
|
+
if (!token || !account.expires || account.expires < Date.now()) {
|
|
89
|
+
const refreshedToken = await refreshAccessToken(account);
|
|
90
|
+
tokenRefreshed = !!refreshedToken;
|
|
91
|
+
if (!refreshedToken) return { usage: null, tokenRefreshed: false };
|
|
92
|
+
token = refreshedToken;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const usage = await fetchUsage(token);
|
|
96
|
+
return { usage, tokenRefreshed };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Open a URL in the user's default browser.
|
|
101
|
+
* Best-effort: uses platform-specific command, silently fails on error.
|
|
102
|
+
*/
|
|
103
|
+
function openBrowser(url: string) {
|
|
104
|
+
if (process.platform === "win32") {
|
|
105
|
+
exec(`cmd /c start "" ${JSON.stringify(url)}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
110
|
+
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Run the OAuth PKCE login flow from the CLI.
|
|
115
|
+
* Opens browser, prompts for code, exchanges for tokens.
|
|
116
|
+
*/
|
|
117
|
+
async function runOAuthFlow() {
|
|
118
|
+
const { url, verifier, state } = await authorize("max");
|
|
119
|
+
|
|
120
|
+
log.info("Opening browser for Anthropic OAuth login...");
|
|
121
|
+
log.info("If your browser didn't open, visit this URL:");
|
|
122
|
+
log.info(url);
|
|
123
|
+
|
|
124
|
+
openBrowser(url);
|
|
125
|
+
|
|
126
|
+
const code = await text({
|
|
127
|
+
message: "Paste the authorization code here:",
|
|
128
|
+
placeholder: "auth-code#state",
|
|
129
|
+
});
|
|
130
|
+
if (isCancel(code)) {
|
|
131
|
+
log.warn("Login cancelled.");
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const trimmed = code.trim();
|
|
136
|
+
if (!trimmed) {
|
|
137
|
+
log.error("Error: no authorization code provided.");
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const parts = trimmed.split("#");
|
|
142
|
+
if (state && parts[1] && parts[1] !== state) {
|
|
143
|
+
log.error("Error: OAuth state mismatch — possible CSRF attack.");
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const s = spinner();
|
|
148
|
+
s.start("Exchanging authorization code for tokens...");
|
|
149
|
+
const credentials = await exchange(trimmed, verifier);
|
|
150
|
+
if (credentials.type === "failed") {
|
|
151
|
+
if (credentials.details) {
|
|
152
|
+
s.stop(`Token exchange failed (${credentials.details}).`);
|
|
153
|
+
} else {
|
|
154
|
+
s.stop("Token exchange failed. The code may be invalid or expired.");
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
s.stop("Token exchange successful.");
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
refresh: credentials.refresh,
|
|
163
|
+
access: credentials.access,
|
|
164
|
+
expires: credentials.expires,
|
|
165
|
+
email: credentials.email,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Login: add a new account via browser OAuth flow.
|
|
171
|
+
*/
|
|
172
|
+
export async function cmdLogin() {
|
|
173
|
+
if (!process.stdin.isTTY) {
|
|
174
|
+
log.error("Error: 'login' requires an interactive terminal.");
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
intro("Login — Add a new account");
|
|
179
|
+
|
|
180
|
+
const stored = await loadAccounts();
|
|
181
|
+
const credentials = await runOAuthFlow();
|
|
182
|
+
if (!credentials) return 1;
|
|
183
|
+
|
|
184
|
+
const storage = stored || { version: 1, accounts: [], activeIndex: 0 };
|
|
185
|
+
const identity = resolveIdentityFromOAuthExchange(credentials);
|
|
186
|
+
|
|
187
|
+
const existing =
|
|
188
|
+
findByIdentity(storage.accounts, identity) ||
|
|
189
|
+
storage.accounts.find((account) => account.refreshToken === credentials.refresh);
|
|
190
|
+
|
|
191
|
+
if (existing) {
|
|
192
|
+
const existingIdx = storage.accounts.indexOf(existing);
|
|
193
|
+
const existingIsCC = existing.source === "cc-keychain" || existing.source === "cc-file";
|
|
194
|
+
existing.refreshToken = credentials.refresh;
|
|
195
|
+
existing.access = credentials.access;
|
|
196
|
+
existing.expires = credentials.expires;
|
|
197
|
+
existing.token_updated_at = Date.now();
|
|
198
|
+
existing.enabled = true;
|
|
199
|
+
if (!existingIsCC) {
|
|
200
|
+
if (credentials.email) existing.email = credentials.email;
|
|
201
|
+
existing.identity = identity;
|
|
202
|
+
existing.source = existing.source ?? "oauth";
|
|
203
|
+
}
|
|
204
|
+
await saveAccounts(storage);
|
|
205
|
+
|
|
206
|
+
const label = credentials.email || existing.email || `Account ${existingIdx + 1}`;
|
|
207
|
+
log.success(`Updated existing account #${existingIdx + 1} (${label}).`);
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (storage.accounts.length >= 10) {
|
|
212
|
+
log.error("Error: maximum of 10 accounts reached. Remove one first.");
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
storage.accounts.push({
|
|
218
|
+
id: `${now}:${credentials.refresh.slice(0, 12)}`,
|
|
219
|
+
email: credentials.email,
|
|
220
|
+
identity,
|
|
221
|
+
refreshToken: credentials.refresh,
|
|
222
|
+
access: credentials.access,
|
|
223
|
+
expires: credentials.expires,
|
|
224
|
+
token_updated_at: now,
|
|
225
|
+
addedAt: now,
|
|
226
|
+
lastUsed: 0,
|
|
227
|
+
enabled: true,
|
|
228
|
+
rateLimitResetTimes: {},
|
|
229
|
+
consecutiveFailures: 0,
|
|
230
|
+
lastFailureTime: null,
|
|
231
|
+
stats: createDefaultStats(now),
|
|
232
|
+
source: "oauth",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await saveAccounts(storage);
|
|
236
|
+
|
|
237
|
+
const label = credentials.email || `Account ${storage.accounts.length}`;
|
|
238
|
+
log.success(`Added account #${storage.accounts.length} (${label}).`);
|
|
239
|
+
log.info(`${storage.accounts.length} account(s) total.`);
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Logout: revoke tokens and remove an account, or all accounts.
|
|
245
|
+
*/
|
|
246
|
+
export async function cmdLogout(arg?: string, opts: LogoutOptions = {}) {
|
|
247
|
+
if (opts.all) {
|
|
248
|
+
return cmdLogoutAll(opts);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const n = parseInt(arg || "", 10);
|
|
252
|
+
if (isNaN(n) || n < 1) {
|
|
253
|
+
log.error("Error: provide a valid account number (e.g., 'logout 2') or --all.");
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const stored = await loadAccounts();
|
|
258
|
+
if (!stored || stored.accounts.length === 0) {
|
|
259
|
+
log.error("Error: no accounts configured.");
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const idx = n - 1;
|
|
264
|
+
if (idx >= stored.accounts.length) {
|
|
265
|
+
log.error(`Error: account ${n} does not exist. You have ${stored.accounts.length} account(s).`);
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
270
|
+
|
|
271
|
+
if (!opts.force) {
|
|
272
|
+
if (!process.stdin.isTTY) {
|
|
273
|
+
log.error("Error: use --force to logout in non-interactive mode.");
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const shouldLogout = await confirm({
|
|
278
|
+
message: `Logout account #${n} (${label})? This will revoke tokens and remove the account.`,
|
|
279
|
+
});
|
|
280
|
+
if (isCancel(shouldLogout) || !shouldLogout) {
|
|
281
|
+
log.info("Cancelled.");
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const revoked = await revoke(stored.accounts[idx].refreshToken);
|
|
287
|
+
if (revoked) {
|
|
288
|
+
log.info("Token revoked server-side.");
|
|
289
|
+
} else {
|
|
290
|
+
log.info("Token revocation skipped (server may not support it).");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
stored.accounts.splice(idx, 1);
|
|
294
|
+
|
|
295
|
+
if (stored.accounts.length === 0) {
|
|
296
|
+
stored.activeIndex = 0;
|
|
297
|
+
} else if (stored.activeIndex >= stored.accounts.length) {
|
|
298
|
+
stored.activeIndex = stored.accounts.length - 1;
|
|
299
|
+
} else if (stored.activeIndex > idx) {
|
|
300
|
+
stored.activeIndex--;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await saveAccounts(stored);
|
|
304
|
+
log.success(`Logged out account #${n} (${label}).`);
|
|
305
|
+
|
|
306
|
+
if (stored.accounts.length > 0) {
|
|
307
|
+
log.info(`${stored.accounts.length} account(s) remaining.`);
|
|
308
|
+
} else {
|
|
309
|
+
log.info("No accounts remaining. Run 'login' to add one.");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function cmdLogoutAll(opts: Pick<LogoutOptions, "force"> = {}) {
|
|
316
|
+
const stored = await loadAccounts();
|
|
317
|
+
if (!stored || stored.accounts.length === 0) {
|
|
318
|
+
log.info("No accounts to logout.");
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const count = stored.accounts.length;
|
|
323
|
+
|
|
324
|
+
if (!opts.force) {
|
|
325
|
+
if (!process.stdin.isTTY) {
|
|
326
|
+
log.error("Error: use --force to logout all in non-interactive mode.");
|
|
327
|
+
return 1;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const shouldLogoutAll = await confirm({
|
|
331
|
+
message: `Logout all ${count} account(s)? This will revoke tokens and remove all accounts.`,
|
|
332
|
+
});
|
|
333
|
+
if (isCancel(shouldLogoutAll) || !shouldLogoutAll) {
|
|
334
|
+
log.info("Cancelled.");
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const results = await Promise.allSettled(stored.accounts.map((account) => revoke(account.refreshToken)));
|
|
340
|
+
const revokedCount = results.filter((result) => result.status === "fulfilled" && result.value === true).length;
|
|
341
|
+
|
|
342
|
+
if (revokedCount > 0) {
|
|
343
|
+
log.info(`Revoked ${revokedCount} of ${count} token(s) server-side.`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
await saveAccounts({ version: 1, accounts: [], activeIndex: 0 });
|
|
347
|
+
log.success(`Logged out all ${count} account(s).`);
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Reauth: re-authenticate an existing account with fresh OAuth tokens.
|
|
353
|
+
*/
|
|
354
|
+
export async function cmdReauth(arg: string) {
|
|
355
|
+
const n = parseInt(arg, 10);
|
|
356
|
+
if (isNaN(n) || n < 1) {
|
|
357
|
+
log.error("Error: provide a valid account number (e.g., 'reauth 1')");
|
|
358
|
+
return 1;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!process.stdin.isTTY) {
|
|
362
|
+
log.error("Error: 'reauth' requires an interactive terminal.");
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const stored = await loadAccounts();
|
|
367
|
+
if (!stored || stored.accounts.length === 0) {
|
|
368
|
+
log.error("Error: no accounts configured.");
|
|
369
|
+
return 1;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const idx = n - 1;
|
|
373
|
+
if (idx >= stored.accounts.length) {
|
|
374
|
+
log.error(`Error: account ${n} does not exist. You have ${stored.accounts.length} account(s).`);
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const existing = stored.accounts[idx];
|
|
379
|
+
const existingIsCC = existing.source === "cc-keychain" || existing.source === "cc-file";
|
|
380
|
+
const wasDisabled = !existing.enabled;
|
|
381
|
+
const oldLabel = existing.email || `Account ${n}`;
|
|
382
|
+
log.info(`Re-authenticating account #${n} (${oldLabel})...`);
|
|
383
|
+
|
|
384
|
+
const credentials = await runOAuthFlow();
|
|
385
|
+
if (!credentials) return 1;
|
|
386
|
+
|
|
387
|
+
existing.refreshToken = credentials.refresh;
|
|
388
|
+
existing.access = credentials.access;
|
|
389
|
+
existing.expires = credentials.expires;
|
|
390
|
+
existing.token_updated_at = Date.now();
|
|
391
|
+
existing.enabled = true;
|
|
392
|
+
existing.consecutiveFailures = 0;
|
|
393
|
+
existing.lastFailureTime = null;
|
|
394
|
+
existing.rateLimitResetTimes = {};
|
|
395
|
+
if (!existingIsCC) {
|
|
396
|
+
if (credentials.email) existing.email = credentials.email;
|
|
397
|
+
existing.identity = resolveIdentityFromOAuthExchange(credentials);
|
|
398
|
+
existing.source = existing.source ?? "oauth";
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
await saveAccounts(stored);
|
|
402
|
+
|
|
403
|
+
const newLabel = credentials.email || `Account ${n}`;
|
|
404
|
+
log.success(`Re-authenticated account #${n} (${newLabel}).`);
|
|
405
|
+
if (wasDisabled) {
|
|
406
|
+
log.info("Account has been re-enabled.");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Refresh: attempt a token refresh for an account without browser interaction.
|
|
414
|
+
*/
|
|
415
|
+
export async function cmdRefresh(arg: string) {
|
|
416
|
+
const n = parseInt(arg, 10);
|
|
417
|
+
if (isNaN(n) || n < 1) {
|
|
418
|
+
log.error("Error: provide a valid account number (e.g., 'refresh 1')");
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const stored = await loadAccounts();
|
|
423
|
+
if (!stored || stored.accounts.length === 0) {
|
|
424
|
+
log.error("Error: no accounts configured.");
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const idx = n - 1;
|
|
429
|
+
if (idx >= stored.accounts.length) {
|
|
430
|
+
log.error(`Error: account ${n} does not exist. You have ${stored.accounts.length} account(s).`);
|
|
431
|
+
return 1;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const account = stored.accounts[idx];
|
|
435
|
+
const label = account.email || `Account ${n}`;
|
|
436
|
+
|
|
437
|
+
const s = spinner();
|
|
438
|
+
s.start(`Refreshing token for account #${n} (${label})...`);
|
|
439
|
+
|
|
440
|
+
const token = await refreshAccessToken(account);
|
|
441
|
+
if (!token) {
|
|
442
|
+
s.stop(`Token refresh failed for account #${n}.`);
|
|
443
|
+
log.error("The refresh token may be invalid or expired.");
|
|
444
|
+
log.error(`Try: opencode-anthropic-auth reauth ${n}`);
|
|
445
|
+
return 1;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const wasDisabled = !account.enabled;
|
|
449
|
+
account.enabled = true;
|
|
450
|
+
account.consecutiveFailures = 0;
|
|
451
|
+
account.lastFailureTime = null;
|
|
452
|
+
account.rateLimitResetTimes = {};
|
|
453
|
+
|
|
454
|
+
await saveAccounts(stored);
|
|
455
|
+
|
|
456
|
+
const expiresIn = account.expires ? formatDuration(account.expires - Date.now()) : "unknown";
|
|
457
|
+
s.stop("Token refreshed.");
|
|
458
|
+
log.success(`Token refreshed for account #${n} (${label}).`);
|
|
459
|
+
log.info(`New token expires in ${expiresIn}.`);
|
|
460
|
+
if (wasDisabled) {
|
|
461
|
+
log.info("Account has been re-enabled.");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* List all accounts with full status table and live usage quotas.
|
|
469
|
+
*/
|
|
470
|
+
export async function cmdList() {
|
|
471
|
+
const stored = await loadAccountsWithRepair();
|
|
472
|
+
if (!stored || stored.accounts.length === 0) {
|
|
473
|
+
log.warn("No accounts configured.");
|
|
474
|
+
log.info(`Storage: ${shortPath(getStoragePath())}`);
|
|
475
|
+
log.info("Run 'opencode auth login' and select 'Claude Pro/Max' to add accounts.");
|
|
476
|
+
return 1;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const config = loadConfig();
|
|
480
|
+
const now = Date.now();
|
|
481
|
+
|
|
482
|
+
const s = spinner();
|
|
483
|
+
s.start("Fetching usage quotas...");
|
|
484
|
+
const usageResults = await Promise.allSettled(stored.accounts.map((account) => ensureTokenAndFetchUsage(account)));
|
|
485
|
+
s.stop("Usage quotas fetched.");
|
|
486
|
+
|
|
487
|
+
let anyRefreshed = false;
|
|
488
|
+
for (const result of usageResults) {
|
|
489
|
+
if (result.status === "fulfilled" && result.value.tokenRefreshed) {
|
|
490
|
+
anyRefreshed = true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (anyRefreshed) {
|
|
495
|
+
await saveAccounts(stored).catch((error) => {
|
|
496
|
+
console.error("[opencode-anthropic-auth] failed to persist refreshed tokens:", error);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
log.message(c.bold("Anthropic Multi-Account Status"));
|
|
501
|
+
log.message(
|
|
502
|
+
" " +
|
|
503
|
+
pad(c.dim("#"), 5) +
|
|
504
|
+
pad(c.dim("Account"), 22) +
|
|
505
|
+
pad(c.dim("Status"), 14) +
|
|
506
|
+
pad(c.dim("Failures"), 11) +
|
|
507
|
+
c.dim("Rate Limit"),
|
|
508
|
+
);
|
|
509
|
+
log.message(c.dim(" " + "─".repeat(62)));
|
|
510
|
+
|
|
511
|
+
for (let i = 0; i < stored.accounts.length; i++) {
|
|
512
|
+
const account = stored.accounts[i];
|
|
513
|
+
const isActive = i === stored.activeIndex;
|
|
514
|
+
const label = account.email || `Account ${i + 1}`;
|
|
515
|
+
const status = !account.enabled ? c.gray("○ disabled") : isActive ? c.green("● active") : c.cyan("● ready");
|
|
516
|
+
const failures = !account.enabled
|
|
517
|
+
? c.dim("—")
|
|
518
|
+
: account.consecutiveFailures > 0
|
|
519
|
+
? c.yellow(String(account.consecutiveFailures))
|
|
520
|
+
: c.dim("0");
|
|
521
|
+
|
|
522
|
+
let rateLimit: string;
|
|
523
|
+
if (!account.enabled) {
|
|
524
|
+
rateLimit = c.dim("—");
|
|
525
|
+
} else {
|
|
526
|
+
const maxReset = Math.max(0, ...Object.values(account.rateLimitResetTimes || {}));
|
|
527
|
+
rateLimit = maxReset > now ? c.yellow(`\u26A0 ${formatDuration(maxReset - now)}`) : c.dim("—");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
log.message(
|
|
531
|
+
" " + pad(c.bold(String(i + 1)), 5) + pad(label, 22) + pad(status, 14) + pad(failures, 11) + rateLimit,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
if (account.enabled) {
|
|
535
|
+
const result = usageResults[i];
|
|
536
|
+
const usage = result.status === "fulfilled" ? result.value.usage : null;
|
|
537
|
+
if (usage) {
|
|
538
|
+
const lines = renderUsageLines(usage);
|
|
539
|
+
for (const line of lines) {
|
|
540
|
+
log.message(line);
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
log.message(c.dim(`${USAGE_INDENT}quotas: unavailable`));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (i < stored.accounts.length - 1) {
|
|
548
|
+
log.message("");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
log.message("");
|
|
553
|
+
|
|
554
|
+
const enabled = stored.accounts.filter((account) => account.enabled).length;
|
|
555
|
+
const disabled = stored.accounts.length - enabled;
|
|
556
|
+
const parts = [
|
|
557
|
+
`Strategy: ${c.cyan(config.account_selection_strategy)}`,
|
|
558
|
+
`${c.bold(String(enabled))} of ${stored.accounts.length} enabled`,
|
|
559
|
+
];
|
|
560
|
+
if (disabled > 0) {
|
|
561
|
+
parts.push(`${c.yellow(String(disabled))} disabled`);
|
|
562
|
+
}
|
|
563
|
+
log.info(parts.join(c.dim(" | ")));
|
|
564
|
+
log.info(`Storage: ${shortPath(getStoragePath())}`);
|
|
565
|
+
|
|
566
|
+
return 0;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Show compact one-liner status.
|
|
571
|
+
*/
|
|
572
|
+
export async function cmdStatus() {
|
|
573
|
+
const stored = await loadAccountsWithRepair();
|
|
574
|
+
if (!stored || stored.accounts.length === 0) {
|
|
575
|
+
console.log("anthropic: no accounts configured");
|
|
576
|
+
return 1;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const config = loadConfig();
|
|
580
|
+
const total = stored.accounts.length;
|
|
581
|
+
const enabled = stored.accounts.filter((account) => account.enabled).length;
|
|
582
|
+
const now = Date.now();
|
|
583
|
+
|
|
584
|
+
let rateLimited = 0;
|
|
585
|
+
for (const account of stored.accounts) {
|
|
586
|
+
if (!account.enabled) continue;
|
|
587
|
+
const maxReset = Math.max(0, ...Object.values(account.rateLimitResetTimes || {}));
|
|
588
|
+
if (maxReset > now) rateLimited++;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
let line = `anthropic: ${total} account${total !== 1 ? "s" : ""} (${enabled} active)`;
|
|
592
|
+
line += `, strategy: ${config.account_selection_strategy}`;
|
|
593
|
+
line += `, next: #${stored.activeIndex + 1}`;
|
|
594
|
+
if (rateLimited > 0) {
|
|
595
|
+
line += `, ${rateLimited} rate-limited`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
console.log(line);
|
|
599
|
+
return 0;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Switch active account.
|
|
604
|
+
*/
|
|
605
|
+
export async function cmdSwitch(arg?: string) {
|
|
606
|
+
const n = parseInt(arg || "", 10);
|
|
607
|
+
if (isNaN(n) || n < 1) {
|
|
608
|
+
log.error("Error: provide a valid account number (e.g., 'switch 2')");
|
|
609
|
+
return 1;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const stored = await loadAccounts();
|
|
613
|
+
if (!stored || stored.accounts.length === 0) {
|
|
614
|
+
log.error("Error: no accounts configured.");
|
|
615
|
+
return 1;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const idx = n - 1;
|
|
619
|
+
if (idx >= stored.accounts.length) {
|
|
620
|
+
log.error(`Error: account ${n} does not exist. You have ${stored.accounts.length} account(s).`);
|
|
621
|
+
return 1;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (!stored.accounts[idx].enabled) {
|
|
625
|
+
log.error(`Warning: account ${n} is disabled. Enable it first with 'enable ${n}'.`);
|
|
626
|
+
return 1;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
stored.activeIndex = idx;
|
|
630
|
+
await saveAccounts(stored);
|
|
631
|
+
|
|
632
|
+
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
633
|
+
log.success(`Switched active account to #${n} (${label}).`);
|
|
634
|
+
return 0;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Enable a disabled account.
|
|
639
|
+
*/
|
|
640
|
+
export async function cmdEnable(arg?: string) {
|
|
641
|
+
const n = parseInt(arg || "", 10);
|
|
642
|
+
if (isNaN(n) || n < 1) {
|
|
643
|
+
log.error("Error: provide a valid account number (e.g., 'enable 3')");
|
|
644
|
+
return 1;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const stored = await loadAccounts();
|
|
648
|
+
if (!stored || stored.accounts.length === 0) {
|
|
649
|
+
log.error("Error: no accounts configured.");
|
|
650
|
+
return 1;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const idx = n - 1;
|
|
654
|
+
if (idx >= stored.accounts.length) {
|
|
655
|
+
log.error(`Error: account ${n} does not exist.`);
|
|
656
|
+
return 1;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (stored.accounts[idx].enabled) {
|
|
660
|
+
log.info(`Account ${n} is already enabled.`);
|
|
661
|
+
return 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
stored.accounts[idx].enabled = true;
|
|
665
|
+
await saveAccounts(stored);
|
|
666
|
+
|
|
667
|
+
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
668
|
+
log.success(`Enabled account #${n} (${label}).`);
|
|
669
|
+
return 0;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Disable an account.
|
|
674
|
+
*/
|
|
675
|
+
export async function cmdDisable(arg?: string) {
|
|
676
|
+
const n = parseInt(arg || "", 10);
|
|
677
|
+
if (isNaN(n) || n < 1) {
|
|
678
|
+
log.error("Error: provide a valid account number (e.g., 'disable 3')");
|
|
679
|
+
return 1;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const stored = await loadAccounts();
|
|
683
|
+
if (!stored || stored.accounts.length === 0) {
|
|
684
|
+
log.error("Error: no accounts configured.");
|
|
685
|
+
return 1;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const idx = n - 1;
|
|
689
|
+
if (idx >= stored.accounts.length) {
|
|
690
|
+
log.error(`Error: account ${n} does not exist.`);
|
|
691
|
+
return 1;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (!stored.accounts[idx].enabled) {
|
|
695
|
+
log.info(`Account ${n} is already disabled.`);
|
|
696
|
+
return 0;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const enabledCount = stored.accounts.filter((account) => account.enabled).length;
|
|
700
|
+
if (enabledCount <= 1) {
|
|
701
|
+
log.error("Error: cannot disable the last enabled account.");
|
|
702
|
+
return 1;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
stored.accounts[idx].enabled = false;
|
|
706
|
+
|
|
707
|
+
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
708
|
+
let switchedTo: number | null = null;
|
|
709
|
+
|
|
710
|
+
if (idx === stored.activeIndex) {
|
|
711
|
+
const nextEnabled = stored.accounts.findIndex((account) => account.enabled);
|
|
712
|
+
if (nextEnabled >= 0) {
|
|
713
|
+
stored.activeIndex = nextEnabled;
|
|
714
|
+
switchedTo = nextEnabled;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
await saveAccounts(stored);
|
|
719
|
+
|
|
720
|
+
log.warn(`Disabled account #${n} (${label}).`);
|
|
721
|
+
if (switchedTo !== null) {
|
|
722
|
+
const nextLabel = stored.accounts[switchedTo].email || `Account ${switchedTo + 1}`;
|
|
723
|
+
log.info(`Active account switched to #${switchedTo + 1} (${nextLabel}).`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return 0;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Reset rate-limit and failure tracking.
|
|
731
|
+
*/
|
|
732
|
+
export async function cmdReset(arg?: string) {
|
|
733
|
+
if (!arg) {
|
|
734
|
+
log.error("Error: provide an account number or 'all' (e.g., 'reset 1' or 'reset all')");
|
|
735
|
+
return 1;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const stored = await loadAccounts();
|
|
739
|
+
if (!stored || stored.accounts.length === 0) {
|
|
740
|
+
log.error("Error: no accounts configured.");
|
|
741
|
+
return 1;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (arg.toLowerCase() === "all") {
|
|
745
|
+
let count = 0;
|
|
746
|
+
for (const account of stored.accounts) {
|
|
747
|
+
account.rateLimitResetTimes = {};
|
|
748
|
+
account.consecutiveFailures = 0;
|
|
749
|
+
account.lastFailureTime = null;
|
|
750
|
+
count++;
|
|
751
|
+
}
|
|
752
|
+
await saveAccounts(stored);
|
|
753
|
+
log.success(`Reset tracking for all ${count} account(s).`);
|
|
754
|
+
return 0;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const n = parseInt(arg, 10);
|
|
758
|
+
if (isNaN(n) || n < 1) {
|
|
759
|
+
log.error("Error: provide a valid account number or 'all'.");
|
|
760
|
+
return 1;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const idx = n - 1;
|
|
764
|
+
if (idx >= stored.accounts.length) {
|
|
765
|
+
log.error(`Error: account ${n} does not exist.`);
|
|
766
|
+
return 1;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
stored.accounts[idx].rateLimitResetTimes = {};
|
|
770
|
+
stored.accounts[idx].consecutiveFailures = 0;
|
|
771
|
+
stored.accounts[idx].lastFailureTime = null;
|
|
772
|
+
await saveAccounts(stored);
|
|
773
|
+
|
|
774
|
+
const label = stored.accounts[idx].email || `Account ${n}`;
|
|
775
|
+
log.success(`Reset tracking for account #${n} (${label}).`);
|
|
776
|
+
return 0;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Show per-account usage statistics.
|
|
781
|
+
*/
|
|
782
|
+
export async function cmdStats() {
|
|
783
|
+
const stored = await loadAccounts();
|
|
784
|
+
if (!stored || stored.accounts.length === 0) {
|
|
785
|
+
log.warn("No accounts configured.");
|
|
786
|
+
return 1;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const widths = { num: 4, name: 22, val: 10 };
|
|
790
|
+
const rule = c.dim(" " + "─".repeat(74));
|
|
791
|
+
|
|
792
|
+
log.message(c.bold("Anthropic Account Usage"));
|
|
793
|
+
log.message(
|
|
794
|
+
" " +
|
|
795
|
+
pad(c.dim("#"), widths.num) +
|
|
796
|
+
pad(c.dim("Account"), widths.name) +
|
|
797
|
+
rpad(c.dim("Requests"), widths.val) +
|
|
798
|
+
rpad(c.dim("Input"), widths.val) +
|
|
799
|
+
rpad(c.dim("Output"), widths.val) +
|
|
800
|
+
rpad(c.dim("Cache R"), widths.val) +
|
|
801
|
+
rpad(c.dim("Cache W"), widths.val),
|
|
802
|
+
);
|
|
803
|
+
log.message(rule);
|
|
804
|
+
|
|
805
|
+
let totalRequests = 0;
|
|
806
|
+
let totalInput = 0;
|
|
807
|
+
let totalOutput = 0;
|
|
808
|
+
let totalCacheRead = 0;
|
|
809
|
+
let totalCacheWrite = 0;
|
|
810
|
+
let oldestReset = Infinity;
|
|
811
|
+
|
|
812
|
+
for (let i = 0; i < stored.accounts.length; i++) {
|
|
813
|
+
const account = stored.accounts[i];
|
|
814
|
+
const stats = account.stats || createDefaultStats();
|
|
815
|
+
const marker = i === stored.activeIndex ? c.green("●") : " ";
|
|
816
|
+
const number = `${marker} ${i + 1}`;
|
|
817
|
+
const name = account.email || `Account ${i + 1}`;
|
|
818
|
+
|
|
819
|
+
log.message(
|
|
820
|
+
" " +
|
|
821
|
+
pad(number, widths.num) +
|
|
822
|
+
pad(name, widths.name) +
|
|
823
|
+
rpad(String(stats.requests), widths.val) +
|
|
824
|
+
rpad(fmtTokens(stats.inputTokens), widths.val) +
|
|
825
|
+
rpad(fmtTokens(stats.outputTokens), widths.val) +
|
|
826
|
+
rpad(fmtTokens(stats.cacheReadTokens), widths.val) +
|
|
827
|
+
rpad(fmtTokens(stats.cacheWriteTokens), widths.val),
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
totalRequests += stats.requests;
|
|
831
|
+
totalInput += stats.inputTokens;
|
|
832
|
+
totalOutput += stats.outputTokens;
|
|
833
|
+
totalCacheRead += stats.cacheReadTokens;
|
|
834
|
+
totalCacheWrite += stats.cacheWriteTokens;
|
|
835
|
+
if (stats.lastReset < oldestReset) oldestReset = stats.lastReset;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (stored.accounts.length > 1) {
|
|
839
|
+
log.message(rule);
|
|
840
|
+
log.message(
|
|
841
|
+
c.bold(
|
|
842
|
+
" " +
|
|
843
|
+
pad("", widths.num) +
|
|
844
|
+
pad("Total", widths.name) +
|
|
845
|
+
rpad(String(totalRequests), widths.val) +
|
|
846
|
+
rpad(fmtTokens(totalInput), widths.val) +
|
|
847
|
+
rpad(fmtTokens(totalOutput), widths.val) +
|
|
848
|
+
rpad(fmtTokens(totalCacheRead), widths.val) +
|
|
849
|
+
rpad(fmtTokens(totalCacheWrite), widths.val),
|
|
850
|
+
),
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (oldestReset < Infinity) {
|
|
855
|
+
log.message(c.dim(`Tracking since: ${new Date(oldestReset).toLocaleString()} (${formatTimeAgo(oldestReset)})`));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return 0;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Remove an account permanently.
|
|
863
|
+
* @returns exit code
|
|
864
|
+
*/
|
|
865
|
+
export async function cmdRemove(arg?: string, opts: RemoveOptions = {}) {
|
|
866
|
+
const n = parseInt(arg || "", 10);
|
|
867
|
+
if (isNaN(n) || n < 1) {
|
|
868
|
+
log.error("Error: provide a valid account number (e.g., 'remove 2')");
|
|
869
|
+
return 1;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const stored = await loadAccounts();
|
|
873
|
+
if (!stored || stored.accounts.length === 0) {
|
|
874
|
+
log.error("Error: no accounts configured.");
|
|
875
|
+
return 1;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const idx = n - 1;
|
|
879
|
+
if (idx >= stored.accounts.length) {
|
|
880
|
+
log.error(`Error: account ${n} does not exist.`);
|
|
881
|
+
return 1;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const label = stored.accounts[idx]!.email || `Account ${n}`;
|
|
885
|
+
|
|
886
|
+
if (!opts.force) {
|
|
887
|
+
if (!process.stdin.isTTY) {
|
|
888
|
+
log.error("Error: use --force to remove accounts in non-interactive mode.");
|
|
889
|
+
return 1;
|
|
890
|
+
}
|
|
891
|
+
const shouldRemove = await confirm({
|
|
892
|
+
message: `Remove account #${n} (${label})? This cannot be undone.`,
|
|
893
|
+
});
|
|
894
|
+
if (isCancel(shouldRemove) || !shouldRemove) {
|
|
895
|
+
log.info("Cancelled.");
|
|
896
|
+
return 0;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
stored.accounts.splice(idx, 1);
|
|
901
|
+
|
|
902
|
+
if (stored.accounts.length === 0) {
|
|
903
|
+
stored.activeIndex = 0;
|
|
904
|
+
} else if (stored.activeIndex >= stored.accounts.length) {
|
|
905
|
+
stored.activeIndex = stored.accounts.length - 1;
|
|
906
|
+
} else if (stored.activeIndex > idx) {
|
|
907
|
+
stored.activeIndex--;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
await saveAccounts(stored);
|
|
911
|
+
log.success(`Removed account #${n} (${label}).`);
|
|
912
|
+
|
|
913
|
+
if (stored.accounts.length > 0) {
|
|
914
|
+
log.info(`${stored.accounts.length} account(s) remaining.`);
|
|
915
|
+
} else {
|
|
916
|
+
log.info("No accounts remaining. Run 'opencode auth login' to add one.");
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return 0;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Show help for auth and account command groups.
|
|
924
|
+
*/
|
|
925
|
+
export function cmdAuthGroupHelp(group: "auth" | "account") {
|
|
926
|
+
const bin = "opencode-anthropic-auth";
|
|
927
|
+
|
|
928
|
+
switch (group) {
|
|
929
|
+
case "auth":
|
|
930
|
+
console.log(`
|
|
931
|
+
${c.bold("Auth Commands")}
|
|
932
|
+
|
|
933
|
+
${pad(c.cyan("login"), 20)}Add a new account via browser OAuth flow (alias: ln)
|
|
934
|
+
${pad(c.cyan("logout") + " <N>", 20)}Revoke tokens and remove account N (alias: lo)
|
|
935
|
+
${pad(c.cyan("logout") + " --all", 20)}Revoke all tokens and clear all accounts
|
|
936
|
+
${pad(c.cyan("reauth") + " <N>", 20)}Re-authenticate account N with fresh tokens (alias: ra)
|
|
937
|
+
${pad(c.cyan("refresh") + " <N>", 20)}Attempt token refresh without browser (alias: rf)
|
|
938
|
+
|
|
939
|
+
${c.dim("Examples:")}
|
|
940
|
+
${bin} auth login
|
|
941
|
+
${bin} auth logout 2
|
|
942
|
+
${bin} auth reauth 1
|
|
943
|
+
`);
|
|
944
|
+
return 0;
|
|
945
|
+
case "account":
|
|
946
|
+
console.log(`
|
|
947
|
+
${c.bold("Account Commands")}
|
|
948
|
+
|
|
949
|
+
${pad(c.cyan("list"), 20)}Show all accounts with status (alias: ls)
|
|
950
|
+
${pad(c.cyan("switch") + " <N>", 20)}Set account N as active (alias: sw)
|
|
951
|
+
${pad(c.cyan("enable") + " <N>", 20)}Enable a disabled account (alias: en)
|
|
952
|
+
${pad(c.cyan("disable") + " <N>", 20)}Disable an account (alias: dis)
|
|
953
|
+
${pad(c.cyan("remove") + " <N>", 20)}Remove an account permanently (alias: rm)
|
|
954
|
+
${pad(c.cyan("reset"), 20)}Clear rate-limit / failure tracking
|
|
955
|
+
|
|
956
|
+
${c.dim("Examples:")}
|
|
957
|
+
${bin} account list
|
|
958
|
+
${bin} account switch 2
|
|
959
|
+
${bin} account disable 3
|
|
960
|
+
`);
|
|
961
|
+
return 0;
|
|
962
|
+
}
|
|
963
|
+
}
|