@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.
Files changed (107) hide show
  1. package/README.md +88 -88
  2. package/dist/opencode-anthropic-auth-cli.mjs +804 -507
  3. package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
  4. package/package.json +67 -59
  5. package/src/__tests__/billing-edge-cases.test.ts +59 -59
  6. package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
  7. package/src/__tests__/cc-comparison.test.ts +87 -87
  8. package/src/__tests__/cc-credentials.test.ts +254 -250
  9. package/src/__tests__/cch-drift-checker.test.ts +51 -51
  10. package/src/__tests__/cch-native-style.test.ts +56 -56
  11. package/src/__tests__/debug-gating.test.ts +42 -42
  12. package/src/__tests__/decomposition-smoke.test.ts +68 -68
  13. package/src/__tests__/fingerprint-regression.test.ts +575 -566
  14. package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
  15. package/src/__tests__/helpers/conversation-history.ts +119 -119
  16. package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
  17. package/src/__tests__/helpers/deferred.ts +69 -69
  18. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
  19. package/src/__tests__/helpers/in-memory-storage.ts +88 -88
  20. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
  21. package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
  22. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
  23. package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
  24. package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
  25. package/src/__tests__/helpers/sse.ts +209 -209
  26. package/src/__tests__/index.parallel.test.ts +605 -595
  27. package/src/__tests__/sanitization-regex.test.ts +112 -112
  28. package/src/__tests__/state-bounds.test.ts +90 -90
  29. package/src/account-identity.test.ts +197 -192
  30. package/src/account-identity.ts +69 -67
  31. package/src/account-state.test.ts +86 -86
  32. package/src/account-state.ts +25 -25
  33. package/src/accounts/matching.test.ts +335 -0
  34. package/src/accounts/matching.ts +167 -0
  35. package/src/accounts/persistence.test.ts +345 -0
  36. package/src/accounts/persistence.ts +432 -0
  37. package/src/accounts/repair.test.ts +276 -0
  38. package/src/accounts/repair.ts +407 -0
  39. package/src/accounts.dedup.test.ts +621 -621
  40. package/src/accounts.test.ts +933 -929
  41. package/src/accounts.ts +633 -989
  42. package/src/backoff.test.ts +345 -345
  43. package/src/backoff.ts +219 -219
  44. package/src/betas.ts +124 -124
  45. package/src/bun-fetch.test.ts +345 -342
  46. package/src/bun-fetch.ts +424 -424
  47. package/src/bun-proxy.test.ts +25 -25
  48. package/src/bun-proxy.ts +209 -209
  49. package/src/cc-credentials.ts +111 -111
  50. package/src/circuit-breaker.test.ts +184 -184
  51. package/src/circuit-breaker.ts +169 -169
  52. package/src/cli/commands/auth.ts +963 -0
  53. package/src/cli/commands/config.ts +547 -0
  54. package/src/cli/formatting.test.ts +406 -0
  55. package/src/cli/formatting.ts +219 -0
  56. package/src/cli.ts +255 -2022
  57. package/src/commands/handlers/betas.ts +100 -0
  58. package/src/commands/handlers/config.ts +99 -0
  59. package/src/commands/handlers/files.ts +375 -0
  60. package/src/commands/oauth-flow.ts +181 -166
  61. package/src/commands/prompts.ts +61 -61
  62. package/src/commands/router.test.ts +421 -0
  63. package/src/commands/router.ts +143 -635
  64. package/src/config.test.ts +482 -482
  65. package/src/config.ts +412 -404
  66. package/src/constants.ts +48 -48
  67. package/src/drift/cch-constants.ts +95 -95
  68. package/src/env.ts +111 -105
  69. package/src/headers/billing.ts +33 -33
  70. package/src/headers/builder.ts +130 -130
  71. package/src/headers/cch.ts +75 -75
  72. package/src/headers/stainless.ts +25 -25
  73. package/src/headers/user-agent.ts +23 -23
  74. package/src/index.ts +436 -828
  75. package/src/models.ts +27 -27
  76. package/src/oauth.test.ts +102 -102
  77. package/src/oauth.ts +178 -178
  78. package/src/parent-pid-watcher.test.ts +148 -148
  79. package/src/parent-pid-watcher.ts +69 -69
  80. package/src/plugin-helpers.ts +82 -82
  81. package/src/refresh-helpers.ts +145 -139
  82. package/src/refresh-lock.test.ts +94 -94
  83. package/src/refresh-lock.ts +93 -93
  84. package/src/request/body.history.test.ts +579 -571
  85. package/src/request/body.ts +255 -255
  86. package/src/request/metadata.ts +65 -65
  87. package/src/request/retry.test.ts +156 -156
  88. package/src/request/retry.ts +67 -67
  89. package/src/request/url.ts +21 -21
  90. package/src/request-orchestration-helpers.ts +648 -0
  91. package/src/response/index.ts +5 -5
  92. package/src/response/mcp.ts +58 -58
  93. package/src/response/streaming.test.ts +313 -311
  94. package/src/response/streaming.ts +412 -410
  95. package/src/rotation.test.ts +304 -301
  96. package/src/rotation.ts +205 -205
  97. package/src/storage.test.ts +547 -547
  98. package/src/storage.ts +315 -291
  99. package/src/system-prompt/builder.ts +38 -38
  100. package/src/system-prompt/index.ts +5 -5
  101. package/src/system-prompt/normalize.ts +60 -60
  102. package/src/system-prompt/sanitize.ts +30 -30
  103. package/src/thinking.ts +21 -20
  104. package/src/token-refresh.test.ts +265 -265
  105. package/src/token-refresh.ts +219 -214
  106. package/src/types.ts +30 -30
  107. package/dist/bun-proxy.mjs +0 -291
@@ -83,9 +83,113 @@ var require_src = __commonJS({
83
83
 
84
84
  // src/cli.ts
85
85
  import { AsyncLocalStorage } from "node:async_hooks";
86
- import { exec as exec2 } from "node:child_process";
87
86
  import { pathToFileURL } from "node:url";
88
87
 
88
+ // src/cli/formatting.ts
89
+ var USE_COLOR = !process.env.NO_COLOR && process.stdout.isTTY !== false;
90
+ function setUseColor(value) {
91
+ USE_COLOR = value;
92
+ }
93
+ function ansi(code, text) {
94
+ return USE_COLOR ? `\x1B[${code}m${text}\x1B[0m` : text;
95
+ }
96
+ var c = {
97
+ bold: (t2) => ansi("1", t2),
98
+ dim: (t2) => ansi("2", t2),
99
+ green: (t2) => ansi("32", t2),
100
+ yellow: (t2) => ansi("33", t2),
101
+ cyan: (t2) => ansi("36", t2),
102
+ red: (t2) => ansi("31", t2),
103
+ gray: (t2) => ansi("90", t2)
104
+ };
105
+ function stripAnsi(str) {
106
+ return str.replace(new RegExp("\x1B\\[[0-9;]*m", "g"), "");
107
+ }
108
+ function formatDuration(ms) {
109
+ if (ms <= 0) return "now";
110
+ const seconds = Math.floor(ms / 1e3);
111
+ if (seconds < 60) return `${seconds}s`;
112
+ const minutes = Math.floor(seconds / 60);
113
+ const remainSec = seconds % 60;
114
+ if (minutes < 60) return remainSec > 0 ? `${minutes}m ${remainSec}s` : `${minutes}m`;
115
+ const hours = Math.floor(minutes / 60);
116
+ const remainMin = minutes % 60;
117
+ if (hours < 24) return remainMin > 0 ? `${hours}h ${remainMin}m` : `${hours}h`;
118
+ const days = Math.floor(hours / 24);
119
+ const remainHours = hours % 24;
120
+ return remainHours > 0 ? `${days}d ${remainHours}h` : `${days}d`;
121
+ }
122
+ function formatTimeAgo(timestamp) {
123
+ if (!timestamp || timestamp === 0) return "never";
124
+ const ms = Date.now() - timestamp;
125
+ if (ms < 0) return "just now";
126
+ return `${formatDuration(ms)} ago`;
127
+ }
128
+ function formatResetTime(isoString) {
129
+ const resetMs = new Date(isoString).getTime();
130
+ const remaining = resetMs - Date.now();
131
+ if (remaining <= 0) return "now";
132
+ return formatDuration(remaining);
133
+ }
134
+ function shortPath(p2) {
135
+ const home = process.env.HOME || process.env.USERPROFILE || "";
136
+ if (home && p2.startsWith(home)) return "~" + p2.slice(home.length);
137
+ return p2;
138
+ }
139
+ function pad(str, width) {
140
+ const diff = width - stripAnsi(str).length;
141
+ return diff > 0 ? str + " ".repeat(diff) : str;
142
+ }
143
+ function rpad(str, width) {
144
+ const diff = width - stripAnsi(str).length;
145
+ return diff > 0 ? " ".repeat(diff) + str : str;
146
+ }
147
+ function renderBar(utilization, width = 10) {
148
+ const pct = Math.max(0, Math.min(100, utilization));
149
+ const filled = Math.round(pct / 100 * width);
150
+ const empty = width - filled;
151
+ let bar;
152
+ if (pct >= 90) {
153
+ bar = c.red("\u2588".repeat(filled)) + c.dim("\u2591".repeat(empty));
154
+ } else if (pct >= 70) {
155
+ bar = c.yellow("\u2588".repeat(filled)) + c.dim("\u2591".repeat(empty));
156
+ } else {
157
+ bar = c.green("\u2588".repeat(filled)) + c.dim("\u2591".repeat(empty));
158
+ }
159
+ return bar;
160
+ }
161
+ var QUOTA_BUCKETS = [
162
+ { key: "five_hour", label: "5h" },
163
+ { key: "seven_day", label: "7d" },
164
+ { key: "seven_day_sonnet", label: "Sonnet 7d" },
165
+ { key: "seven_day_opus", label: "Opus 7d" },
166
+ { key: "seven_day_oauth_apps", label: "OAuth Apps 7d" },
167
+ { key: "seven_day_cowork", label: "Cowork 7d" }
168
+ ];
169
+ var USAGE_INDENT = " ";
170
+ var USAGE_LABEL_WIDTH = 13;
171
+ function renderUsageLines(usage) {
172
+ const lines = [];
173
+ for (const { key, label } of QUOTA_BUCKETS) {
174
+ const bucket = usage[key];
175
+ if (!bucket || bucket.utilization == null) continue;
176
+ const pct = bucket.utilization;
177
+ const bar = renderBar(pct);
178
+ const pctStr = pad(String(Math.round(pct)) + "%", 4);
179
+ const reset = bucket.resets_at ? c.dim(`resets in ${formatResetTime(bucket.resets_at)}`) : "";
180
+ lines.push(`${USAGE_INDENT}${pad(label, USAGE_LABEL_WIDTH)} ${bar} ${pctStr}${reset ? ` ${reset}` : ""}`);
181
+ }
182
+ return lines;
183
+ }
184
+ function fmtTokens(n) {
185
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
186
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
187
+ return String(n);
188
+ }
189
+
190
+ // src/cli/commands/auth.ts
191
+ import { exec as exec2 } from "node:child_process";
192
+
89
193
  // src/account-identity.ts
90
194
  function isCCAccountSource(source) {
91
195
  return source === "cc-keychain" || source === "cc-file";
@@ -270,7 +374,12 @@ function validateConfig(raw) {
270
374
  config.health_score = {
271
375
  initial: clampNumber(hs.initial, 0, 100, DEFAULT_CONFIG.health_score.initial),
272
376
  success_reward: clampNumber(hs.success_reward, 0, 10, DEFAULT_CONFIG.health_score.success_reward),
273
- rate_limit_penalty: clampNumber(hs.rate_limit_penalty, -50, 0, DEFAULT_CONFIG.health_score.rate_limit_penalty),
377
+ rate_limit_penalty: clampNumber(
378
+ hs.rate_limit_penalty,
379
+ -50,
380
+ 0,
381
+ DEFAULT_CONFIG.health_score.rate_limit_penalty
382
+ ),
274
383
  failure_penalty: clampNumber(hs.failure_penalty, -100, 0, DEFAULT_CONFIG.health_score.failure_penalty),
275
384
  recovery_rate_per_hour: clampNumber(
276
385
  hs.recovery_rate_per_hour,
@@ -754,7 +863,7 @@ function mergeAccountWithFresherAuth(account, diskMatch) {
754
863
  token_updated_at: diskTokenUpdatedAt
755
864
  };
756
865
  }
757
- function unionAccountsWithDisk(storage, disk) {
866
+ function unionAccountsWithDisk(storage, disk, options = {}) {
758
867
  if (!disk || storage.accounts.length === 0) {
759
868
  return {
760
869
  ...storage,
@@ -770,7 +879,10 @@ function unionAccountsWithDisk(storage, disk) {
770
879
  }
771
880
  return mergeAccountWithFresherAuth(account, diskMatch);
772
881
  });
773
- const diskOnlyAccounts = disk.accounts.filter((account) => !matchedDiskAccounts.has(account));
882
+ const droppedIds = options.droppedIds;
883
+ const diskOnlyAccounts = disk.accounts.filter(
884
+ (account) => !matchedDiskAccounts.has(account) && !(droppedIds && droppedIds.has(account.id))
885
+ );
774
886
  const accounts = [...mergedAccounts, ...diskOnlyAccounts];
775
887
  const activeIndex = activeAccountId ? accounts.findIndex((account) => account.id === activeAccountId) : -1;
776
888
  return {
@@ -812,7 +924,7 @@ async function loadAccounts() {
812
924
  return null;
813
925
  }
814
926
  }
815
- async function saveAccounts(storage) {
927
+ async function saveAccounts(storage, options = {}) {
816
928
  const storagePath = getStoragePath();
817
929
  const configDir = dirname2(storagePath);
818
930
  await fs.mkdir(configDir, { recursive: true });
@@ -823,7 +935,7 @@ async function saveAccounts(storage) {
823
935
  };
824
936
  try {
825
937
  const disk = await loadAccounts();
826
- storageToWrite = unionAccountsWithDisk(storageToWrite, disk);
938
+ storageToWrite = unionAccountsWithDisk(storageToWrite, disk, { droppedIds: options.droppedIds });
827
939
  } catch {
828
940
  }
829
941
  const tempPath = `${storagePath}.${randomBytes2(6).toString("hex")}.tmp`;
@@ -840,6 +952,250 @@ async function saveAccounts(storage) {
840
952
  }
841
953
  }
842
954
 
955
+ // src/cc-credentials.ts
956
+ import { execSync } from "node:child_process";
957
+ import { readFileSync as readFileSync3 } from "node:fs";
958
+ import { homedir as homedir2 } from "node:os";
959
+ import { join as join3 } from "node:path";
960
+ var SECURITY_TIMEOUT_MS = 5e3;
961
+ var SECURITY_HANDLED_EXIT_CODES = /* @__PURE__ */ new Set([36, 44, 128]);
962
+ var CLAUDE_CODE_SERVICE_PATTERN = /"svce"<blob>="(Claude Code-credentials[^"]*)"/g;
963
+ function isRecord(value) {
964
+ return typeof value === "object" && value !== null && !Array.isArray(value);
965
+ }
966
+ function isValidCredentialShape(value) {
967
+ if (!isRecord(value)) return false;
968
+ if (typeof value.accessToken !== "string" || value.accessToken.length === 0) return false;
969
+ if (typeof value.refreshToken !== "string" || value.refreshToken.length === 0) return false;
970
+ if (typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt)) return false;
971
+ if (value.subscriptionType !== void 0 && typeof value.subscriptionType !== "string") return false;
972
+ return true;
973
+ }
974
+ function parseCCCredentialWithMeta(raw, meta) {
975
+ try {
976
+ const parsed = JSON.parse(raw);
977
+ if (!isRecord(parsed)) return null;
978
+ const wrapped = parsed.claudeAiOauth;
979
+ const candidate = isValidCredentialShape(wrapped) ? wrapped : isValidCredentialShape(parsed) ? parsed : parsed.mcpOAuth !== void 0 && parsed.accessToken === void 0 ? null : null;
980
+ if (!candidate) return null;
981
+ return {
982
+ accessToken: candidate.accessToken,
983
+ refreshToken: candidate.refreshToken,
984
+ expiresAt: candidate.expiresAt,
985
+ subscriptionType: candidate.subscriptionType,
986
+ source: meta.source,
987
+ label: meta.label
988
+ };
989
+ } catch {
990
+ return null;
991
+ }
992
+ }
993
+ function extractClaudeCodeServices(raw) {
994
+ const services = /* @__PURE__ */ new Set();
995
+ for (const match of raw.matchAll(CLAUDE_CODE_SERVICE_PATTERN)) {
996
+ const service = match[1]?.trim();
997
+ if (service) services.add(service);
998
+ }
999
+ return Array.from(services);
1000
+ }
1001
+ function shellQuote(value) {
1002
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1003
+ }
1004
+ function runSecurityCommand(command) {
1005
+ try {
1006
+ return execSync(command, {
1007
+ encoding: "utf-8",
1008
+ timeout: SECURITY_TIMEOUT_MS
1009
+ });
1010
+ } catch (error) {
1011
+ const securityError = error;
1012
+ if (typeof securityError.status === "number" && SECURITY_HANDLED_EXIT_CODES.has(securityError.status)) {
1013
+ return null;
1014
+ }
1015
+ if (securityError.code === "ETIMEDOUT" || securityError.signal === "SIGTERM") {
1016
+ return null;
1017
+ }
1018
+ return null;
1019
+ }
1020
+ }
1021
+ function readCCCredentialsFromKeychain() {
1022
+ if (process.platform !== "darwin") return null;
1023
+ const dumpOutput = runSecurityCommand("security dump-keychain");
1024
+ if (!dumpOutput) return null;
1025
+ const services = extractClaudeCodeServices(dumpOutput);
1026
+ if (services.length === 0) return null;
1027
+ const credentials = [];
1028
+ for (const service of services) {
1029
+ const rawCredential = runSecurityCommand(`security find-generic-password -s ${shellQuote(service)} -w`);
1030
+ if (!rawCredential) return null;
1031
+ const credential = parseCCCredentialWithMeta(rawCredential, {
1032
+ source: "cc-keychain",
1033
+ label: service
1034
+ });
1035
+ if (credential) credentials.push(credential);
1036
+ }
1037
+ return credentials.length > 0 ? credentials : null;
1038
+ }
1039
+ function readCCCredentialsFromFile() {
1040
+ const credentialsPath = join3(homedir2(), ".claude", ".credentials.json");
1041
+ try {
1042
+ const raw = readFileSync3(credentialsPath, "utf-8");
1043
+ return parseCCCredentialWithMeta(raw, {
1044
+ source: "cc-file",
1045
+ label: credentialsPath
1046
+ });
1047
+ } catch {
1048
+ return null;
1049
+ }
1050
+ }
1051
+ function readCCCredentials() {
1052
+ const credentials = [];
1053
+ if (process.platform === "darwin") {
1054
+ const keychainCredentials = readCCCredentialsFromKeychain();
1055
+ if (keychainCredentials) credentials.push(...keychainCredentials);
1056
+ }
1057
+ const fileCredential = readCCCredentialsFromFile();
1058
+ if (fileCredential) credentials.push(fileCredential);
1059
+ return credentials;
1060
+ }
1061
+
1062
+ // src/accounts/repair.ts
1063
+ var CC_ID_PATTERN = /^cc-(cc-keychain|cc-file)-\d+:/;
1064
+ function inferCCSourceFromId(id) {
1065
+ const match = CC_ID_PATTERN.exec(id);
1066
+ if (!match) return null;
1067
+ const source = match[1];
1068
+ return source === "cc-keychain" || source === "cc-file" ? source : null;
1069
+ }
1070
+ function isCCIdentity(identity) {
1071
+ return identity?.kind === "cc";
1072
+ }
1073
+ function looksCorruptedStored(account, inferredSource) {
1074
+ if (account.source !== inferredSource) return true;
1075
+ if (!isCCIdentity(account.identity)) return true;
1076
+ if (!account.label) return true;
1077
+ return false;
1078
+ }
1079
+ function findMatchingCredentialStored(account, inferredSource, ccCredentials) {
1080
+ const sameSource = ccCredentials.filter((credential) => credential.source === inferredSource);
1081
+ if (sameSource.length === 0) return null;
1082
+ const byRefresh = sameSource.find((credential) => credential.refreshToken === account.refreshToken);
1083
+ if (byRefresh) return byRefresh;
1084
+ if (account.access) {
1085
+ const byAccess = sameSource.find((credential) => credential.accessToken === account.access);
1086
+ if (byAccess) return byAccess;
1087
+ }
1088
+ if (sameSource.length === 1) return sameSource[0];
1089
+ return null;
1090
+ }
1091
+ function repairStoredAccount(account, inferredSource, ccCredentials) {
1092
+ const matched = findMatchingCredentialStored(account, inferredSource, ccCredentials);
1093
+ if (matched) {
1094
+ account.source = matched.source;
1095
+ account.label = matched.label;
1096
+ account.identity = {
1097
+ kind: "cc",
1098
+ source: matched.source,
1099
+ label: matched.label
1100
+ };
1101
+ return true;
1102
+ }
1103
+ let mutated = false;
1104
+ if (account.source !== inferredSource) {
1105
+ account.source = inferredSource;
1106
+ mutated = true;
1107
+ }
1108
+ if (account.identity?.kind === "legacy") {
1109
+ account.identity = void 0;
1110
+ mutated = true;
1111
+ }
1112
+ return mutated;
1113
+ }
1114
+ function mergeStoredDuplicate(keep, loser) {
1115
+ const keepTokenTs = keep.token_updated_at ?? 0;
1116
+ const loserTokenTs = loser.token_updated_at ?? 0;
1117
+ if (loserTokenTs > keepTokenTs) {
1118
+ keep.refreshToken = loser.refreshToken;
1119
+ keep.access = loser.access;
1120
+ keep.expires = loser.expires;
1121
+ keep.token_updated_at = loserTokenTs;
1122
+ }
1123
+ keep.lastUsed = Math.max(keep.lastUsed, loser.lastUsed);
1124
+ keep.enabled = keep.enabled || loser.enabled;
1125
+ keep.addedAt = Math.min(keep.addedAt, loser.addedAt);
1126
+ const keepIsLegacy = keep.identity?.kind === "legacy" || keep.identity === void 0;
1127
+ const loserIsRicher = loser.identity !== void 0 && loser.identity.kind !== "legacy";
1128
+ if (keepIsLegacy && loserIsRicher) {
1129
+ keep.identity = loser.identity;
1130
+ if (loser.source) keep.source = loser.source;
1131
+ if (loser.label !== void 0) keep.label = loser.label;
1132
+ if (loser.email !== void 0) keep.email = loser.email;
1133
+ }
1134
+ }
1135
+ function collapseStoredDuplicates(accounts) {
1136
+ const kept = [];
1137
+ const droppedIds = [];
1138
+ let collapsed = 0;
1139
+ for (const account of accounts) {
1140
+ const identity = resolveIdentity(account);
1141
+ if (identity.kind === "legacy") {
1142
+ kept.push(account);
1143
+ continue;
1144
+ }
1145
+ const duplicate = kept.find((existing) => identitiesMatch(resolveIdentity(existing), identity));
1146
+ if (duplicate) {
1147
+ mergeStoredDuplicate(duplicate, account);
1148
+ droppedIds.push(account.id);
1149
+ collapsed += 1;
1150
+ continue;
1151
+ }
1152
+ kept.push(account);
1153
+ }
1154
+ return { kept, collapsed, droppedIds };
1155
+ }
1156
+ function repairStoredCCAccounts(accounts, ccCredentials) {
1157
+ let repaired = 0;
1158
+ for (const account of accounts) {
1159
+ const inferredSource = inferCCSourceFromId(account.id);
1160
+ if (!inferredSource) continue;
1161
+ if (!looksCorruptedStored(account, inferredSource)) continue;
1162
+ if (repairStoredAccount(account, inferredSource, ccCredentials)) {
1163
+ repaired += 1;
1164
+ }
1165
+ }
1166
+ const { kept, collapsed, droppedIds } = collapseStoredDuplicates(accounts);
1167
+ return {
1168
+ accounts: kept,
1169
+ result: { repaired, collapsed },
1170
+ droppedIds
1171
+ };
1172
+ }
1173
+ async function loadAccountsWithRepair() {
1174
+ const stored = await loadAccounts();
1175
+ if (!stored) return stored;
1176
+ let ccCredentials;
1177
+ try {
1178
+ ccCredentials = readCCCredentials();
1179
+ } catch {
1180
+ ccCredentials = [];
1181
+ }
1182
+ const { accounts, result, droppedIds } = repairStoredCCAccounts(stored.accounts, ccCredentials);
1183
+ if (result.repaired === 0 && result.collapsed === 0) {
1184
+ return stored;
1185
+ }
1186
+ const healed = {
1187
+ ...stored,
1188
+ accounts,
1189
+ activeIndex: accounts.length === 0 ? 0 : Math.max(0, Math.min(stored.activeIndex, accounts.length - 1))
1190
+ };
1191
+ try {
1192
+ await saveAccounts(healed, { droppedIds: new Set(droppedIds) });
1193
+ } catch (err) {
1194
+ console.error("[opencode-anthropic-auth] loadAccountsWithRepair: save failed:", err.message);
1195
+ }
1196
+ return healed;
1197
+ }
1198
+
843
1199
  // node_modules/@clack/core/dist/index.mjs
844
1200
  import { styleText as y } from "node:util";
845
1201
  import { stdout as S, stdin as $ } from "node:process";
@@ -1775,54 +2131,7 @@ ${c3}
1775
2131
  }
1776
2132
  } }).prompt();
1777
2133
 
1778
- // src/cli.ts
1779
- var USE_COLOR = !process.env.NO_COLOR && process.stdout.isTTY !== false;
1780
- var ansi = (code, text) => USE_COLOR ? `\x1B[${code}m${text}\x1B[0m` : text;
1781
- var c2 = {
1782
- bold: (t2) => ansi("1", t2),
1783
- dim: (t2) => ansi("2", t2),
1784
- green: (t2) => ansi("32", t2),
1785
- yellow: (t2) => ansi("33", t2),
1786
- cyan: (t2) => ansi("36", t2),
1787
- red: (t2) => ansi("31", t2),
1788
- gray: (t2) => ansi("90", t2)
1789
- };
1790
- function formatDuration(ms) {
1791
- if (ms <= 0) return "now";
1792
- const seconds = Math.floor(ms / 1e3);
1793
- if (seconds < 60) return `${seconds}s`;
1794
- const minutes = Math.floor(seconds / 60);
1795
- const remainSec = seconds % 60;
1796
- if (minutes < 60) return remainSec > 0 ? `${minutes}m ${remainSec}s` : `${minutes}m`;
1797
- const hours = Math.floor(minutes / 60);
1798
- const remainMin = minutes % 60;
1799
- if (hours < 24) return remainMin > 0 ? `${hours}h ${remainMin}m` : `${hours}h`;
1800
- const days = Math.floor(hours / 24);
1801
- const remainHours = hours % 24;
1802
- return remainHours > 0 ? `${days}d ${remainHours}h` : `${days}d`;
1803
- }
1804
- function formatTimeAgo(timestamp) {
1805
- if (!timestamp || timestamp === 0) return "never";
1806
- const ms = Date.now() - timestamp;
1807
- if (ms < 0) return "just now";
1808
- return `${formatDuration(ms)} ago`;
1809
- }
1810
- function shortPath(p2) {
1811
- const home = process.env.HOME || process.env.USERPROFILE || "";
1812
- if (home && p2.startsWith(home)) return "~" + p2.slice(home.length);
1813
- return p2;
1814
- }
1815
- function stripAnsi(str) {
1816
- return str.replace(new RegExp("\x1B\\[[0-9;]*m", "g"), "");
1817
- }
1818
- function pad(str, width) {
1819
- const diff = width - stripAnsi(str).length;
1820
- return diff > 0 ? str + " ".repeat(diff) : str;
1821
- }
1822
- function rpad(str, width) {
1823
- const diff = width - stripAnsi(str).length;
1824
- return diff > 0 ? " ".repeat(diff) + str : str;
1825
- }
2134
+ // src/cli/commands/auth.ts
1826
2135
  async function refreshAccessToken(account) {
1827
2136
  try {
1828
2137
  const resp = await fetch("https://platform.claude.com/v1/oauth/token", {
@@ -1867,56 +2176,14 @@ async function ensureTokenAndFetchUsage(account) {
1867
2176
  let token = account.access;
1868
2177
  let tokenRefreshed = false;
1869
2178
  if (!token || !account.expires || account.expires < Date.now()) {
1870
- token = await refreshAccessToken(account);
1871
- tokenRefreshed = !!token;
1872
- if (!token) return { usage: null, tokenRefreshed: false };
2179
+ const refreshedToken = await refreshAccessToken(account);
2180
+ tokenRefreshed = !!refreshedToken;
2181
+ if (!refreshedToken) return { usage: null, tokenRefreshed: false };
2182
+ token = refreshedToken;
1873
2183
  }
1874
2184
  const usage = await fetchUsage(token);
1875
2185
  return { usage, tokenRefreshed };
1876
2186
  }
1877
- function renderBar(utilization, width = 10) {
1878
- const pct = Math.max(0, Math.min(100, utilization));
1879
- const filled = Math.round(pct / 100 * width);
1880
- const empty = width - filled;
1881
- let bar;
1882
- if (pct >= 90) {
1883
- bar = c2.red("\u2588".repeat(filled)) + c2.dim("\u2591".repeat(empty));
1884
- } else if (pct >= 70) {
1885
- bar = c2.yellow("\u2588".repeat(filled)) + c2.dim("\u2591".repeat(empty));
1886
- } else {
1887
- bar = c2.green("\u2588".repeat(filled)) + c2.dim("\u2591".repeat(empty));
1888
- }
1889
- return bar;
1890
- }
1891
- function formatResetTime(isoString) {
1892
- const resetMs = new Date(isoString).getTime();
1893
- const remaining = resetMs - Date.now();
1894
- if (remaining <= 0) return "now";
1895
- return formatDuration(remaining);
1896
- }
1897
- var QUOTA_BUCKETS = [
1898
- { key: "five_hour", label: "5h" },
1899
- { key: "seven_day", label: "7d" },
1900
- { key: "seven_day_sonnet", label: "Sonnet 7d" },
1901
- { key: "seven_day_opus", label: "Opus 7d" },
1902
- { key: "seven_day_oauth_apps", label: "OAuth Apps 7d" },
1903
- { key: "seven_day_cowork", label: "Cowork 7d" }
1904
- ];
1905
- var USAGE_INDENT = " ";
1906
- var USAGE_LABEL_WIDTH = 13;
1907
- function renderUsageLines(usage) {
1908
- const lines = [];
1909
- for (const { key, label } of QUOTA_BUCKETS) {
1910
- const bucket = usage[key];
1911
- if (!bucket || bucket.utilization == null) continue;
1912
- const pct = bucket.utilization;
1913
- const bar = renderBar(pct);
1914
- const pctStr = pad(String(Math.round(pct)) + "%", 4);
1915
- const reset = bucket.resets_at ? c2.dim(`resets in ${formatResetTime(bucket.resets_at)}`) : "";
1916
- lines.push(`${USAGE_INDENT}${pad(label, USAGE_LABEL_WIDTH)} ${bar} ${pctStr}${reset ? ` ${reset}` : ""}`);
1917
- }
1918
- return lines;
1919
- }
1920
2187
  function openBrowser(url) {
1921
2188
  if (process.platform === "win32") {
1922
2189
  exec2(`cmd /c start "" ${JSON.stringify(url)}`);
@@ -1979,17 +2246,20 @@ async function cmdLogin() {
1979
2246
  if (!credentials) return 1;
1980
2247
  const storage = stored || { version: 1, accounts: [], activeIndex: 0 };
1981
2248
  const identity = resolveIdentityFromOAuthExchange(credentials);
1982
- const existing = findByIdentity(storage.accounts, identity) || storage.accounts.find((acc) => acc.refreshToken === credentials.refresh);
2249
+ const existing = findByIdentity(storage.accounts, identity) || storage.accounts.find((account) => account.refreshToken === credentials.refresh);
1983
2250
  if (existing) {
1984
2251
  const existingIdx = storage.accounts.indexOf(existing);
2252
+ const existingIsCC = existing.source === "cc-keychain" || existing.source === "cc-file";
1985
2253
  existing.refreshToken = credentials.refresh;
1986
2254
  existing.access = credentials.access;
1987
2255
  existing.expires = credentials.expires;
1988
2256
  existing.token_updated_at = Date.now();
1989
- if (credentials.email) existing.email = credentials.email;
1990
- existing.identity = identity;
1991
- existing.source = existing.source ?? "oauth";
1992
2257
  existing.enabled = true;
2258
+ if (!existingIsCC) {
2259
+ if (credentials.email) existing.email = credentials.email;
2260
+ existing.identity = identity;
2261
+ existing.source = existing.source ?? "oauth";
2262
+ }
1993
2263
  await saveAccounts(storage);
1994
2264
  const label2 = credentials.email || existing.email || `Account ${existingIdx + 1}`;
1995
2265
  O2.success(`Updated existing account #${existingIdx + 1} (${label2}).`);
@@ -2099,8 +2369,8 @@ async function cmdLogoutAll(opts = {}) {
2099
2369
  return 0;
2100
2370
  }
2101
2371
  }
2102
- const results = await Promise.allSettled(stored.accounts.map((acc) => revoke(acc.refreshToken)));
2103
- const revokedCount = results.filter((r) => r.status === "fulfilled" && r.value === true).length;
2372
+ const results = await Promise.allSettled(stored.accounts.map((account) => revoke(account.refreshToken)));
2373
+ const revokedCount = results.filter((result) => result.status === "fulfilled" && result.value === true).length;
2104
2374
  if (revokedCount > 0) {
2105
2375
  O2.info(`Revoked ${revokedCount} of ${count} token(s) server-side.`);
2106
2376
  }
@@ -2129,6 +2399,7 @@ async function cmdReauth(arg) {
2129
2399
  return 1;
2130
2400
  }
2131
2401
  const existing = stored.accounts[idx];
2402
+ const existingIsCC = existing.source === "cc-keychain" || existing.source === "cc-file";
2132
2403
  const wasDisabled = !existing.enabled;
2133
2404
  const oldLabel = existing.email || `Account ${n}`;
2134
2405
  O2.info(`Re-authenticating account #${n} (${oldLabel})...`);
@@ -2137,11 +2408,16 @@ async function cmdReauth(arg) {
2137
2408
  existing.refreshToken = credentials.refresh;
2138
2409
  existing.access = credentials.access;
2139
2410
  existing.expires = credentials.expires;
2140
- if (credentials.email) existing.email = credentials.email;
2411
+ existing.token_updated_at = Date.now();
2141
2412
  existing.enabled = true;
2142
2413
  existing.consecutiveFailures = 0;
2143
2414
  existing.lastFailureTime = null;
2144
2415
  existing.rateLimitResetTimes = {};
2416
+ if (!existingIsCC) {
2417
+ if (credentials.email) existing.email = credentials.email;
2418
+ existing.identity = resolveIdentityFromOAuthExchange(credentials);
2419
+ existing.source = existing.source ?? "oauth";
2420
+ }
2145
2421
  await saveAccounts(stored);
2146
2422
  const newLabel = credentials.email || `Account ${n}`;
2147
2423
  O2.success(`Re-authenticated account #${n} (${newLabel}).`);
@@ -2193,7 +2469,7 @@ async function cmdRefresh(arg) {
2193
2469
  return 0;
2194
2470
  }
2195
2471
  async function cmdList() {
2196
- const stored = await loadAccounts();
2472
+ const stored = await loadAccountsWithRepair();
2197
2473
  if (!stored || stored.accounts.length === 0) {
2198
2474
  O2.warn("No accounts configured.");
2199
2475
  O2.info(`Storage: ${shortPath(getStoragePath())}`);
@@ -2204,7 +2480,7 @@ async function cmdList() {
2204
2480
  const now = Date.now();
2205
2481
  const s = fe();
2206
2482
  s.start("Fetching usage quotas...");
2207
- const usageResults = await Promise.allSettled(stored.accounts.map((acc) => ensureTokenAndFetchUsage(acc)));
2483
+ const usageResults = await Promise.allSettled(stored.accounts.map((account) => ensureTokenAndFetchUsage(account)));
2208
2484
  s.stop("Usage quotas fetched.");
2209
2485
  let anyRefreshed = false;
2210
2486
  for (const result of usageResults) {
@@ -2213,50 +2489,32 @@ async function cmdList() {
2213
2489
  }
2214
2490
  }
2215
2491
  if (anyRefreshed) {
2216
- await saveAccounts(stored).catch((err) => {
2217
- console.error("[opencode-anthropic-auth] failed to persist refreshed tokens:", err);
2492
+ await saveAccounts(stored).catch((error) => {
2493
+ console.error("[opencode-anthropic-auth] failed to persist refreshed tokens:", error);
2218
2494
  });
2219
2495
  }
2220
- O2.message(c2.bold("Anthropic Multi-Account Status"));
2496
+ O2.message(c.bold("Anthropic Multi-Account Status"));
2221
2497
  O2.message(
2222
- " " + pad(c2.dim("#"), 5) + pad(c2.dim("Account"), 22) + pad(c2.dim("Status"), 14) + pad(c2.dim("Failures"), 11) + c2.dim("Rate Limit")
2498
+ " " + pad(c.dim("#"), 5) + pad(c.dim("Account"), 22) + pad(c.dim("Status"), 14) + pad(c.dim("Failures"), 11) + c.dim("Rate Limit")
2223
2499
  );
2224
- O2.message(c2.dim(" " + "\u2500".repeat(62)));
2500
+ O2.message(c.dim(" " + "\u2500".repeat(62)));
2225
2501
  for (let i = 0; i < stored.accounts.length; i++) {
2226
- const acc = stored.accounts[i];
2502
+ const account = stored.accounts[i];
2227
2503
  const isActive = i === stored.activeIndex;
2228
- const num = String(i + 1);
2229
- const label = acc.email || `Account ${i + 1}`;
2230
- let status;
2231
- if (!acc.enabled) {
2232
- status = c2.gray("\u25CB disabled");
2233
- } else if (isActive) {
2234
- status = c2.green("\u25CF active");
2504
+ const label = account.email || `Account ${i + 1}`;
2505
+ const status = !account.enabled ? c.gray("\u25CB disabled") : isActive ? c.green("\u25CF active") : c.cyan("\u25CF ready");
2506
+ const failures = !account.enabled ? c.dim("\u2014") : account.consecutiveFailures > 0 ? c.yellow(String(account.consecutiveFailures)) : c.dim("0");
2507
+ let rateLimit;
2508
+ if (!account.enabled) {
2509
+ rateLimit = c.dim("\u2014");
2235
2510
  } else {
2236
- status = c2.cyan("\u25CF ready");
2511
+ const maxReset = Math.max(0, ...Object.values(account.rateLimitResetTimes || {}));
2512
+ rateLimit = maxReset > now ? c.yellow(`\u26A0 ${formatDuration(maxReset - now)}`) : c.dim("\u2014");
2237
2513
  }
2238
- let failures;
2239
- if (!acc.enabled) {
2240
- failures = c2.dim("\u2014");
2241
- } else if (acc.consecutiveFailures > 0) {
2242
- failures = c2.yellow(String(acc.consecutiveFailures));
2243
- } else {
2244
- failures = c2.dim("0");
2245
- }
2246
- let rateLimit;
2247
- if (!acc.enabled) {
2248
- rateLimit = c2.dim("\u2014");
2249
- } else {
2250
- const resetTimes = acc.rateLimitResetTimes || {};
2251
- const maxReset = Math.max(0, ...Object.values(resetTimes));
2252
- if (maxReset > now) {
2253
- rateLimit = c2.yellow(`\u26A0 ${formatDuration(maxReset - now)}`);
2254
- } else {
2255
- rateLimit = c2.dim("\u2014");
2256
- }
2257
- }
2258
- O2.message(" " + pad(c2.bold(num), 5) + pad(label, 22) + pad(status, 14) + pad(failures, 11) + rateLimit);
2259
- if (acc.enabled) {
2514
+ O2.message(
2515
+ " " + pad(c.bold(String(i + 1)), 5) + pad(label, 22) + pad(status, 14) + pad(failures, 11) + rateLimit
2516
+ );
2517
+ if (account.enabled) {
2260
2518
  const result = usageResults[i];
2261
2519
  const usage = result.status === "fulfilled" ? result.value.usage : null;
2262
2520
  if (usage) {
@@ -2265,7 +2523,7 @@ async function cmdList() {
2265
2523
  O2.message(line);
2266
2524
  }
2267
2525
  } else {
2268
- O2.message(c2.dim(`${USAGE_INDENT}quotas: unavailable`));
2526
+ O2.message(c.dim(`${USAGE_INDENT}quotas: unavailable`));
2269
2527
  }
2270
2528
  }
2271
2529
  if (i < stored.accounts.length - 1) {
@@ -2273,34 +2531,33 @@ async function cmdList() {
2273
2531
  }
2274
2532
  }
2275
2533
  O2.message("");
2276
- const enabled = stored.accounts.filter((a) => a.enabled).length;
2534
+ const enabled = stored.accounts.filter((account) => account.enabled).length;
2277
2535
  const disabled = stored.accounts.length - enabled;
2278
2536
  const parts = [
2279
- `Strategy: ${c2.cyan(config.account_selection_strategy)}`,
2280
- `${c2.bold(String(enabled))} of ${stored.accounts.length} enabled`
2537
+ `Strategy: ${c.cyan(config.account_selection_strategy)}`,
2538
+ `${c.bold(String(enabled))} of ${stored.accounts.length} enabled`
2281
2539
  ];
2282
2540
  if (disabled > 0) {
2283
- parts.push(`${c2.yellow(String(disabled))} disabled`);
2541
+ parts.push(`${c.yellow(String(disabled))} disabled`);
2284
2542
  }
2285
- O2.info(parts.join(c2.dim(" | ")));
2543
+ O2.info(parts.join(c.dim(" | ")));
2286
2544
  O2.info(`Storage: ${shortPath(getStoragePath())}`);
2287
2545
  return 0;
2288
2546
  }
2289
2547
  async function cmdStatus() {
2290
- const stored = await loadAccounts();
2548
+ const stored = await loadAccountsWithRepair();
2291
2549
  if (!stored || stored.accounts.length === 0) {
2292
2550
  console.log("anthropic: no accounts configured");
2293
2551
  return 1;
2294
2552
  }
2295
2553
  const config = loadConfig();
2296
2554
  const total = stored.accounts.length;
2297
- const enabled = stored.accounts.filter((a) => a.enabled).length;
2555
+ const enabled = stored.accounts.filter((account) => account.enabled).length;
2298
2556
  const now = Date.now();
2299
2557
  let rateLimited = 0;
2300
- for (const acc of stored.accounts) {
2301
- if (!acc.enabled) continue;
2302
- const resetTimes = acc.rateLimitResetTimes || {};
2303
- const maxReset = Math.max(0, ...Object.values(resetTimes));
2558
+ for (const account of stored.accounts) {
2559
+ if (!account.enabled) continue;
2560
+ const maxReset = Math.max(0, ...Object.values(account.rateLimitResetTimes || {}));
2304
2561
  if (maxReset > now) rateLimited++;
2305
2562
  }
2306
2563
  let line = `anthropic: ${total} account${total !== 1 ? "s" : ""} (${enabled} active)`;
@@ -2384,7 +2641,7 @@ async function cmdDisable(arg) {
2384
2641
  O2.info(`Account ${n} is already disabled.`);
2385
2642
  return 0;
2386
2643
  }
2387
- const enabledCount = stored.accounts.filter((a) => a.enabled).length;
2644
+ const enabledCount = stored.accounts.filter((account) => account.enabled).length;
2388
2645
  if (enabledCount <= 1) {
2389
2646
  O2.error("Error: cannot disable the last enabled account.");
2390
2647
  return 1;
@@ -2393,7 +2650,7 @@ async function cmdDisable(arg) {
2393
2650
  const label = stored.accounts[idx].email || `Account ${n}`;
2394
2651
  let switchedTo = null;
2395
2652
  if (idx === stored.activeIndex) {
2396
- const nextEnabled = stored.accounts.findIndex((a) => a.enabled);
2653
+ const nextEnabled = stored.accounts.findIndex((account) => account.enabled);
2397
2654
  if (nextEnabled >= 0) {
2398
2655
  stored.activeIndex = nextEnabled;
2399
2656
  switchedTo = nextEnabled;
@@ -2407,6 +2664,94 @@ async function cmdDisable(arg) {
2407
2664
  }
2408
2665
  return 0;
2409
2666
  }
2667
+ async function cmdReset(arg) {
2668
+ if (!arg) {
2669
+ O2.error("Error: provide an account number or 'all' (e.g., 'reset 1' or 'reset all')");
2670
+ return 1;
2671
+ }
2672
+ const stored = await loadAccounts();
2673
+ if (!stored || stored.accounts.length === 0) {
2674
+ O2.error("Error: no accounts configured.");
2675
+ return 1;
2676
+ }
2677
+ if (arg.toLowerCase() === "all") {
2678
+ let count = 0;
2679
+ for (const account of stored.accounts) {
2680
+ account.rateLimitResetTimes = {};
2681
+ account.consecutiveFailures = 0;
2682
+ account.lastFailureTime = null;
2683
+ count++;
2684
+ }
2685
+ await saveAccounts(stored);
2686
+ O2.success(`Reset tracking for all ${count} account(s).`);
2687
+ return 0;
2688
+ }
2689
+ const n = parseInt(arg, 10);
2690
+ if (isNaN(n) || n < 1) {
2691
+ O2.error("Error: provide a valid account number or 'all'.");
2692
+ return 1;
2693
+ }
2694
+ const idx = n - 1;
2695
+ if (idx >= stored.accounts.length) {
2696
+ O2.error(`Error: account ${n} does not exist.`);
2697
+ return 1;
2698
+ }
2699
+ stored.accounts[idx].rateLimitResetTimes = {};
2700
+ stored.accounts[idx].consecutiveFailures = 0;
2701
+ stored.accounts[idx].lastFailureTime = null;
2702
+ await saveAccounts(stored);
2703
+ const label = stored.accounts[idx].email || `Account ${n}`;
2704
+ O2.success(`Reset tracking for account #${n} (${label}).`);
2705
+ return 0;
2706
+ }
2707
+ async function cmdStats() {
2708
+ const stored = await loadAccounts();
2709
+ if (!stored || stored.accounts.length === 0) {
2710
+ O2.warn("No accounts configured.");
2711
+ return 1;
2712
+ }
2713
+ const widths = { num: 4, name: 22, val: 10 };
2714
+ const rule = c.dim(" " + "\u2500".repeat(74));
2715
+ O2.message(c.bold("Anthropic Account Usage"));
2716
+ O2.message(
2717
+ " " + pad(c.dim("#"), widths.num) + pad(c.dim("Account"), widths.name) + rpad(c.dim("Requests"), widths.val) + rpad(c.dim("Input"), widths.val) + rpad(c.dim("Output"), widths.val) + rpad(c.dim("Cache R"), widths.val) + rpad(c.dim("Cache W"), widths.val)
2718
+ );
2719
+ O2.message(rule);
2720
+ let totalRequests = 0;
2721
+ let totalInput = 0;
2722
+ let totalOutput = 0;
2723
+ let totalCacheRead = 0;
2724
+ let totalCacheWrite = 0;
2725
+ let oldestReset = Infinity;
2726
+ for (let i = 0; i < stored.accounts.length; i++) {
2727
+ const account = stored.accounts[i];
2728
+ const stats = account.stats || createDefaultStats();
2729
+ const marker = i === stored.activeIndex ? c.green("\u25CF") : " ";
2730
+ const number = `${marker} ${i + 1}`;
2731
+ const name = account.email || `Account ${i + 1}`;
2732
+ O2.message(
2733
+ " " + pad(number, widths.num) + pad(name, widths.name) + rpad(String(stats.requests), widths.val) + rpad(fmtTokens(stats.inputTokens), widths.val) + rpad(fmtTokens(stats.outputTokens), widths.val) + rpad(fmtTokens(stats.cacheReadTokens), widths.val) + rpad(fmtTokens(stats.cacheWriteTokens), widths.val)
2734
+ );
2735
+ totalRequests += stats.requests;
2736
+ totalInput += stats.inputTokens;
2737
+ totalOutput += stats.outputTokens;
2738
+ totalCacheRead += stats.cacheReadTokens;
2739
+ totalCacheWrite += stats.cacheWriteTokens;
2740
+ if (stats.lastReset < oldestReset) oldestReset = stats.lastReset;
2741
+ }
2742
+ if (stored.accounts.length > 1) {
2743
+ O2.message(rule);
2744
+ O2.message(
2745
+ c.bold(
2746
+ " " + pad("", widths.num) + pad("Total", widths.name) + rpad(String(totalRequests), widths.val) + rpad(fmtTokens(totalInput), widths.val) + rpad(fmtTokens(totalOutput), widths.val) + rpad(fmtTokens(totalCacheRead), widths.val) + rpad(fmtTokens(totalCacheWrite), widths.val)
2747
+ )
2748
+ );
2749
+ }
2750
+ if (oldestReset < Infinity) {
2751
+ O2.message(c.dim(`Tracking since: ${new Date(oldestReset).toLocaleString()} (${formatTimeAgo(oldestReset)})`));
2752
+ }
2753
+ return 0;
2754
+ }
2410
2755
  async function cmdRemove(arg, opts = {}) {
2411
2756
  const n = parseInt(arg || "", 10);
2412
2757
  if (isNaN(n) || n < 1) {
@@ -2454,54 +2799,126 @@ async function cmdRemove(arg, opts = {}) {
2454
2799
  }
2455
2800
  return 0;
2456
2801
  }
2457
- async function cmdReset(arg) {
2458
- if (!arg) {
2459
- O2.error("Error: provide an account number or 'all' (e.g., 'reset 1' or 'reset all')");
2460
- return 1;
2461
- }
2462
- const stored = await loadAccounts();
2463
- if (!stored || stored.accounts.length === 0) {
2464
- O2.error("Error: no accounts configured.");
2465
- return 1;
2466
- }
2467
- if (arg.toLowerCase() === "all") {
2468
- let count = 0;
2469
- for (const acc of stored.accounts) {
2470
- acc.rateLimitResetTimes = {};
2471
- acc.consecutiveFailures = 0;
2472
- acc.lastFailureTime = null;
2473
- count++;
2474
- }
2475
- await saveAccounts(stored);
2476
- O2.success(`Reset tracking for all ${count} account(s).`);
2477
- return 0;
2478
- }
2479
- const n = parseInt(arg, 10);
2480
- if (isNaN(n) || n < 1) {
2481
- O2.error("Error: provide a valid account number or 'all'.");
2482
- return 1;
2802
+ function cmdAuthGroupHelp(group) {
2803
+ const bin = "opencode-anthropic-auth";
2804
+ switch (group) {
2805
+ case "auth":
2806
+ console.log(`
2807
+ ${c.bold("Auth Commands")}
2808
+
2809
+ ${pad(c.cyan("login"), 20)}Add a new account via browser OAuth flow (alias: ln)
2810
+ ${pad(c.cyan("logout") + " <N>", 20)}Revoke tokens and remove account N (alias: lo)
2811
+ ${pad(c.cyan("logout") + " --all", 20)}Revoke all tokens and clear all accounts
2812
+ ${pad(c.cyan("reauth") + " <N>", 20)}Re-authenticate account N with fresh tokens (alias: ra)
2813
+ ${pad(c.cyan("refresh") + " <N>", 20)}Attempt token refresh without browser (alias: rf)
2814
+
2815
+ ${c.dim("Examples:")}
2816
+ ${bin} auth login
2817
+ ${bin} auth logout 2
2818
+ ${bin} auth reauth 1
2819
+ `);
2820
+ return 0;
2821
+ case "account":
2822
+ console.log(`
2823
+ ${c.bold("Account Commands")}
2824
+
2825
+ ${pad(c.cyan("list"), 20)}Show all accounts with status (alias: ls)
2826
+ ${pad(c.cyan("switch") + " <N>", 20)}Set account N as active (alias: sw)
2827
+ ${pad(c.cyan("enable") + " <N>", 20)}Enable a disabled account (alias: en)
2828
+ ${pad(c.cyan("disable") + " <N>", 20)}Disable an account (alias: dis)
2829
+ ${pad(c.cyan("remove") + " <N>", 20)}Remove an account permanently (alias: rm)
2830
+ ${pad(c.cyan("reset"), 20)}Clear rate-limit / failure tracking
2831
+
2832
+ ${c.dim("Examples:")}
2833
+ ${bin} account list
2834
+ ${bin} account switch 2
2835
+ ${bin} account disable 3
2836
+ `);
2837
+ return 0;
2483
2838
  }
2484
- const idx = n - 1;
2485
- if (idx >= stored.accounts.length) {
2486
- O2.error(`Error: account ${n} does not exist.`);
2487
- return 1;
2839
+ }
2840
+
2841
+ // src/cli/commands/config.ts
2842
+ var STRATEGY_DESCRIPTIONS = {
2843
+ sticky: "Stay on one account until it fails or is rate-limited",
2844
+ "round-robin": "Rotate through accounts on every request",
2845
+ hybrid: "Prefer healthy accounts, rotate when degraded"
2846
+ };
2847
+ function cmdGroupHelp(group) {
2848
+ const bin = "opencode-anthropic-auth";
2849
+ switch (group) {
2850
+ case "usage":
2851
+ console.log(`
2852
+ ${c.bold("Usage Commands")}
2853
+
2854
+ ${pad(c.cyan("stats"), 20)}Show per-account usage statistics
2855
+ ${pad(c.cyan("reset-stats") + " [N|all]", 20)}Reset usage statistics
2856
+ ${pad(c.cyan("status"), 20)}Compact one-liner for scripts/prompts (alias: st)
2857
+
2858
+ ${c.dim("Examples:")}
2859
+ ${bin} usage stats
2860
+ ${bin} usage status
2861
+ `);
2862
+ return 0;
2863
+ case "config":
2864
+ console.log(`
2865
+ ${c.bold("Config Commands")}
2866
+
2867
+ ${pad(c.cyan("show"), 20)}Show current configuration and file paths (alias: cfg)
2868
+ ${pad(c.cyan("strategy") + " [name]", 20)}Show or change selection strategy (alias: strat)
2869
+
2870
+ ${c.dim("Examples:")}
2871
+ ${bin} config show
2872
+ ${bin} config strategy round-robin
2873
+ `);
2874
+ return 0;
2875
+ case "manage":
2876
+ console.log(`
2877
+ ${c.bold("Manage Command")}
2878
+
2879
+ ${pad(c.cyan("manage"), 20)}Interactive account management menu (alias: mg)
2880
+
2881
+ ${c.dim("Examples:")}
2882
+ ${bin} manage
2883
+ `);
2884
+ return 0;
2488
2885
  }
2489
- stored.accounts[idx].rateLimitResetTimes = {};
2490
- stored.accounts[idx].consecutiveFailures = 0;
2491
- stored.accounts[idx].lastFailureTime = null;
2492
- await saveAccounts(stored);
2493
- const label = stored.accounts[idx].email || `Account ${n}`;
2494
- O2.success(`Reset tracking for account #${n} (${label}).`);
2495
- return 0;
2886
+ }
2887
+ function renderManageAccounts(accounts, activeIndex, currentStrategy) {
2888
+ console.log("");
2889
+ console.log(c.bold(`${accounts.length} account(s):`));
2890
+ for (let i = 0; i < accounts.length; i++) {
2891
+ const num = i + 1;
2892
+ const label = accounts[i].email || `Account ${num}`;
2893
+ const active = i === activeIndex ? c.green(" (active)") : "";
2894
+ const disabled = !accounts[i].enabled ? c.yellow(" [disabled]") : "";
2895
+ console.log(` ${c.bold(String(num))}. ${label}${active}${disabled}`);
2896
+ }
2897
+ console.log("");
2898
+ console.log(c.dim(`Strategy: ${currentStrategy}`));
2899
+ }
2900
+ function buildManageTargetOptions(accounts, activeIndex) {
2901
+ return accounts.map((account, index) => {
2902
+ const num = index + 1;
2903
+ const label = account.email || `Account ${num}`;
2904
+ const statuses = [index === activeIndex ? "active" : null, !account.enabled ? "disabled" : null].filter(
2905
+ Boolean
2906
+ );
2907
+ return {
2908
+ value: String(index),
2909
+ label: `#${num} ${label}`,
2910
+ hint: statuses.length > 0 ? statuses.join(", ") : void 0
2911
+ };
2912
+ });
2496
2913
  }
2497
2914
  async function cmdConfig() {
2498
2915
  const config = loadConfig();
2499
2916
  const stored = await loadAccounts();
2500
- O2.info(c2.bold("Anthropic Auth Configuration"));
2917
+ O2.info(c.bold("Anthropic Auth Configuration"));
2501
2918
  const generalLines = [
2502
- `Strategy: ${c2.cyan(config.account_selection_strategy)}`,
2919
+ `Strategy: ${c.cyan(config.account_selection_strategy)}`,
2503
2920
  `Failure TTL: ${config.failure_ttl_seconds}s`,
2504
- `Debug: ${config.debug ? c2.yellow("on") : "off"}`
2921
+ `Debug: ${config.debug ? c.yellow("on") : "off"}`
2505
2922
  ];
2506
2923
  wt(generalLines.join("\n"), "General");
2507
2924
  const healthLines = [
@@ -2524,10 +2941,10 @@ async function cmdConfig() {
2524
2941
  `Accounts: ${shortPath(getStoragePath())}`
2525
2942
  ];
2526
2943
  if (stored) {
2527
- const enabled = stored.accounts.filter((a) => a.enabled).length;
2944
+ const enabled = stored.accounts.filter((account) => account.enabled).length;
2528
2945
  fileLines.push(`Accounts total: ${stored.accounts.length} (${enabled} enabled)`);
2529
2946
  } else {
2530
- fileLines.push(`Accounts total: none`);
2947
+ fileLines.push("Accounts total: none");
2531
2948
  }
2532
2949
  wt(fileLines.join("\n"), "Files");
2533
2950
  const envOverrides = [];
@@ -2538,28 +2955,23 @@ async function cmdConfig() {
2538
2955
  envOverrides.push(`OPENCODE_ANTHROPIC_DEBUG=${process.env.OPENCODE_ANTHROPIC_DEBUG}`);
2539
2956
  }
2540
2957
  if (envOverrides.length > 0) {
2541
- O2.warn("Environment overrides:\n" + envOverrides.map((ov) => ` ${c2.yellow(ov)}`).join("\n"));
2958
+ O2.warn("Environment overrides:\n" + envOverrides.map((override) => ` ${c.yellow(override)}`).join("\n"));
2542
2959
  }
2543
2960
  return 0;
2544
2961
  }
2545
2962
  async function cmdStrategy(arg) {
2546
2963
  const config = loadConfig();
2547
2964
  if (!arg) {
2548
- O2.info(c2.bold("Account Selection Strategy"));
2549
- const descriptions = {
2550
- sticky: "Stay on one account until it fails or is rate-limited",
2551
- "round-robin": "Rotate through accounts on every request",
2552
- hybrid: "Prefer healthy accounts, rotate when degraded"
2553
- };
2554
- const lines = VALID_STRATEGIES.map((s) => {
2555
- const current = s === config.account_selection_strategy;
2556
- const marker = current ? c2.green("\u25B8 ") : " ";
2557
- const name = current ? c2.bold(c2.cyan(s)) : c2.dim(s);
2558
- const desc = current ? descriptions[s] : c2.dim(descriptions[s]);
2559
- return `${marker}${pad(name, 16)}${desc}`;
2965
+ O2.info(c.bold("Account Selection Strategy"));
2966
+ const lines = VALID_STRATEGIES.map((strategy) => {
2967
+ const current = strategy === config.account_selection_strategy;
2968
+ const marker = current ? c.green("\u25B8 ") : " ";
2969
+ const name = current ? c.bold(c.cyan(strategy)) : c.dim(strategy);
2970
+ const description = current ? STRATEGY_DESCRIPTIONS[strategy] : c.dim(STRATEGY_DESCRIPTIONS[strategy]);
2971
+ return `${marker}${pad(name, 16)}${description}`;
2560
2972
  });
2561
2973
  O2.message(lines.join("\n"));
2562
- O2.message(c2.dim(`Change with: opencode-anthropic-auth strategy <${VALID_STRATEGIES.join("|")}>`));
2974
+ O2.message(c.dim(`Change with: opencode-anthropic-auth strategy <${VALID_STRATEGIES.join("|")}>`));
2563
2975
  if (process.env.OPENCODE_ANTHROPIC_STRATEGY) {
2564
2976
  O2.warn(
2565
2977
  `OPENCODE_ANTHROPIC_STRATEGY=${process.env.OPENCODE_ANTHROPIC_STRATEGY} overrides config file at runtime.`
@@ -2573,64 +2985,42 @@ async function cmdStrategy(arg) {
2573
2985
  return 1;
2574
2986
  }
2575
2987
  if (normalized === config.account_selection_strategy && !process.env.OPENCODE_ANTHROPIC_STRATEGY) {
2576
- O2.message(c2.dim(`Strategy is already '${normalized}'.`));
2988
+ O2.message(c.dim(`Strategy is already '${normalized}'.`));
2577
2989
  return 0;
2578
2990
  }
2579
2991
  saveConfig({ account_selection_strategy: normalized });
2580
2992
  O2.success(`Strategy changed to '${normalized}'.`);
2581
2993
  if (process.env.OPENCODE_ANTHROPIC_STRATEGY) {
2582
- O2.warn(`OPENCODE_ANTHROPIC_STRATEGY=${process.env.OPENCODE_ANTHROPIC_STRATEGY} will override this at runtime.`);
2994
+ O2.warn(
2995
+ `OPENCODE_ANTHROPIC_STRATEGY=${process.env.OPENCODE_ANTHROPIC_STRATEGY} will override this at runtime.`
2996
+ );
2583
2997
  }
2584
2998
  return 0;
2585
2999
  }
2586
- function fmtTokens(n) {
2587
- if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
2588
- if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
2589
- return String(n);
2590
- }
2591
- async function cmdStats() {
3000
+ async function cmdStatus2() {
2592
3001
  const stored = await loadAccounts();
2593
3002
  if (!stored || stored.accounts.length === 0) {
2594
- O2.warn("No accounts configured.");
3003
+ console.log("anthropic: no accounts configured");
2595
3004
  return 1;
2596
3005
  }
2597
- const W = { num: 4, name: 22, val: 10 };
2598
- const RULE = c2.dim(" " + "\u2500".repeat(74));
2599
- O2.message(c2.bold("Anthropic Account Usage"));
2600
- O2.message(
2601
- " " + pad(c2.dim("#"), W.num) + pad(c2.dim("Account"), W.name) + rpad(c2.dim("Requests"), W.val) + rpad(c2.dim("Input"), W.val) + rpad(c2.dim("Output"), W.val) + rpad(c2.dim("Cache R"), W.val) + rpad(c2.dim("Cache W"), W.val)
2602
- );
2603
- O2.message(RULE);
2604
- let totReq = 0, totIn = 0, totOut = 0, totCR = 0, totCW = 0;
2605
- let oldestReset = Infinity;
2606
- for (let i = 0; i < stored.accounts.length; i++) {
2607
- const acc = stored.accounts[i];
2608
- const s = acc.stats || createDefaultStats();
2609
- const isActive = i === stored.activeIndex;
2610
- const marker = isActive ? c2.green("\u25CF") : " ";
2611
- const num = `${marker} ${i + 1}`;
2612
- const name = acc.email || `Account ${i + 1}`;
2613
- O2.message(
2614
- " " + pad(num, W.num) + pad(name, W.name) + rpad(String(s.requests), W.val) + rpad(fmtTokens(s.inputTokens), W.val) + rpad(fmtTokens(s.outputTokens), W.val) + rpad(fmtTokens(s.cacheReadTokens), W.val) + rpad(fmtTokens(s.cacheWriteTokens), W.val)
2615
- );
2616
- totReq += s.requests;
2617
- totIn += s.inputTokens;
2618
- totOut += s.outputTokens;
2619
- totCR += s.cacheReadTokens;
2620
- totCW += s.cacheWriteTokens;
2621
- if (s.lastReset < oldestReset) oldestReset = s.lastReset;
2622
- }
2623
- if (stored.accounts.length > 1) {
2624
- O2.message(RULE);
2625
- O2.message(
2626
- c2.bold(
2627
- " " + pad("", W.num) + pad("Total", W.name) + rpad(String(totReq), W.val) + rpad(fmtTokens(totIn), W.val) + rpad(fmtTokens(totOut), W.val) + rpad(fmtTokens(totCR), W.val) + rpad(fmtTokens(totCW), W.val)
2628
- )
2629
- );
3006
+ const config = loadConfig();
3007
+ const total = stored.accounts.length;
3008
+ const enabled = stored.accounts.filter((account) => account.enabled).length;
3009
+ const now = Date.now();
3010
+ let rateLimited = 0;
3011
+ for (const account of stored.accounts) {
3012
+ if (!account.enabled) continue;
3013
+ const resetTimes = account.rateLimitResetTimes || {};
3014
+ const maxReset = Math.max(0, ...Object.values(resetTimes));
3015
+ if (maxReset > now) rateLimited++;
2630
3016
  }
2631
- if (oldestReset < Infinity) {
2632
- O2.message(c2.dim(`Tracking since: ${new Date(oldestReset).toLocaleString()} (${formatTimeAgo(oldestReset)})`));
3017
+ let line = `anthropic: ${total} account${total !== 1 ? "s" : ""} (${enabled} active)`;
3018
+ line += `, strategy: ${config.account_selection_strategy}`;
3019
+ line += `, next: #${stored.activeIndex + 1}`;
3020
+ if (rateLimited > 0) {
3021
+ line += `, ${rateLimited} rate-limited`;
2633
3022
  }
3023
+ console.log(line);
2634
3024
  return 0;
2635
3025
  }
2636
3026
  async function cmdResetStats(arg) {
@@ -2641,8 +3031,8 @@ async function cmdResetStats(arg) {
2641
3031
  }
2642
3032
  const now = Date.now();
2643
3033
  if (!arg || arg === "all") {
2644
- for (const acc of stored.accounts) {
2645
- acc.stats = createDefaultStats(now);
3034
+ for (const account of stored.accounts) {
3035
+ account.stats = createDefaultStats(now);
2646
3036
  }
2647
3037
  await saveAccounts(stored);
2648
3038
  O2.success("Reset usage statistics for all accounts.");
@@ -2662,35 +3052,25 @@ async function cmdResetStats(arg) {
2662
3052
  async function cmdManage() {
2663
3053
  let stored = await loadAccounts();
2664
3054
  if (!stored || stored.accounts.length === 0) {
2665
- console.log(c2.yellow("No accounts configured."));
2666
- console.log(c2.dim("Run 'opencode auth login' and select 'Claude Pro/Max' to add accounts."));
3055
+ console.log(c.yellow("No accounts configured."));
3056
+ console.log(c.dim("Run 'opencode auth login' and select 'Claude Pro/Max' to add accounts."));
2667
3057
  return 1;
2668
3058
  }
2669
3059
  if (!process.stdin.isTTY) {
2670
- console.error(c2.red("Error: 'manage' requires an interactive terminal."));
2671
- console.error(c2.dim("Use 'enable', 'disable', 'remove', 'switch' for non-interactive use."));
3060
+ console.error(c.red("Error: 'manage' requires an interactive terminal."));
3061
+ console.error(c.dim("Use 'enable', 'disable', 'remove', 'switch' for non-interactive use."));
2672
3062
  return 1;
2673
3063
  }
2674
3064
  while (true) {
2675
3065
  stored = await loadAccounts();
2676
3066
  if (!stored || stored.accounts.length === 0) {
2677
- console.log(c2.dim("No accounts remaining."));
3067
+ console.log(c.dim("No accounts remaining."));
2678
3068
  break;
2679
3069
  }
2680
3070
  const accounts = stored.accounts;
2681
3071
  const activeIndex = stored.activeIndex;
2682
3072
  const currentStrategy = loadConfig().account_selection_strategy;
2683
- console.log("");
2684
- console.log(c2.bold(`${accounts.length} account(s):`));
2685
- for (let i = 0; i < accounts.length; i++) {
2686
- const num2 = i + 1;
2687
- const label = accounts[i].email || `Account ${num2}`;
2688
- const active = i === stored.activeIndex ? c2.green(" (active)") : "";
2689
- const disabled = !accounts[i].enabled ? c2.yellow(" [disabled]") : "";
2690
- console.log(` ${c2.bold(String(num2))}. ${label}${active}${disabled}`);
2691
- }
2692
- console.log("");
2693
- console.log(c2.dim(`Strategy: ${currentStrategy}`));
3073
+ renderManageAccounts(accounts, activeIndex, currentStrategy);
2694
3074
  const action = await _t({
2695
3075
  message: "Choose an action.",
2696
3076
  options: [
@@ -2716,24 +3096,13 @@ async function cmdManage() {
2716
3096
  });
2717
3097
  if (q(strategy)) break;
2718
3098
  saveConfig({ account_selection_strategy: strategy });
2719
- console.log(c2.green(`Strategy changed to '${strategy}'.`));
3099
+ console.log(c.green(`Strategy changed to '${strategy}'.`));
2720
3100
  continue;
2721
3101
  }
2722
3102
  const target = await _t({
2723
3103
  message: "Choose an account.",
2724
3104
  initialValue: String(activeIndex),
2725
- options: accounts.map((account, index) => {
2726
- const num2 = index + 1;
2727
- const label = account.email || `Account ${num2}`;
2728
- const statuses = [index === activeIndex ? "active" : null, !account.enabled ? "disabled" : null].filter(
2729
- Boolean
2730
- );
2731
- return {
2732
- value: String(index),
2733
- label: `#${num2} ${label}`,
2734
- hint: statuses.length > 0 ? statuses.join(", ") : void 0
2735
- };
2736
- })
3105
+ options: buildManageTargetOptions(accounts, activeIndex)
2737
3106
  });
2738
3107
  if (q(target)) break;
2739
3108
  const idx = Number.parseInt(target, 10);
@@ -2741,42 +3110,44 @@ async function cmdManage() {
2741
3110
  switch (action) {
2742
3111
  case "switch": {
2743
3112
  if (!accounts[idx].enabled) {
2744
- console.log(c2.yellow(`Account ${num} is disabled. Enable it first.`));
3113
+ console.log(c.yellow(`Account ${num} is disabled. Enable it first.`));
2745
3114
  break;
2746
3115
  }
2747
3116
  stored.activeIndex = idx;
2748
3117
  await saveAccounts(stored);
2749
3118
  const switchLabel = accounts[idx].email || `Account ${num}`;
2750
- console.log(c2.green(`Switched to #${num} (${switchLabel}).`));
3119
+ console.log(c.green(`Switched to #${num} (${switchLabel}).`));
2751
3120
  break;
2752
3121
  }
2753
3122
  case "enable": {
2754
3123
  if (accounts[idx].enabled) {
2755
- console.log(c2.dim(`Account ${num} is already enabled.`));
3124
+ console.log(c.dim(`Account ${num} is already enabled.`));
2756
3125
  break;
2757
3126
  }
2758
3127
  stored.accounts[idx].enabled = true;
2759
3128
  await saveAccounts(stored);
2760
- console.log(c2.green(`Enabled account #${num}.`));
3129
+ console.log(c.green(`Enabled account #${num}.`));
2761
3130
  break;
2762
3131
  }
2763
3132
  case "disable": {
2764
3133
  if (!accounts[idx].enabled) {
2765
- console.log(c2.dim(`Account ${num} is already disabled.`));
3134
+ console.log(c.dim(`Account ${num} is already disabled.`));
2766
3135
  break;
2767
3136
  }
2768
3137
  const enabledCount = accounts.filter((account) => account.enabled).length;
2769
3138
  if (enabledCount <= 1) {
2770
- console.log(c2.red("Cannot disable the last enabled account."));
3139
+ console.log(c.red("Cannot disable the last enabled account."));
2771
3140
  break;
2772
3141
  }
2773
3142
  stored.accounts[idx].enabled = false;
2774
3143
  if (idx === stored.activeIndex) {
2775
- const nextEnabled = accounts.findIndex((account, accountIndex) => accountIndex !== idx && account.enabled);
3144
+ const nextEnabled = accounts.findIndex(
3145
+ (account, accountIndex) => accountIndex !== idx && account.enabled
3146
+ );
2776
3147
  if (nextEnabled >= 0) stored.activeIndex = nextEnabled;
2777
3148
  }
2778
3149
  await saveAccounts(stored);
2779
- console.log(c2.yellow(`Disabled account #${num}.`));
3150
+ console.log(c.yellow(`Disabled account #${num}.`));
2780
3151
  break;
2781
3152
  }
2782
3153
  case "remove": {
@@ -2795,9 +3166,9 @@ async function cmdManage() {
2795
3166
  stored.activeIndex--;
2796
3167
  }
2797
3168
  await saveAccounts(stored);
2798
- console.log(c2.green(`Removed account #${num}.`));
3169
+ console.log(c.green(`Removed account #${num}.`));
2799
3170
  } else {
2800
- console.log(c2.dim("Cancelled."));
3171
+ console.log(c.dim("Cancelled."));
2801
3172
  }
2802
3173
  break;
2803
3174
  }
@@ -2806,7 +3177,7 @@ async function cmdManage() {
2806
3177
  stored.accounts[idx].consecutiveFailures = 0;
2807
3178
  stored.accounts[idx].lastFailureTime = null;
2808
3179
  await saveAccounts(stored);
2809
- console.log(c2.green(`Reset tracking for account #${num}.`));
3180
+ console.log(c.green(`Reset tracking for account #${num}.`));
2810
3181
  break;
2811
3182
  }
2812
3183
  }
@@ -2816,78 +3187,137 @@ async function cmdManage() {
2816
3187
  function cmdHelp() {
2817
3188
  const bin = "opencode-anthropic-auth";
2818
3189
  console.log(`
2819
- ${c2.bold("Anthropic Multi-Account Auth CLI")}
3190
+ ${c.bold("Anthropic Multi-Account Auth CLI")}
2820
3191
 
2821
- ${c2.dim("Usage:")}
3192
+ ${c.dim("Usage:")}
2822
3193
  ${bin} [group] [command] [args]
2823
- ${bin} [command] [args] ${c2.dim("(legacy format, still supported)")}
2824
- oaa [group] [command] [args] ${c2.dim("(short alias)")}
3194
+ ${bin} [command] [args] ${c.dim("(legacy format, still supported)")}
3195
+ oaa [group] [command] [args] ${c.dim("(short alias)")}
2825
3196
 
2826
- ${c2.dim("Command Groups:")}
2827
- ${pad(c2.cyan("auth"), 22)}Authentication: login, logout, reauth, refresh
2828
- ${pad(c2.cyan("account"), 22)}Account management: list, switch, enable, disable, remove, reset
2829
- ${pad(c2.cyan("usage"), 22)}Usage statistics: stats, reset-stats, status
2830
- ${pad(c2.cyan("config"), 22)}Configuration: show, strategy
2831
- ${pad(c2.cyan("manage"), 22)}Interactive account management menu
3197
+ ${c.dim("Command Groups:")}
3198
+ ${pad(c.cyan("auth"), 22)}Authentication: login, logout, reauth, refresh
3199
+ ${pad(c.cyan("account"), 22)}Account management: list, switch, enable, disable, remove, reset
3200
+ ${pad(c.cyan("usage"), 22)}Usage statistics: stats, reset-stats, status
3201
+ ${pad(c.cyan("config"), 22)}Configuration: show, strategy
3202
+ ${pad(c.cyan("manage"), 22)}Interactive account management menu
2832
3203
 
2833
- ${c2.dim("Auth Commands:")}
2834
- ${pad(c2.cyan("login"), 22)}Add a new account via browser OAuth (alias: ln)
2835
- ${pad(c2.cyan("logout") + " <N>", 22)}Revoke tokens and remove account N (alias: lo)
2836
- ${pad(c2.cyan("logout") + " --all", 22)}Revoke all tokens and clear all accounts
2837
- ${pad(c2.cyan("reauth") + " <N>", 22)}Re-authenticate account N (alias: ra)
2838
- ${pad(c2.cyan("refresh") + " <N>", 22)}Attempt token refresh (alias: rf)
3204
+ ${c.dim("Auth Commands:")}
3205
+ ${pad(c.cyan("login"), 22)}Add a new account via browser OAuth (alias: ln)
3206
+ ${pad(c.cyan("logout") + " <N>", 22)}Revoke tokens and remove account N (alias: lo)
3207
+ ${pad(c.cyan("logout") + " --all", 22)}Revoke all tokens and clear all accounts
3208
+ ${pad(c.cyan("reauth") + " <N>", 22)}Re-authenticate account N (alias: ra)
3209
+ ${pad(c.cyan("refresh") + " <N>", 22)}Attempt token refresh (alias: rf)
2839
3210
 
2840
- ${c2.dim("Account Commands:")}
2841
- ${pad(c2.cyan("list"), 22)}Show all accounts with status ${c2.dim("(default, alias: ls)")}
2842
- ${pad(c2.cyan("switch") + " <N>", 22)}Set account N as active (alias: sw)
2843
- ${pad(c2.cyan("enable") + " <N>", 22)}Enable a disabled account (alias: en)
2844
- ${pad(c2.cyan("disable") + " <N>", 22)}Disable an account (alias: dis)
2845
- ${pad(c2.cyan("remove") + " <N>", 22)}Remove an account permanently (alias: rm)
2846
- ${pad(c2.cyan("reset") + " <N|all>", 22)}Clear rate-limit / failure tracking
3211
+ ${c.dim("Account Commands:")}
3212
+ ${pad(c.cyan("list"), 22)}Show all accounts with status ${c.dim("(default, alias: ls)")}
3213
+ ${pad(c.cyan("switch") + " <N>", 22)}Set account N as active (alias: sw)
3214
+ ${pad(c.cyan("enable") + " <N>", 22)}Enable a disabled account (alias: en)
3215
+ ${pad(c.cyan("disable") + " <N>", 22)}Disable an account (alias: dis)
3216
+ ${pad(c.cyan("remove") + " <N>", 22)}Remove an account permanently (alias: rm)
3217
+ ${pad(c.cyan("reset") + " <N|all>", 22)}Clear rate-limit / failure tracking
2847
3218
 
2848
- ${c2.dim("Usage Commands:")}
2849
- ${pad(c2.cyan("stats"), 22)}Show per-account usage statistics
2850
- ${pad(c2.cyan("reset-stats") + " [N|all]", 22)}Reset usage statistics
2851
- ${pad(c2.cyan("status"), 22)}Compact one-liner for scripts/prompts (alias: st)
3219
+ ${c.dim("Usage Commands:")}
3220
+ ${pad(c.cyan("stats"), 22)}Show per-account usage statistics
3221
+ ${pad(c.cyan("reset-stats") + " [N|all]", 22)}Reset usage statistics
3222
+ ${pad(c.cyan("status"), 22)}Compact one-liner for scripts/prompts (alias: st)
2852
3223
 
2853
- ${c2.dim("Config Commands:")}
2854
- ${pad(c2.cyan("config"), 22)}Show configuration and file paths (alias: cfg)
2855
- ${pad(c2.cyan("strategy") + " [name]", 22)}Show or change selection strategy (alias: strat)
3224
+ ${c.dim("Config Commands:")}
3225
+ ${pad(c.cyan("config"), 22)}Show configuration and file paths (alias: cfg)
3226
+ ${pad(c.cyan("strategy") + " [name]", 22)}Show or change selection strategy (alias: strat)
2856
3227
 
2857
- ${c2.dim("Manage Commands:")}
2858
- ${pad(c2.cyan("manage"), 22)}Interactive account management menu (alias: mg)
2859
- ${pad(c2.cyan("help"), 22)}Show this help message
3228
+ ${c.dim("Manage Commands:")}
3229
+ ${pad(c.cyan("manage"), 22)}Interactive account management menu (alias: mg)
3230
+ ${pad(c.cyan("help"), 22)}Show this help message
2860
3231
 
2861
- ${c2.dim("Group Help:")}
2862
- ${bin} auth help ${c2.dim("# Show auth commands")}
2863
- ${bin} account help ${c2.dim("# Show account commands")}
2864
- ${bin} usage help ${c2.dim("# Show usage commands")}
2865
- ${bin} config help ${c2.dim("# Show config commands")}
3232
+ ${c.dim("Group Help:")}
3233
+ ${bin} auth help ${c.dim("# Show auth commands")}
3234
+ ${bin} account help ${c.dim("# Show account commands")}
3235
+ ${bin} usage help ${c.dim("# Show usage commands")}
3236
+ ${bin} config help ${c.dim("# Show config commands")}
2866
3237
 
2867
- ${c2.dim("Options:")}
3238
+ ${c.dim("Options:")}
2868
3239
  --force Skip confirmation prompts
2869
3240
  --all Target all accounts (for logout)
2870
3241
  --no-color Disable colored output
2871
3242
 
2872
- ${c2.dim("Examples:")}
2873
- ${bin} login ${c2.dim("# Add a new account via browser")}
2874
- ${bin} auth login ${c2.dim("# Same as above (group format)")}
2875
- oaa login ${c2.dim("# Same as above (short alias)")}
2876
- ${bin} logout 2 ${c2.dim("# Revoke tokens & remove account 2")}
2877
- ${bin} auth logout 2 ${c2.dim("# Same as above (group format)")}
2878
- ${bin} list ${c2.dim("# Show all accounts (default)")}
2879
- ${bin} account list ${c2.dim("# Same as above (group format)")}
2880
- ${bin} switch 2 ${c2.dim("# Make account 2 active")}
2881
- ${bin} account switch 2 ${c2.dim("# Same as above (group format)")}
2882
- ${bin} stats ${c2.dim("# Show token usage per account")}
2883
- ${bin} usage stats ${c2.dim("# Same as above (group format)")}
3243
+ ${c.dim("Examples:")}
3244
+ ${bin} login ${c.dim("# Add a new account via browser")}
3245
+ ${bin} auth login ${c.dim("# Same as above (group format)")}
3246
+ oaa login ${c.dim("# Same as above (short alias)")}
3247
+ ${bin} logout 2 ${c.dim("# Revoke tokens & remove account 2")}
3248
+ ${bin} auth logout 2 ${c.dim("# Same as above (group format)")}
3249
+ ${bin} list ${c.dim("# Show all accounts (default)")}
3250
+ ${bin} account list ${c.dim("# Same as above (group format)")}
3251
+ ${bin} switch 2 ${c.dim("# Make account 2 active")}
3252
+ ${bin} account switch 2 ${c.dim("# Same as above (group format)")}
3253
+ ${bin} stats ${c.dim("# Show token usage per account")}
3254
+ ${bin} usage stats ${c.dim("# Same as above (group format)")}
2884
3255
 
2885
- ${c2.dim("Files:")}
3256
+ ${c.dim("Files:")}
2886
3257
  Config: ${shortPath(getConfigPath())}
2887
3258
  Accounts: ${shortPath(getStoragePath())}
2888
3259
  `);
2889
3260
  return 0;
2890
3261
  }
3262
+ async function dispatchUsageCommands(args) {
3263
+ const subcommand = args[0] || "stats";
3264
+ const arg = args[1];
3265
+ switch (subcommand) {
3266
+ case "stats":
3267
+ return cmdStats();
3268
+ case "reset-stats":
3269
+ return cmdResetStats(arg);
3270
+ case "status":
3271
+ case "st":
3272
+ return cmdStatus2();
3273
+ case "help":
3274
+ case "-h":
3275
+ case "--help":
3276
+ return cmdGroupHelp("usage");
3277
+ default:
3278
+ console.error(c.red(`Unknown usage command: ${subcommand}`));
3279
+ console.error(c.dim("Run 'opencode-anthropic-auth usage help' for usage."));
3280
+ return 1;
3281
+ }
3282
+ }
3283
+ async function dispatchConfigCommands(args) {
3284
+ const subcommand = args[0] || "show";
3285
+ const arg = args[1];
3286
+ switch (subcommand) {
3287
+ case "show":
3288
+ case "cfg":
3289
+ return cmdConfig();
3290
+ case "strategy":
3291
+ case "strat":
3292
+ return cmdStrategy(arg);
3293
+ case "help":
3294
+ case "-h":
3295
+ case "--help":
3296
+ return cmdGroupHelp("config");
3297
+ default:
3298
+ console.error(c.red(`Unknown config command: ${subcommand}`));
3299
+ console.error(c.dim("Run 'opencode-anthropic-auth config help' for usage."));
3300
+ return 1;
3301
+ }
3302
+ }
3303
+ async function dispatchManageCommands(args) {
3304
+ const subcommand = args[0] || "manage";
3305
+ switch (subcommand) {
3306
+ case "manage":
3307
+ case "mg":
3308
+ return cmdManage();
3309
+ case "help":
3310
+ case "-h":
3311
+ case "--help":
3312
+ return cmdGroupHelp("manage");
3313
+ default:
3314
+ console.error(c.red(`Unknown manage command: ${subcommand}`));
3315
+ console.error(c.dim("Run 'opencode-anthropic-auth manage help' for usage."));
3316
+ return 1;
3317
+ }
3318
+ }
3319
+
3320
+ // src/cli.ts
2891
3321
  var ioContext = new AsyncLocalStorage();
2892
3322
  var nativeConsoleLog = console.log.bind(console);
2893
3323
  var nativeConsoleError = console.error.bind(console);
@@ -2922,82 +3352,6 @@ async function runWithIoContext(io, fn) {
2922
3352
  uninstallConsoleRouter();
2923
3353
  }
2924
3354
  }
2925
- function cmdGroupHelp(group) {
2926
- const bin = "opencode-anthropic-auth";
2927
- switch (group) {
2928
- case "auth":
2929
- console.log(`
2930
- ${c2.bold("Auth Commands")}
2931
-
2932
- ${pad(c2.cyan("login"), 20)}Add a new account via browser OAuth flow (alias: ln)
2933
- ${pad(c2.cyan("logout") + " <N>", 20)}Revoke tokens and remove account N (alias: lo)
2934
- ${pad(c2.cyan("logout") + " --all", 20)}Revoke all tokens and clear all accounts
2935
- ${pad(c2.cyan("reauth") + " <N>", 20)}Re-authenticate account N with fresh tokens (alias: ra)
2936
- ${pad(c2.cyan("refresh") + " <N>", 20)}Attempt token refresh without browser (alias: rf)
2937
-
2938
- ${c2.dim("Examples:")}
2939
- ${bin} auth login
2940
- ${bin} auth logout 2
2941
- ${bin} auth reauth 1
2942
- `);
2943
- return 0;
2944
- case "account":
2945
- console.log(`
2946
- ${c2.bold("Account Commands")}
2947
-
2948
- ${pad(c2.cyan("list"), 20)}Show all accounts with status (alias: ls)
2949
- ${pad(c2.cyan("switch") + " <N>", 20)}Set account N as active (alias: sw)
2950
- ${pad(c2.cyan("enable") + " <N>", 20)}Enable a disabled account (alias: en)
2951
- ${pad(c2.cyan("disable") + " <N>", 20)}Disable an account (alias: dis)
2952
- ${pad(c2.cyan("remove") + " <N>", 20)}Remove an account permanently (alias: rm)
2953
- ${pad(c2.cyan("reset"), 20)}Clear rate-limit / failure tracking
2954
-
2955
- ${c2.dim("Examples:")}
2956
- ${bin} account list
2957
- ${bin} account switch 2
2958
- ${bin} account disable 3
2959
- `);
2960
- return 0;
2961
- case "usage":
2962
- console.log(`
2963
- ${c2.bold("Usage Commands")}
2964
-
2965
- ${pad(c2.cyan("stats"), 20)}Show per-account usage statistics
2966
- ${pad(c2.cyan("reset-stats") + " [N|all]", 20)}Reset usage statistics
2967
- ${pad(c2.cyan("status"), 20)}Compact one-liner for scripts/prompts (alias: st)
2968
-
2969
- ${c2.dim("Examples:")}
2970
- ${bin} usage stats
2971
- ${bin} usage status
2972
- `);
2973
- return 0;
2974
- case "config":
2975
- console.log(`
2976
- ${c2.bold("Config Commands")}
2977
-
2978
- ${pad(c2.cyan("show"), 20)}Show current configuration and file paths (alias: cfg)
2979
- ${pad(c2.cyan("strategy") + " [name]", 20)}Show or change selection strategy (alias: strat)
2980
-
2981
- ${c2.dim("Examples:")}
2982
- ${bin} config show
2983
- ${bin} config strategy round-robin
2984
- `);
2985
- return 0;
2986
- case "manage":
2987
- console.log(`
2988
- ${c2.bold("Manage Command")}
2989
-
2990
- ${pad(c2.cyan("manage"), 20)}Interactive account management menu (alias: mg)
2991
-
2992
- ${c2.dim("Examples:")}
2993
- ${bin} manage
2994
- `);
2995
- return 0;
2996
- default:
2997
- console.error(c2.red(`Unknown command group: ${group}`));
2998
- return 1;
2999
- }
3000
- }
3001
3355
  async function dispatchAuth(args, flags) {
3002
3356
  const subcommand = args[0] || "help";
3003
3357
  const arg = args[1];
@@ -3017,10 +3371,10 @@ async function dispatchAuth(args, flags) {
3017
3371
  case "help":
3018
3372
  case "-h":
3019
3373
  case "--help":
3020
- return cmdGroupHelp("auth");
3374
+ return cmdAuthGroupHelp("auth");
3021
3375
  default:
3022
- console.error(c2.red(`Unknown auth command: ${subcommand}`));
3023
- console.error(c2.dim("Run 'opencode-anthropic-auth auth help' for usage."));
3376
+ console.error(c.red(`Unknown auth command: ${subcommand}`));
3377
+ console.error(c.dim("Run 'opencode-anthropic-auth auth help' for usage."));
3024
3378
  return 1;
3025
3379
  }
3026
3380
  }
@@ -3048,93 +3402,36 @@ async function dispatchAccount(args, flags) {
3048
3402
  case "help":
3049
3403
  case "-h":
3050
3404
  case "--help":
3051
- return cmdGroupHelp("account");
3052
- default:
3053
- console.error(c2.red(`Unknown account command: ${subcommand}`));
3054
- console.error(c2.dim("Run 'opencode-anthropic-auth account help' for usage."));
3055
- return 1;
3056
- }
3057
- }
3058
- async function dispatchUsage(args) {
3059
- const subcommand = args[0] || "stats";
3060
- const arg = args[1];
3061
- switch (subcommand) {
3062
- case "stats":
3063
- return cmdStats();
3064
- case "reset-stats":
3065
- return cmdResetStats(arg);
3066
- case "status":
3067
- case "st":
3068
- return cmdStatus();
3069
- case "help":
3070
- case "-h":
3071
- case "--help":
3072
- return cmdGroupHelp("usage");
3073
- default:
3074
- console.error(c2.red(`Unknown usage command: ${subcommand}`));
3075
- console.error(c2.dim("Run 'opencode-anthropic-auth usage help' for usage."));
3076
- return 1;
3077
- }
3078
- }
3079
- async function dispatchConfig(args) {
3080
- const subcommand = args[0] || "show";
3081
- const arg = args[1];
3082
- switch (subcommand) {
3083
- case "show":
3084
- case "cfg":
3085
- return cmdConfig();
3086
- case "strategy":
3087
- case "strat":
3088
- return cmdStrategy(arg);
3089
- case "help":
3090
- case "-h":
3091
- case "--help":
3092
- return cmdGroupHelp("config");
3093
- default:
3094
- console.error(c2.red(`Unknown config command: ${subcommand}`));
3095
- console.error(c2.dim("Run 'opencode-anthropic-auth config help' for usage."));
3096
- return 1;
3097
- }
3098
- }
3099
- async function dispatchManage(args) {
3100
- const subcommand = args[0] || "help";
3101
- switch (subcommand) {
3102
- case "manage":
3103
- case "mg":
3104
- return cmdManage();
3105
- case "help":
3106
- case "-h":
3107
- case "--help":
3108
- return cmdGroupHelp("manage");
3405
+ return cmdAuthGroupHelp("account");
3109
3406
  default:
3110
- console.error(c2.red(`Unknown manage command: ${subcommand}`));
3111
- console.error(c2.dim("Run 'opencode-anthropic-auth manage help' for usage."));
3407
+ console.error(c.red(`Unknown account command: ${subcommand}`));
3408
+ console.error(c.dim("Run 'opencode-anthropic-auth account help' for usage."));
3112
3409
  return 1;
3113
3410
  }
3114
3411
  }
3115
3412
  async function dispatch(argv) {
3116
3413
  const args = argv.filter((a) => !a.startsWith("--"));
3117
3414
  const flags = argv.filter((a) => a.startsWith("--"));
3118
- if (flags.includes("--no-color")) USE_COLOR = false;
3415
+ if (flags.includes("--no-color")) setUseColor(false);
3119
3416
  if (flags.includes("--help")) return cmdHelp();
3120
3417
  const command = args[0] || "list";
3121
3418
  const remainingArgs = args.slice(1);
3122
3419
  const force = flags.includes("--force");
3123
3420
  const all = flags.includes("--all");
3124
3421
  switch (command) {
3125
- // Group dispatchers
3422
+ // ── Group dispatchers ──────────────────────────────────────────────
3126
3423
  case "auth":
3127
3424
  return dispatchAuth(remainingArgs, { force, all });
3128
3425
  case "account":
3129
3426
  return dispatchAccount(remainingArgs, { force });
3130
3427
  case "usage":
3131
- return dispatchUsage(remainingArgs);
3428
+ return dispatchUsageCommands(remainingArgs);
3132
3429
  case "config":
3133
- return dispatchConfig(remainingArgs);
3430
+ return dispatchConfigCommands(remainingArgs);
3134
3431
  case "manage":
3135
- return dispatchManage(remainingArgs);
3136
- // Legacy backward compatibility: direct commands (map to groups)
3137
- // Auth commands
3432
+ return dispatchManageCommands(remainingArgs);
3433
+ // ── Legacy flat aliases (backward compat) ──────────────────────────
3434
+ // Auth
3138
3435
  case "login":
3139
3436
  case "ln":
3140
3437
  return cmdLogin();
@@ -3147,7 +3444,7 @@ async function dispatch(argv) {
3147
3444
  case "refresh":
3148
3445
  case "rf":
3149
3446
  return cmdRefresh(remainingArgs[0]);
3150
- // Account commands
3447
+ // Account
3151
3448
  case "list":
3152
3449
  case "ls":
3153
3450
  return cmdList();
@@ -3165,7 +3462,7 @@ async function dispatch(argv) {
3165
3462
  return cmdRemove(remainingArgs[0], { force });
3166
3463
  case "reset":
3167
3464
  return cmdReset(remainingArgs[0]);
3168
- // Usage commands
3465
+ // Usage
3169
3466
  case "stats":
3170
3467
  return cmdStats();
3171
3468
  case "reset-stats":
@@ -3173,13 +3470,13 @@ async function dispatch(argv) {
3173
3470
  case "status":
3174
3471
  case "st":
3175
3472
  return cmdStatus();
3176
- // Config commands (strategy only - config/cfg handled by group dispatcher)
3473
+ // Config
3177
3474
  case "strategy":
3178
3475
  case "strat":
3179
3476
  return cmdStrategy(remainingArgs[0]);
3180
3477
  case "cfg":
3181
- return dispatchConfig(["show"]);
3182
- // Manage commands
3478
+ return dispatchConfigCommands(["show"]);
3479
+ // Manage
3183
3480
  case "mg":
3184
3481
  return cmdManage();
3185
3482
  // Help
@@ -3188,8 +3485,8 @@ async function dispatch(argv) {
3188
3485
  case "--help":
3189
3486
  return cmdHelp();
3190
3487
  default:
3191
- console.error(c2.red(`Unknown command: ${command}`));
3192
- console.error(c2.dim("Run 'opencode-anthropic-auth help' for usage."));
3488
+ console.error(c.red(`Unknown command: ${command}`));
3489
+ console.error(c.dim("Run 'opencode-anthropic-auth help' for usage."));
3193
3490
  return 1;
3194
3491
  }
3195
3492
  }
@@ -3212,7 +3509,7 @@ async function detectMain() {
3212
3509
  }
3213
3510
  if (await detectMain()) {
3214
3511
  main(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
3215
- console.error(c2.red(`Fatal: ${err.message}`));
3512
+ console.error(c.red(`Fatal: ${err.message}`));
3216
3513
  process.exit(1);
3217
3514
  });
3218
3515
  }