opencode-anthropic-multi-account 0.2.3 → 0.2.5

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/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // src/index.ts
2
- import { AnthropicAuthPlugin as AnthropicAuthPlugin2 } from "opencode-anthropic-auth";
3
2
  import { tool } from "@opencode-ai/plugin";
4
3
 
5
4
  // ../multi-account-core/src/account-manager.ts
@@ -85,6 +84,23 @@ var PluginConfigSchema = v.object({
85
84
  quiet_mode: v.optional(v.boolean(), false),
86
85
  debug: v.optional(v.boolean(), false)
87
86
  });
87
+ var TokenRefreshError = class _TokenRefreshError extends Error {
88
+ status;
89
+ permanent;
90
+ constructor(permanent, status) {
91
+ super(status === void 0 ? "Token refresh failed" : `Token refresh failed: ${status}`);
92
+ this.name = "TokenRefreshError";
93
+ this.status = status;
94
+ this.permanent = permanent;
95
+ Object.setPrototypeOf(this, _TokenRefreshError.prototype);
96
+ }
97
+ };
98
+ function isTokenRefreshError(error) {
99
+ if (error instanceof TokenRefreshError) return true;
100
+ if (!(error instanceof Error)) return false;
101
+ const candidate = error;
102
+ return candidate.name === "TokenRefreshError" && typeof candidate.permanent === "boolean" && (candidate.status === void 0 || typeof candidate.status === "number");
103
+ }
88
104
 
89
105
  // ../multi-account-core/src/config.ts
90
106
  var DEFAULT_CONFIG_FILENAME = "multiauth-config.json";
@@ -225,6 +241,14 @@ function createMinimalClient() {
225
241
  }
226
242
  };
227
243
  }
244
+ function getClearedOAuthBody() {
245
+ return {
246
+ type: "oauth",
247
+ refresh: "",
248
+ access: "",
249
+ expires: 0
250
+ };
251
+ }
228
252
 
229
253
  // ../multi-account-core/src/claims.ts
230
254
  var CLAIMS_FILENAME = "multiauth-claims.json";
@@ -648,13 +672,7 @@ function createAccountManagerForProvider(dependencies) {
648
672
  });
649
673
  }
650
674
  async markRevoked(uuid) {
651
- await this.store.mutateAccount(uuid, (account) => {
652
- account.isAuthDisabled = true;
653
- account.authDisabledReason = "OAuth token revoked (403)";
654
- account.accessToken = void 0;
655
- account.expiresAt = void 0;
656
- });
657
- this.runtimeFactory?.invalidate(uuid);
675
+ await this.removeAccountByUuid(uuid);
658
676
  }
659
677
  async markSuccess(uuid) {
660
678
  this.last429Map.delete(uuid);
@@ -677,15 +695,32 @@ function createAccountManagerForProvider(dependencies) {
677
695
  }).catch(() => {
678
696
  });
679
697
  }
698
+ async clearOpenCodeAuthIfNoAccountsRemain() {
699
+ if (!this.client) return;
700
+ const storage = await this.store.load();
701
+ if (storage.accounts.length > 0) return;
702
+ await this.client.auth.set({
703
+ path: { id: providerAuthId },
704
+ body: getClearedOAuthBody()
705
+ }).catch(() => {
706
+ });
707
+ }
708
+ async removeAccountByUuid(uuid) {
709
+ const removed = await this.store.removeAccount(uuid);
710
+ if (!removed) return;
711
+ this.last429Map.delete(uuid);
712
+ this.runtimeFactory?.invalidate(uuid);
713
+ await this.refresh();
714
+ await this.clearOpenCodeAuthIfNoAccountsRemain();
715
+ }
680
716
  async markAuthFailure(uuid, result) {
717
+ if (!result.ok && result.permanent) {
718
+ await this.removeAccountByUuid(uuid);
719
+ return;
720
+ }
681
721
  await this.store.mutateStorage((storage) => {
682
722
  const account = storage.accounts.find((entry) => entry.uuid === uuid);
683
723
  if (!account) return;
684
- if (!result.ok && result.permanent) {
685
- account.isAuthDisabled = true;
686
- account.authDisabledReason = "Token permanently rejected (400/401/403)";
687
- return;
688
- }
689
724
  account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
690
725
  const maxFailures = getConfig().max_consecutive_auth_failures;
691
726
  const usableCount = storage.accounts.filter(
@@ -782,11 +817,21 @@ function createAccountManagerForProvider(dependencies) {
782
817
  this.cached = [];
783
818
  this.activeAccountUuid = void 0;
784
819
  }
785
- async addAccount(auth) {
820
+ async addAccount(auth, email) {
786
821
  if (!auth.refresh) return;
787
- const existing = this.cached.find((account) => account.refreshToken === auth.refresh);
788
- if (existing) return;
822
+ const existingByToken = this.cached.find((account) => account.refreshToken === auth.refresh);
823
+ if (existingByToken) return;
824
+ if (email) {
825
+ const existingByEmail = this.cached.find(
826
+ (account) => account.email && account.email === email
827
+ );
828
+ if (existingByEmail?.uuid) {
829
+ await this.replaceAccountCredentials(existingByEmail.uuid, auth);
830
+ return;
831
+ }
832
+ }
789
833
  const newAccount = this.createNewAccount(auth, Date.now());
834
+ if (email) newAccount.email = email;
790
835
  await this.store.addAccount(newAccount);
791
836
  this.activeAccountUuid = newAccount.uuid;
792
837
  await this.store.setActiveUuid(newAccount.uuid);
@@ -1112,7 +1157,6 @@ var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
1112
1157
  var MAX_RESOLVE_ATTEMPTS = 10;
1113
1158
  var SERVER_RETRY_BASE_MS = 1e3;
1114
1159
  var SERVER_RETRY_MAX_MS = 4e3;
1115
- var PERMANENT_AUTH_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
1116
1160
  function isAbortError(error) {
1117
1161
  return error instanceof Error && error.name === "AbortError";
1118
1162
  }
@@ -1126,80 +1170,49 @@ function createExecutorForProvider(providerName, dependencies) {
1126
1170
  } = dependencies;
1127
1171
  async function executeWithAccountRotation2(manager, runtimeFactory, client, input, init) {
1128
1172
  const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
1129
- let retries = 0;
1130
1173
  let previousAccountUuid;
1131
- while (true) {
1132
- if (++retries > maxRetries) {
1133
- throw new Error(
1134
- `Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
1135
- );
1136
- }
1137
- await manager.refresh();
1138
- const account = await resolveAccount(manager, client);
1139
- const accountUuid = account.uuid;
1140
- if (!accountUuid) continue;
1141
- if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
1142
- void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
1143
- }
1144
- previousAccountUuid = accountUuid;
1145
- let runtime;
1146
- let response;
1147
- try {
1148
- runtime = await runtimeFactory.getRuntime(accountUuid);
1149
- response = await runtime.fetch(input, init);
1150
- } catch (error) {
1151
- if (isAbortError(error)) throw error;
1152
- if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1153
- continue;
1174
+ async function retryServerErrors(account, runtime) {
1175
+ for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
1176
+ const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
1177
+ const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
1178
+ await sleep2(jitteredBackoff);
1179
+ let retryResponse;
1180
+ try {
1181
+ retryResponse = await runtime.fetch(input, init);
1182
+ } catch (error) {
1183
+ if (isAbortError(error)) throw error;
1184
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1185
+ return null;
1186
+ }
1187
+ void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1188
+ return null;
1154
1189
  }
1155
- void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1156
- continue;
1190
+ if (retryResponse.status < 500) return retryResponse;
1157
1191
  }
1192
+ return null;
1193
+ }
1194
+ const dispatchResponseStatus = async (account, accountUuid, runtime, response, allow401Retry, from401RefreshRetry) => {
1158
1195
  if (response.status >= 500) {
1159
- let serverResponse = response;
1160
- let networkErrorDuringServerRetry = false;
1161
- let authFailureDuringServerRetry = false;
1162
- for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
1163
- const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
1164
- const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
1165
- await sleep2(jitteredBackoff);
1196
+ const recovered = await retryServerErrors(account, runtime);
1197
+ if (recovered === null) {
1198
+ return { type: "retryOuter" };
1199
+ }
1200
+ response = recovered;
1201
+ }
1202
+ if (response.status === 401) {
1203
+ if (allow401Retry) {
1204
+ runtimeFactory.invalidate(accountUuid);
1166
1205
  try {
1167
- serverResponse = await runtime.fetch(input, init);
1206
+ const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
1207
+ const retryResponse = await retryRuntime.fetch(input, init);
1208
+ return dispatchResponseStatus(account, accountUuid, retryRuntime, retryResponse, false, true);
1168
1209
  } catch (error) {
1169
1210
  if (isAbortError(error)) throw error;
1170
1211
  if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1171
- authFailureDuringServerRetry = true;
1172
- break;
1212
+ return { type: "retryOuter" };
1173
1213
  }
1174
- networkErrorDuringServerRetry = true;
1175
- void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1176
- break;
1177
- }
1178
- if (serverResponse.status < 500) break;
1179
- }
1180
- if (authFailureDuringServerRetry) {
1181
- continue;
1182
- }
1183
- if (networkErrorDuringServerRetry || serverResponse.status >= 500) {
1184
- continue;
1185
- }
1186
- response = serverResponse;
1187
- }
1188
- if (response.status === 401) {
1189
- runtimeFactory.invalidate(accountUuid);
1190
- try {
1191
- const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
1192
- const retryResponse = await retryRuntime.fetch(input, init);
1193
- if (retryResponse.status !== 401) {
1194
- await manager.markSuccess(accountUuid);
1195
- return retryResponse;
1196
- }
1197
- } catch (error) {
1198
- if (isAbortError(error)) throw error;
1199
- if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1200
- continue;
1214
+ return { type: "retryOuter" };
1201
1215
  }
1202
- continue;
1203
1216
  }
1204
1217
  await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
1205
1218
  await manager.refresh();
@@ -1210,7 +1223,7 @@ function createExecutorForProvider(providerName, dependencies) {
1210
1223
  );
1211
1224
  }
1212
1225
  void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1213
- continue;
1226
+ return { type: "retryOuter" };
1214
1227
  }
1215
1228
  if (response.status === 403) {
1216
1229
  const revoked = await isRevokedTokenResponse(response);
@@ -1227,26 +1240,62 @@ function createExecutorForProvider(providerName, dependencies) {
1227
1240
  `All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
1228
1241
  );
1229
1242
  }
1230
- continue;
1243
+ return { type: "retryOuter" };
1244
+ }
1245
+ if (from401RefreshRetry) {
1246
+ return { type: "handled", response };
1231
1247
  }
1232
1248
  }
1233
1249
  if (response.status === 429) {
1234
1250
  await handleRateLimitResponse2(manager, client, account, response);
1251
+ return { type: "handled" };
1252
+ }
1253
+ return { type: "success", response };
1254
+ };
1255
+ for (let retries = 1; retries <= maxRetries; retries++) {
1256
+ await manager.refresh();
1257
+ const account = await resolveAccount(manager, client);
1258
+ const accountUuid = account.uuid;
1259
+ if (!accountUuid) continue;
1260
+ if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
1261
+ void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
1262
+ }
1263
+ previousAccountUuid = accountUuid;
1264
+ let runtime;
1265
+ let response;
1266
+ try {
1267
+ runtime = await runtimeFactory.getRuntime(accountUuid);
1268
+ response = await runtime.fetch(input, init);
1269
+ } catch (error) {
1270
+ if (isAbortError(error)) throw error;
1271
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1272
+ continue;
1273
+ }
1274
+ void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1275
+ continue;
1276
+ }
1277
+ const transition = await dispatchResponseStatus(account, accountUuid, runtime, response, true, false);
1278
+ if (transition.type === "retryOuter" || transition.type === "handled") {
1279
+ if (transition.type === "handled" && transition.response) {
1280
+ return transition.response;
1281
+ }
1235
1282
  continue;
1236
1283
  }
1237
1284
  await manager.markSuccess(accountUuid);
1238
- return response;
1285
+ return transition.response;
1239
1286
  }
1287
+ throw new Error(
1288
+ `Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
1289
+ );
1240
1290
  }
1241
1291
  async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
1242
- const refreshFailureStatus = getRefreshFailureStatus(error);
1243
- if (refreshFailureStatus === void 0) return false;
1292
+ if (!isTokenRefreshError(error)) return false;
1244
1293
  if (!account.uuid) return false;
1245
1294
  const accountUuid = account.uuid;
1246
1295
  runtimeFactory.invalidate(accountUuid);
1247
1296
  await manager.markAuthFailure(accountUuid, {
1248
1297
  ok: false,
1249
- permanent: PERMANENT_AUTH_FAILURE_STATUSES.has(refreshFailureStatus)
1298
+ permanent: error.permanent
1250
1299
  });
1251
1300
  await manager.refresh();
1252
1301
  if (!manager.hasAnyUsableAccount()) {
@@ -1291,13 +1340,6 @@ function createExecutorForProvider(providerName, dependencies) {
1291
1340
  executeWithAccountRotation: executeWithAccountRotation2
1292
1341
  };
1293
1342
  }
1294
- function getRefreshFailureStatus(error) {
1295
- if (!(error instanceof Error)) return void 0;
1296
- const matched = error.message.match(/Token refresh failed:\s*(\d{3})/);
1297
- if (!matched) return void 0;
1298
- const status = Number(matched[1]);
1299
- return Number.isFinite(status) ? status : void 0;
1300
- }
1301
1343
  async function isRevokedTokenResponse(response) {
1302
1344
  try {
1303
1345
  const cloned = response.clone();
@@ -1312,6 +1354,7 @@ async function isRevokedTokenResponse(response) {
1312
1354
  var INITIAL_DELAY_MS = 5e3;
1313
1355
  function createProactiveRefreshQueueForProvider(dependencies) {
1314
1356
  const {
1357
+ providerAuthId,
1315
1358
  getConfig: getConfig2,
1316
1359
  refreshToken: refreshToken2,
1317
1360
  isTokenExpired: isTokenExpired2,
@@ -1330,6 +1373,10 @@ function createProactiveRefreshQueueForProvider(dependencies) {
1330
1373
  const config = getConfig2();
1331
1374
  if (!config.proactive_refresh) return;
1332
1375
  this.runToken++;
1376
+ if (this.timeoutHandle) {
1377
+ clearTimeout(this.timeoutHandle);
1378
+ this.timeoutHandle = null;
1379
+ }
1333
1380
  this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
1334
1381
  debugLog2(this.client, "Proactive refresh started", {
1335
1382
  intervalSeconds: config.proactive_refresh_interval_seconds,
@@ -1404,23 +1451,41 @@ function createProactiveRefreshQueueForProvider(dependencies) {
1404
1451
  }
1405
1452
  async persistFailure(account, permanent) {
1406
1453
  try {
1407
- await this.store.mutateAccount(account.uuid, (target) => {
1408
- if (permanent) {
1454
+ const accountUuid = account.uuid;
1455
+ if (!accountUuid) return;
1456
+ if (permanent) {
1457
+ const removed = await this.store.removeAccount(accountUuid);
1458
+ if (!removed) return;
1459
+ this.onInvalidate?.(accountUuid);
1460
+ await this.clearOpenCodeAuthIfNoAccountsRemain();
1461
+ return;
1462
+ }
1463
+ await this.store.mutateStorage((storage) => {
1464
+ const target = storage.accounts.find((entry) => entry.uuid === accountUuid);
1465
+ if (!target) return;
1466
+ target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1467
+ const maxFailures = getConfig2().max_consecutive_auth_failures;
1468
+ const usableCount = storage.accounts.filter(
1469
+ (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== accountUuid
1470
+ ).length;
1471
+ if (target.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
1409
1472
  target.isAuthDisabled = true;
1410
- target.authDisabledReason = "Token permanently rejected (proactive refresh)";
1411
- } else {
1412
- target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1413
- const maxFailures = getConfig2().max_consecutive_auth_failures;
1414
- if (target.consecutiveAuthFailures >= maxFailures) {
1415
- target.isAuthDisabled = true;
1416
- target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1417
- }
1473
+ target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1418
1474
  }
1419
1475
  });
1420
1476
  } catch {
1421
1477
  debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
1422
1478
  }
1423
1479
  }
1480
+ async clearOpenCodeAuthIfNoAccountsRemain() {
1481
+ const storage = await this.store.load();
1482
+ if (storage.accounts.length > 0) return;
1483
+ await this.client.auth.set({
1484
+ path: { id: providerAuthId },
1485
+ body: getClearedOAuthBody()
1486
+ }).catch(() => {
1487
+ });
1488
+ }
1424
1489
  };
1425
1490
  }
1426
1491
 
@@ -1769,6 +1834,8 @@ var anthropicOAuthAdapter = {
1769
1834
  oauthBetaHeader: "oauth-2025-04-20",
1770
1835
  requestBetaHeader: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
1771
1836
  cliUserAgent: "claude-cli/2.1.2 (external, cli)",
1837
+ cliVersion: "2.1.80",
1838
+ billingSalt: "59cf53e54c78",
1772
1839
  toolPrefix: "mcp_",
1773
1840
  accountStorageFilename: "anthropic-multi-account-accounts.json",
1774
1841
  transform: {
@@ -1785,6 +1852,336 @@ var anthropicOAuthAdapter = {
1785
1852
  supported: true
1786
1853
  };
1787
1854
 
1855
+ // ../multi-account-core/src/pool-types.ts
1856
+ import * as v5 from "valibot";
1857
+ var PoolConfigSchema = v5.object({
1858
+ name: v5.string(),
1859
+ baseProvider: v5.string(),
1860
+ members: v5.array(v5.string()),
1861
+ enabled: v5.boolean()
1862
+ });
1863
+ var ChainEntryConfigSchema = v5.object({
1864
+ pool: v5.string(),
1865
+ model: v5.optional(v5.string()),
1866
+ enabled: v5.boolean()
1867
+ });
1868
+ var ChainConfigSchema = v5.object({
1869
+ name: v5.string(),
1870
+ entries: v5.array(ChainEntryConfigSchema),
1871
+ enabled: v5.boolean()
1872
+ });
1873
+ var PoolChainConfigSchema = v5.object({
1874
+ pools: v5.optional(v5.array(PoolConfigSchema), []),
1875
+ chains: v5.optional(v5.array(ChainConfigSchema), [])
1876
+ });
1877
+
1878
+ // ../multi-account-core/src/pool-config-store.ts
1879
+ import { promises as fs6 } from "node:fs";
1880
+ import { dirname as dirname5, join as join7 } from "node:path";
1881
+ import lockfile2 from "proper-lockfile";
1882
+ import * as v6 from "valibot";
1883
+ var POOL_CONFIG_FILENAME = "multiauth-pools.json";
1884
+ function createEmptyConfig() {
1885
+ return { pools: [], chains: [] };
1886
+ }
1887
+ function getGlobalConfigPath() {
1888
+ return join7(getConfigDir2(), POOL_CONFIG_FILENAME);
1889
+ }
1890
+ async function resolveConfigPath() {
1891
+ const projectPath = join7(process.cwd(), ".opencode", POOL_CONFIG_FILENAME);
1892
+ try {
1893
+ await fs6.access(projectPath);
1894
+ return projectPath;
1895
+ } catch {
1896
+ }
1897
+ return getGlobalConfigPath();
1898
+ }
1899
+ function parsePoolChainConfig(content) {
1900
+ let parsed;
1901
+ try {
1902
+ parsed = JSON.parse(content);
1903
+ } catch {
1904
+ return null;
1905
+ }
1906
+ const validation = v6.safeParse(PoolChainConfigSchema, parsed);
1907
+ return validation.success ? validation.output : null;
1908
+ }
1909
+ async function loadPoolChainConfig() {
1910
+ const path = await resolveConfigPath();
1911
+ try {
1912
+ const content = await fs6.readFile(path, "utf-8");
1913
+ return parsePoolChainConfig(content) ?? createEmptyConfig();
1914
+ } catch {
1915
+ return createEmptyConfig();
1916
+ }
1917
+ }
1918
+
1919
+ // ../multi-account-core/src/pool-manager.ts
1920
+ var DEFAULT_EXHAUSTED_COOLDOWN_MS = 5 * 60 * 1e3;
1921
+ var PoolManager = class {
1922
+ poolsByName = /* @__PURE__ */ new Map();
1923
+ exhaustedUntilByAccount = /* @__PURE__ */ new Map();
1924
+ exhaustedCooldownMs;
1925
+ constructor(options) {
1926
+ this.exhaustedCooldownMs = options?.exhaustedCooldownMs ?? DEFAULT_EXHAUSTED_COOLDOWN_MS;
1927
+ }
1928
+ loadPools(configs) {
1929
+ this.poolsByName.clear();
1930
+ for (const pool of configs) {
1931
+ this.poolsByName.set(pool.name, pool);
1932
+ }
1933
+ }
1934
+ getPoolForAccount(accountUuid) {
1935
+ for (const pool of this.poolsByName.values()) {
1936
+ if (!pool.enabled) continue;
1937
+ if (pool.members.includes(accountUuid)) return pool;
1938
+ }
1939
+ return null;
1940
+ }
1941
+ getAvailableMembers(pool, accountManager) {
1942
+ if (!pool.enabled) return [];
1943
+ this.clearExpiredExhausted();
1944
+ const accountsByUuid = /* @__PURE__ */ new Map();
1945
+ for (const account of accountManager.getAccounts()) {
1946
+ if (!account.uuid) continue;
1947
+ accountsByUuid.set(account.uuid, account);
1948
+ }
1949
+ return pool.members.filter((accountUuid) => {
1950
+ const account = accountsByUuid.get(accountUuid);
1951
+ if (!account) return false;
1952
+ if (!account.enabled || account.isAuthDisabled) return false;
1953
+ if (this.isExhausted(accountUuid)) return false;
1954
+ if (accountManager.isRateLimited(account)) return false;
1955
+ return true;
1956
+ });
1957
+ }
1958
+ markExhausted(accountUuid) {
1959
+ this.exhaustedUntilByAccount.set(accountUuid, Date.now() + this.exhaustedCooldownMs);
1960
+ }
1961
+ async getNextMember(pool, currentUuid, accountManager) {
1962
+ const availableMembers = this.getAvailableMembers(pool, accountManager);
1963
+ if (availableMembers.length === 0) return null;
1964
+ const excluded = /* @__PURE__ */ new Set();
1965
+ if (currentUuid) excluded.add(currentUuid);
1966
+ const preferred = await this.selectPreferredMember(availableMembers, excluded, accountManager);
1967
+ if (preferred) return preferred;
1968
+ for (const candidate of availableMembers) {
1969
+ if (candidate !== currentUuid) return candidate;
1970
+ }
1971
+ return null;
1972
+ }
1973
+ async buildFailoverPlan(currentAccount, config, accountManager, options) {
1974
+ this.loadPools(config.pools ?? []);
1975
+ if ((config.pools?.length ?? 0) === 0 && (config.chains?.length ?? 0) === 0) {
1976
+ return { candidates: [], skips: [] };
1977
+ }
1978
+ const attemptedAccounts = options?.attemptedAccounts ?? /* @__PURE__ */ new Set();
1979
+ const visitedChainIndexes = options?.visitedChainIndexes ?? /* @__PURE__ */ new Set();
1980
+ const currentUuid = currentAccount?.uuid;
1981
+ const candidates = [];
1982
+ const skips = [];
1983
+ const addedCandidateUuids = /* @__PURE__ */ new Set();
1984
+ const appendPoolCandidates = async (poolName, source, chainIndex) => {
1985
+ const pool = this.poolsByName.get(poolName);
1986
+ if (!pool || !pool.enabled) {
1987
+ skips.push({
1988
+ type: "chain_disabled",
1989
+ poolName,
1990
+ reason: "Pool is missing or disabled"
1991
+ });
1992
+ return;
1993
+ }
1994
+ const available = this.getAvailableMembers(pool, accountManager);
1995
+ if (available.length === 0) {
1996
+ skips.push({
1997
+ type: "pool_exhausted",
1998
+ poolName,
1999
+ reason: "No available members"
2000
+ });
2001
+ return;
2002
+ }
2003
+ const poolExclusions = /* @__PURE__ */ new Set();
2004
+ if (currentUuid) poolExclusions.add(currentUuid);
2005
+ while (poolExclusions.size < available.length + (currentUuid ? 1 : 0)) {
2006
+ const nextMember = await this.selectPreferredMember(available, poolExclusions, accountManager);
2007
+ if (!nextMember) break;
2008
+ poolExclusions.add(nextMember);
2009
+ if (attemptedAccounts.has(nextMember)) {
2010
+ skips.push({
2011
+ type: "account_attempted",
2012
+ poolName,
2013
+ reason: "Already attempted in this cascade",
2014
+ detail: nextMember
2015
+ });
2016
+ continue;
2017
+ }
2018
+ if (addedCandidateUuids.has(nextMember)) continue;
2019
+ candidates.push({
2020
+ poolName,
2021
+ accountUuid: nextMember,
2022
+ source,
2023
+ chainIndex
2024
+ });
2025
+ addedCandidateUuids.add(nextMember);
2026
+ }
2027
+ for (const memberUuid of available) {
2028
+ if (poolExclusions.has(memberUuid)) continue;
2029
+ if (attemptedAccounts.has(memberUuid)) {
2030
+ skips.push({
2031
+ type: "account_attempted",
2032
+ poolName,
2033
+ reason: "Already attempted in this cascade",
2034
+ detail: memberUuid
2035
+ });
2036
+ continue;
2037
+ }
2038
+ if (addedCandidateUuids.has(memberUuid)) continue;
2039
+ candidates.push({
2040
+ poolName,
2041
+ accountUuid: memberUuid,
2042
+ source,
2043
+ chainIndex
2044
+ });
2045
+ addedCandidateUuids.add(memberUuid);
2046
+ }
2047
+ };
2048
+ if (currentUuid) {
2049
+ const currentPool = this.getPoolForAccount(currentUuid);
2050
+ if (currentPool) {
2051
+ await appendPoolCandidates(currentPool.name, "pool");
2052
+ }
2053
+ }
2054
+ let flattenedChainIndex = 0;
2055
+ for (const chain of config.chains ?? []) {
2056
+ if (!chain.enabled) {
2057
+ for (let i = 0; i < chain.entries.length; i++) {
2058
+ skips.push({
2059
+ type: "chain_disabled",
2060
+ poolName: chain.entries[i]?.pool ?? chain.name,
2061
+ reason: `Chain '${chain.name}' is disabled`
2062
+ });
2063
+ flattenedChainIndex += 1;
2064
+ }
2065
+ continue;
2066
+ }
2067
+ for (const entry of chain.entries) {
2068
+ if (visitedChainIndexes.has(flattenedChainIndex)) {
2069
+ skips.push({
2070
+ type: "chain_disabled",
2071
+ poolName: entry.pool,
2072
+ reason: "Chain entry already visited in this cascade",
2073
+ detail: `${flattenedChainIndex}`
2074
+ });
2075
+ flattenedChainIndex += 1;
2076
+ continue;
2077
+ }
2078
+ if (!entry.enabled) {
2079
+ skips.push({
2080
+ type: "chain_disabled",
2081
+ poolName: entry.pool,
2082
+ reason: "Chain entry is disabled",
2083
+ detail: `${flattenedChainIndex}`
2084
+ });
2085
+ flattenedChainIndex += 1;
2086
+ continue;
2087
+ }
2088
+ await appendPoolCandidates(entry.pool, "chain", flattenedChainIndex);
2089
+ flattenedChainIndex += 1;
2090
+ }
2091
+ }
2092
+ return { candidates, skips };
2093
+ }
2094
+ isExhausted(accountUuid) {
2095
+ const exhaustedUntil = this.exhaustedUntilByAccount.get(accountUuid);
2096
+ if (!exhaustedUntil) return false;
2097
+ if (Date.now() >= exhaustedUntil) {
2098
+ this.exhaustedUntilByAccount.delete(accountUuid);
2099
+ return false;
2100
+ }
2101
+ return true;
2102
+ }
2103
+ clearExpiredExhausted() {
2104
+ const now = Date.now();
2105
+ for (const [accountUuid, exhaustedUntil] of this.exhaustedUntilByAccount.entries()) {
2106
+ if (now >= exhaustedUntil) this.exhaustedUntilByAccount.delete(accountUuid);
2107
+ }
2108
+ }
2109
+ async selectPreferredMember(availableMembers, excludedMembers, accountManager) {
2110
+ const availableSet = new Set(availableMembers);
2111
+ const maxAttempts = Math.max(availableMembers.length * 2, 6);
2112
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
2113
+ const selected = await accountManager.selectAccount();
2114
+ if (!selected?.uuid) continue;
2115
+ if (!availableSet.has(selected.uuid)) continue;
2116
+ if (excludedMembers.has(selected.uuid)) continue;
2117
+ return selected.uuid;
2118
+ }
2119
+ for (const memberUuid of availableMembers) {
2120
+ if (!excludedMembers.has(memberUuid)) return memberUuid;
2121
+ }
2122
+ return null;
2123
+ }
2124
+ };
2125
+
2126
+ // ../multi-account-core/src/cascade-state.ts
2127
+ function createCascadeState(prompt, currentAccountUuid) {
2128
+ const attemptedAccounts = /* @__PURE__ */ new Set();
2129
+ if (currentAccountUuid) {
2130
+ attemptedAccounts.add(currentAccountUuid);
2131
+ }
2132
+ return {
2133
+ prompt,
2134
+ attemptedAccounts,
2135
+ visitedChainIndexes: /* @__PURE__ */ new Set()
2136
+ };
2137
+ }
2138
+ var CascadeStateManager = class {
2139
+ suppressNextStartTurn = false;
2140
+ cascadeState = null;
2141
+ startTurn(prompt, currentAccountUuid) {
2142
+ if (this.suppressNextStartTurn) {
2143
+ this.suppressNextStartTurn = false;
2144
+ return this.ensureCascadeState(prompt, currentAccountUuid);
2145
+ }
2146
+ const shouldReset = !this.cascadeState || this.cascadeState.prompt !== prompt;
2147
+ if (shouldReset) {
2148
+ this.cascadeState = createCascadeState(prompt, currentAccountUuid);
2149
+ return this.cascadeState;
2150
+ }
2151
+ return this.ensureCascadeState(prompt, currentAccountUuid);
2152
+ }
2153
+ ensureCascadeState(prompt, currentAccountUuid) {
2154
+ if (!this.cascadeState || this.cascadeState.prompt !== prompt) {
2155
+ this.cascadeState = createCascadeState(prompt, currentAccountUuid);
2156
+ return this.cascadeState;
2157
+ }
2158
+ if (currentAccountUuid) {
2159
+ this.cascadeState.attemptedAccounts.add(currentAccountUuid);
2160
+ }
2161
+ return this.cascadeState;
2162
+ }
2163
+ markAttempted(accountUuid) {
2164
+ if (!this.cascadeState) return;
2165
+ this.cascadeState.attemptedAccounts.add(accountUuid);
2166
+ }
2167
+ markVisitedChainIndex(index) {
2168
+ if (!this.cascadeState) return;
2169
+ this.cascadeState.visitedChainIndexes.add(index);
2170
+ }
2171
+ clearCascadeState() {
2172
+ this.cascadeState = null;
2173
+ this.suppressNextStartTurn = false;
2174
+ }
2175
+ getSnapshot() {
2176
+ if (!this.cascadeState) return null;
2177
+ return {
2178
+ prompt: this.cascadeState.prompt,
2179
+ attemptedAccounts: new Set(this.cascadeState.attemptedAccounts),
2180
+ visitedChainIndexes: new Set(this.cascadeState.visitedChainIndexes)
2181
+ };
2182
+ }
2183
+ };
2184
+
1788
2185
  // src/constants.ts
1789
2186
  var ANTHROPIC_OAUTH_ADAPTER = anthropicOAuthAdapter;
1790
2187
  var ANTHROPIC_CLIENT_ID = ANTHROPIC_OAUTH_ADAPTER.oauthClientId;
@@ -1799,182 +2196,199 @@ var PLAN_LABELS = ANTHROPIC_OAUTH_ADAPTER.planLabels;
1799
2196
  var TOKEN_EXPIRY_BUFFER_MS = 6e4;
1800
2197
  var TOKEN_REFRESH_TIMEOUT_MS = 3e4;
1801
2198
 
1802
- // src/token.ts
1803
- import * as v6 from "valibot";
2199
+ // src/pi-ai-adapter.ts
2200
+ import { AsyncLocalStorage } from "node:async_hooks";
2201
+ import * as piAiOauth from "@mariozechner/pi-ai/oauth";
1804
2202
 
1805
- // src/types.ts
1806
- import * as v5 from "valibot";
1807
- var OAuthCredentialsSchema2 = v5.object({
1808
- type: v5.literal("oauth"),
1809
- refresh: v5.string(),
1810
- access: v5.string(),
1811
- expires: v5.number()
2203
+ // src/token-node-request.ts
2204
+ import * as childProcess from "node:child_process";
2205
+ function buildNodeTokenRequestScript() {
2206
+ return `
2207
+ const https = require("node:https");
2208
+ const endpoint = process.env.ANTHROPIC_REFRESH_ENDPOINT;
2209
+ const timeoutMs = Number(process.env.ANTHROPIC_REFRESH_TIMEOUT_MS || "30000");
2210
+ const payload = process.env.ANTHROPIC_REFRESH_REQUEST_BODY || "";
2211
+
2212
+ function printSuccess(body) {
2213
+ console.log(JSON.stringify({ ok: true, body }));
2214
+ }
2215
+
2216
+ function printFailure(error) {
2217
+ console.log(JSON.stringify({ ok: false, ...error }));
2218
+ }
2219
+
2220
+ const request = https.request(endpoint, {
2221
+ method: "POST",
2222
+ headers: {
2223
+ "Content-Type": "application/json",
2224
+ Accept: "application/json",
2225
+ "Content-Length": Buffer.byteLength(payload).toString(),
2226
+ },
2227
+ }, (response) => {
2228
+ let body = "";
2229
+ response.setEncoding("utf8");
2230
+ response.on("data", (chunk) => {
2231
+ body += chunk;
2232
+ });
2233
+ response.on("end", () => {
2234
+ const status = response.statusCode ?? 0;
2235
+ if (status < 200 || status >= 300) {
2236
+ printFailure({ status, body });
2237
+ return;
2238
+ }
2239
+
2240
+ printSuccess(body);
2241
+ });
1812
2242
  });
1813
- var UsageLimitEntrySchema2 = v5.object({
1814
- utilization: v5.number(),
1815
- resets_at: v5.nullable(v5.string())
2243
+
2244
+ request.setTimeout(timeoutMs, () => {
2245
+ request.destroy(new Error("Request timed out after " + timeoutMs + "ms"));
1816
2246
  });
1817
- var UsageLimitsSchema2 = v5.object({
1818
- five_hour: v5.optional(v5.nullable(UsageLimitEntrySchema2), null),
1819
- seven_day: v5.optional(v5.nullable(UsageLimitEntrySchema2), null),
1820
- seven_day_sonnet: v5.optional(v5.nullable(UsageLimitEntrySchema2), null)
2247
+
2248
+ request.on("error", (error) => {
2249
+ printFailure({ error: error instanceof Error ? error.name + ": " + error.message : String(error) });
1821
2250
  });
1822
- var CredentialRefreshPatchSchema2 = v5.object({
1823
- accessToken: v5.string(),
1824
- expiresAt: v5.number(),
1825
- refreshToken: v5.optional(v5.string()),
1826
- uuid: v5.optional(v5.string()),
1827
- email: v5.optional(v5.string())
2251
+
2252
+ request.write(payload);
2253
+ request.end();
2254
+ `;
2255
+ }
2256
+ async function defaultRunNodeTokenRequest(options) {
2257
+ const script = buildNodeTokenRequestScript();
2258
+ return await new Promise((resolve, reject) => {
2259
+ childProcess.execFile(
2260
+ options.executable,
2261
+ ["-e", script],
2262
+ {
2263
+ timeout: options.timeoutMs + 1e3,
2264
+ maxBuffer: 1024 * 1024,
2265
+ env: {
2266
+ ...process.env,
2267
+ ANTHROPIC_REFRESH_ENDPOINT: options.endpoint,
2268
+ ANTHROPIC_REFRESH_REQUEST_BODY: options.body,
2269
+ ANTHROPIC_REFRESH_TIMEOUT_MS: String(options.timeoutMs)
2270
+ }
2271
+ },
2272
+ (error, stdout, stderr) => {
2273
+ const trimmedStdout = stdout.trim();
2274
+ if (error) {
2275
+ reject(new Error(stderr.trim() || error.message));
2276
+ return;
2277
+ }
2278
+ if (!trimmedStdout) {
2279
+ reject(new Error("Empty response from Node refresh helper"));
2280
+ return;
2281
+ }
2282
+ resolve(trimmedStdout);
2283
+ }
2284
+ );
2285
+ });
2286
+ }
2287
+ var nodeTokenRequestRunner = defaultRunNodeTokenRequest;
2288
+ async function runNodeTokenRequest(options) {
2289
+ return await nodeTokenRequestRunner(options);
2290
+ }
2291
+
2292
+ // src/config.ts
2293
+ initCoreConfig("claude-multiauth.json");
2294
+
2295
+ // src/utils.ts
2296
+ setConfigGetter(getConfig);
2297
+
2298
+ // src/usage.ts
2299
+ import * as v8 from "valibot";
2300
+
2301
+ // src/types.ts
2302
+ import * as v7 from "valibot";
2303
+ var OAuthCredentialsSchema2 = v7.object({
2304
+ type: v7.literal("oauth"),
2305
+ refresh: v7.string(),
2306
+ access: v7.string(),
2307
+ expires: v7.number()
1828
2308
  });
1829
- var StoredAccountSchema2 = v5.object({
1830
- uuid: v5.optional(v5.string()),
1831
- label: v5.optional(v5.string()),
1832
- email: v5.optional(v5.string()),
1833
- planTier: v5.optional(v5.string(), ""),
1834
- refreshToken: v5.string(),
1835
- accessToken: v5.optional(v5.string()),
1836
- expiresAt: v5.optional(v5.number()),
1837
- addedAt: v5.number(),
1838
- lastUsed: v5.number(),
1839
- enabled: v5.optional(v5.boolean(), true),
1840
- rateLimitResetAt: v5.optional(v5.number()),
1841
- cachedUsage: v5.optional(UsageLimitsSchema2),
1842
- cachedUsageAt: v5.optional(v5.number()),
1843
- consecutiveAuthFailures: v5.optional(v5.number(), 0),
1844
- isAuthDisabled: v5.optional(v5.boolean(), false),
1845
- authDisabledReason: v5.optional(v5.string())
2309
+ var UsageLimitEntrySchema2 = v7.object({
2310
+ utilization: v7.number(),
2311
+ resets_at: v7.nullable(v7.string())
1846
2312
  });
1847
- var AccountStorageSchema2 = v5.object({
1848
- version: v5.literal(1),
1849
- accounts: v5.optional(v5.array(StoredAccountSchema2), []),
1850
- activeAccountUuid: v5.optional(v5.string())
2313
+ var UsageLimitsSchema2 = v7.object({
2314
+ five_hour: v7.optional(v7.nullable(UsageLimitEntrySchema2), null),
2315
+ seven_day: v7.optional(v7.nullable(UsageLimitEntrySchema2), null),
2316
+ seven_day_sonnet: v7.optional(v7.nullable(UsageLimitEntrySchema2), null)
1851
2317
  });
1852
- var TokenResponseSchema = v5.object({
1853
- access_token: v5.string(),
1854
- refresh_token: v5.optional(v5.string()),
1855
- expires_in: v5.number(),
1856
- account: v5.optional(v5.object({
1857
- uuid: v5.optional(v5.string()),
1858
- email_address: v5.optional(v5.string())
2318
+ var CredentialRefreshPatchSchema2 = v7.object({
2319
+ accessToken: v7.string(),
2320
+ expiresAt: v7.number(),
2321
+ refreshToken: v7.optional(v7.string()),
2322
+ uuid: v7.optional(v7.string()),
2323
+ email: v7.optional(v7.string())
2324
+ });
2325
+ var StoredAccountSchema2 = v7.object({
2326
+ uuid: v7.optional(v7.string()),
2327
+ label: v7.optional(v7.string()),
2328
+ email: v7.optional(v7.string()),
2329
+ planTier: v7.optional(v7.string(), ""),
2330
+ refreshToken: v7.string(),
2331
+ accessToken: v7.optional(v7.string()),
2332
+ expiresAt: v7.optional(v7.number()),
2333
+ addedAt: v7.number(),
2334
+ lastUsed: v7.number(),
2335
+ enabled: v7.optional(v7.boolean(), true),
2336
+ rateLimitResetAt: v7.optional(v7.number()),
2337
+ cachedUsage: v7.optional(UsageLimitsSchema2),
2338
+ cachedUsageAt: v7.optional(v7.number()),
2339
+ consecutiveAuthFailures: v7.optional(v7.number(), 0),
2340
+ isAuthDisabled: v7.optional(v7.boolean(), false),
2341
+ authDisabledReason: v7.optional(v7.string())
2342
+ });
2343
+ var AccountStorageSchema2 = v7.object({
2344
+ version: v7.literal(1),
2345
+ accounts: v7.optional(v7.array(StoredAccountSchema2), []),
2346
+ activeAccountUuid: v7.optional(v7.string())
2347
+ });
2348
+ var TokenResponseSchema = v7.object({
2349
+ access_token: v7.string(),
2350
+ refresh_token: v7.optional(v7.string()),
2351
+ expires_in: v7.number(),
2352
+ account: v7.optional(v7.object({
2353
+ uuid: v7.optional(v7.string()),
2354
+ email_address: v7.optional(v7.string())
1859
2355
  }))
1860
2356
  });
1861
- var AccountSelectionStrategySchema2 = v5.picklist(["sticky", "round-robin", "hybrid"]);
1862
- var PluginConfigSchema2 = v5.object({
2357
+ var AccountSelectionStrategySchema2 = v7.picklist(["sticky", "round-robin", "hybrid"]);
2358
+ var PluginConfigSchema2 = v7.object({
1863
2359
  /** sticky: same account until failure, round-robin: rotate every request, hybrid: health+usage scoring */
1864
- account_selection_strategy: v5.optional(AccountSelectionStrategySchema2, "sticky"),
2360
+ account_selection_strategy: v7.optional(AccountSelectionStrategySchema2, "sticky"),
1865
2361
  /** Use cross-process claim file to distribute parallel sessions across accounts */
1866
- cross_process_claims: v5.optional(v5.boolean(), true),
2362
+ cross_process_claims: v7.optional(v7.boolean(), true),
1867
2363
  /** Skip account when any usage tier utilization >= this % (100 = disabled) */
1868
- soft_quota_threshold_percent: v5.optional(v5.pipe(v5.number(), v5.minValue(0), v5.maxValue(100)), 100),
2364
+ soft_quota_threshold_percent: v7.optional(v7.pipe(v7.number(), v7.minValue(0), v7.maxValue(100)), 100),
1869
2365
  /** Minimum backoff after rate limit (ms) */
1870
- rate_limit_min_backoff_ms: v5.optional(v5.pipe(v5.number(), v5.minValue(0)), 3e4),
2366
+ rate_limit_min_backoff_ms: v7.optional(v7.pipe(v7.number(), v7.minValue(0)), 3e4),
1871
2367
  /** Default retry-after when header is missing (ms) */
1872
- default_retry_after_ms: v5.optional(v5.pipe(v5.number(), v5.minValue(0)), 6e4),
2368
+ default_retry_after_ms: v7.optional(v7.pipe(v7.number(), v7.minValue(0)), 6e4),
1873
2369
  /** Consecutive auth failures before disabling account */
1874
- max_consecutive_auth_failures: v5.optional(v5.pipe(v5.number(), v5.integer(), v5.minValue(1)), 3),
2370
+ max_consecutive_auth_failures: v7.optional(v7.pipe(v7.number(), v7.integer(), v7.minValue(1)), 3),
1875
2371
  /** Backoff after token refresh failure (ms) */
1876
- token_failure_backoff_ms: v5.optional(v5.pipe(v5.number(), v5.minValue(0)), 3e4),
2372
+ token_failure_backoff_ms: v7.optional(v7.pipe(v7.number(), v7.minValue(0)), 3e4),
1877
2373
  /** Enable proactive background token refresh */
1878
- proactive_refresh: v5.optional(v5.boolean(), true),
2374
+ proactive_refresh: v7.optional(v7.boolean(), true),
1879
2375
  /** Seconds before expiry to trigger proactive refresh (default 30 min) */
1880
- proactive_refresh_buffer_seconds: v5.optional(v5.pipe(v5.number(), v5.minValue(60)), 1800),
2376
+ proactive_refresh_buffer_seconds: v7.optional(v7.pipe(v7.number(), v7.minValue(60)), 1800),
1881
2377
  /** Interval between background refresh checks in seconds (default 5 min) */
1882
- proactive_refresh_interval_seconds: v5.optional(v5.pipe(v5.number(), v5.minValue(30)), 300),
2378
+ proactive_refresh_interval_seconds: v7.optional(v7.pipe(v7.number(), v7.minValue(30)), 300),
1883
2379
  /** Suppress toast notifications */
1884
- quiet_mode: v5.optional(v5.boolean(), false),
2380
+ quiet_mode: v7.optional(v7.boolean(), false),
1885
2381
  /** Enable debug logging */
1886
- debug: v5.optional(v5.boolean(), false)
1887
- });
1888
-
1889
- // src/token.ts
1890
- var PERMANENT_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
1891
- var refreshMutexByAccountId = /* @__PURE__ */ new Map();
1892
- function isTokenExpired(account) {
1893
- if (!account.accessToken || !account.expiresAt) return true;
1894
- return account.expiresAt <= Date.now() + TOKEN_EXPIRY_BUFFER_MS;
1895
- }
1896
- async function refreshToken(currentRefreshToken, accountId, client) {
1897
- if (!currentRefreshToken) return { ok: false, permanent: true };
1898
- const inFlightRefresh = refreshMutexByAccountId.get(accountId);
1899
- if (inFlightRefresh) return inFlightRefresh;
1900
- const refreshPromise = (async () => {
1901
- const controller = new AbortController();
1902
- const timeout = setTimeout(() => controller.abort(), TOKEN_REFRESH_TIMEOUT_MS);
1903
- try {
1904
- const startTime = Date.now();
1905
- const response = await fetch(ANTHROPIC_TOKEN_ENDPOINT, {
1906
- method: "POST",
1907
- headers: { "Content-Type": "application/json" },
1908
- body: JSON.stringify({
1909
- grant_type: "refresh_token",
1910
- refresh_token: currentRefreshToken,
1911
- client_id: ANTHROPIC_CLIENT_ID
1912
- }),
1913
- signal: controller.signal
1914
- });
1915
- if (!response.ok) {
1916
- const isPermanent = PERMANENT_FAILURE_STATUSES.has(response.status);
1917
- await client.app.log({
1918
- body: {
1919
- service: ANTHROPIC_OAUTH_ADAPTER.serviceLogName,
1920
- level: isPermanent ? "error" : "warn",
1921
- message: `Token refresh failed: ${response.status}${isPermanent ? " (permanent)" : ""}`,
1922
- extra: { accountId }
1923
- }
1924
- }).catch(() => {
1925
- });
1926
- return { ok: false, permanent: isPermanent };
1927
- }
1928
- const json = v6.parse(TokenResponseSchema, await response.json());
1929
- const patch = {
1930
- accessToken: json.access_token,
1931
- expiresAt: startTime + json.expires_in * 1e3,
1932
- refreshToken: json.refresh_token,
1933
- uuid: json.account?.uuid,
1934
- email: json.account?.email_address
1935
- };
1936
- return { ok: true, patch };
1937
- } catch (error) {
1938
- await client.app.log({
1939
- body: {
1940
- service: ANTHROPIC_OAUTH_ADAPTER.serviceLogName,
1941
- level: "warn",
1942
- message: `Token refresh network error: ${error instanceof Error ? error.message : String(error)}`,
1943
- extra: { accountId }
1944
- }
1945
- }).catch(() => {
1946
- });
1947
- return { ok: false, permanent: false };
1948
- } finally {
1949
- clearTimeout(timeout);
1950
- refreshMutexByAccountId.delete(accountId);
1951
- }
1952
- })();
1953
- refreshMutexByAccountId.set(accountId, refreshPromise);
1954
- return refreshPromise;
1955
- }
1956
-
1957
- // src/account-manager.ts
1958
- var AccountManager = createAccountManagerForProvider({
1959
- providerAuthId: "anthropic",
1960
- isTokenExpired,
1961
- refreshToken
2382
+ debug: v7.optional(v7.boolean(), false)
1962
2383
  });
1963
2384
 
1964
- // src/config.ts
1965
- initCoreConfig("claude-multiauth.json");
1966
-
1967
- // src/utils.ts
1968
- setConfigGetter(getConfig);
1969
-
1970
2385
  // src/usage.ts
1971
- import * as v7 from "valibot";
1972
2386
  var OAUTH_BETA_HEADER = ANTHROPIC_OAUTH_ADAPTER.oauthBetaHeader;
1973
- var ProfileResponseSchema = v7.object({
1974
- account: v7.object({
1975
- email: v7.optional(v7.string()),
1976
- has_claude_pro: v7.optional(v7.boolean(), false),
1977
- has_claude_max: v7.optional(v7.boolean(), false)
2387
+ var ProfileResponseSchema = v8.object({
2388
+ account: v8.object({
2389
+ email: v8.optional(v8.string()),
2390
+ has_claude_pro: v8.optional(v8.boolean(), false),
2391
+ has_claude_max: v8.optional(v8.boolean(), false)
1978
2392
  })
1979
2393
  });
1980
2394
  async function fetchUsage(accessToken) {
@@ -1989,7 +2403,7 @@ async function fetchUsage(accessToken) {
1989
2403
  if (!response.ok) {
1990
2404
  return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
1991
2405
  }
1992
- const result = v7.safeParse(UsageLimitsSchema2, await response.json());
2406
+ const result = v8.safeParse(UsageLimitsSchema2, await response.json());
1993
2407
  if (!result.success) {
1994
2408
  return { ok: false, reason: `Invalid response: ${result.issues[0]?.message ?? "unknown"}` };
1995
2409
  }
@@ -2011,7 +2425,7 @@ async function fetchProfile(accessToken) {
2011
2425
  if (!response.ok) {
2012
2426
  return { ok: false, reason: `HTTP ${response.status} ${response.statusText}` };
2013
2427
  }
2014
- const result = v7.safeParse(ProfileResponseSchema, await response.json());
2428
+ const result = v8.safeParse(ProfileResponseSchema, await response.json());
2015
2429
  if (!result.success) {
2016
2430
  return { ok: false, reason: `Invalid response: ${result.issues[0]?.message ?? "unknown"}` };
2017
2431
  }
@@ -2047,6 +2461,220 @@ function getPlanLabel(account) {
2047
2461
  return PLAN_LABELS[account.planTier] ?? account.planTier.charAt(0).toUpperCase() + account.planTier.slice(1);
2048
2462
  }
2049
2463
 
2464
+ // src/pi-ai-adapter.ts
2465
+ function fromPiAiCredentials(creds) {
2466
+ return {
2467
+ accessToken: creds.access,
2468
+ refreshToken: creds.refresh,
2469
+ expiresAt: creds.expires
2470
+ };
2471
+ }
2472
+ var ANTHROPIC_REFRESH_ENDPOINT = "https://platform.claude.com/v1/oauth/token";
2473
+ var REFRESH_NODE_EXECUTABLE = process.env.OPENCODE_REFRESH_NODE_EXECUTABLE || "node";
2474
+ var tokenProxyContext = new AsyncLocalStorage();
2475
+ var tokenProxyInstalled = false;
2476
+ var tokenProxyOriginalFetch = null;
2477
+ var refreshEndpointUrl = new URL(ANTHROPIC_REFRESH_ENDPOINT);
2478
+ function buildRefreshRequestError(details) {
2479
+ return new Error(`Anthropic token refresh request failed. url=${ANTHROPIC_REFRESH_ENDPOINT}; details=${details}`);
2480
+ }
2481
+ function buildRefreshInvalidJsonError(body, details) {
2482
+ return new Error(`Anthropic token refresh returned invalid JSON. url=${ANTHROPIC_REFRESH_ENDPOINT}; body=${body}; details=${details}`);
2483
+ }
2484
+ function getRequestUrlString(input) {
2485
+ if (typeof input === "string") return input;
2486
+ if (input instanceof URL) return input.toString();
2487
+ return input.url;
2488
+ }
2489
+ function isAnthropicTokenEndpoint(input) {
2490
+ const rawUrl = getRequestUrlString(input);
2491
+ try {
2492
+ const url = new URL(rawUrl);
2493
+ return url.origin === refreshEndpointUrl.origin && url.pathname === refreshEndpointUrl.pathname;
2494
+ } catch {
2495
+ return rawUrl === ANTHROPIC_REFRESH_ENDPOINT;
2496
+ }
2497
+ }
2498
+ function getRequestBodySource(input, init) {
2499
+ if (init?.body !== void 0) {
2500
+ return init.body;
2501
+ }
2502
+ if (input instanceof Request) {
2503
+ return input.body;
2504
+ }
2505
+ return void 0;
2506
+ }
2507
+ function stringifyBinaryBody(body) {
2508
+ if (body instanceof ArrayBuffer) {
2509
+ return Buffer.from(body).toString("utf8");
2510
+ }
2511
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString("utf8");
2512
+ }
2513
+ async function getRequestBody(input, init) {
2514
+ const body = getRequestBodySource(input, init);
2515
+ if (typeof body === "string") return body;
2516
+ if (body instanceof URLSearchParams) return body.toString();
2517
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
2518
+ return stringifyBinaryBody(body);
2519
+ }
2520
+ if (typeof Blob !== "undefined" && body instanceof Blob) return await body.text();
2521
+ if (body instanceof ReadableStream) return await new Response(body).text();
2522
+ if (input instanceof Request && init?.body === void 0) return await input.clone().text();
2523
+ if (body == null) return "";
2524
+ throw buildRefreshRequestError(`Unsupported token request body type: ${Object.prototype.toString.call(body)}`);
2525
+ }
2526
+ function getRequestMethod(input, init) {
2527
+ return init?.method ?? (input instanceof Request ? input.method : "GET");
2528
+ }
2529
+ function shouldProxyTokenRequest(input) {
2530
+ return tokenProxyContext.getStore() === true && isAnthropicTokenEndpoint(input);
2531
+ }
2532
+ async function postAnthropicTokenViaNode(body) {
2533
+ let output;
2534
+ try {
2535
+ output = await runNodeTokenRequest({
2536
+ body,
2537
+ endpoint: ANTHROPIC_REFRESH_ENDPOINT,
2538
+ executable: REFRESH_NODE_EXECUTABLE,
2539
+ timeoutMs: TOKEN_REFRESH_TIMEOUT_MS
2540
+ });
2541
+ } catch (error) {
2542
+ const details = error instanceof Error ? error.message : String(error);
2543
+ throw buildRefreshRequestError(details);
2544
+ }
2545
+ let parsed;
2546
+ try {
2547
+ parsed = JSON.parse(output);
2548
+ } catch (error) {
2549
+ const details = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2550
+ throw buildRefreshInvalidJsonError(output, details);
2551
+ }
2552
+ const result = parsed;
2553
+ if (!result.ok) {
2554
+ if (result.error) {
2555
+ throw buildRefreshRequestError(result.error);
2556
+ }
2557
+ throw buildRefreshRequestError(`Error: HTTP request failed. status=${result.status ?? 0}; url=${ANTHROPIC_REFRESH_ENDPOINT}; body=${result.body ?? ""}`);
2558
+ }
2559
+ return new Response(result.body ?? "", {
2560
+ status: 200,
2561
+ headers: {
2562
+ "content-type": "application/json"
2563
+ }
2564
+ });
2565
+ }
2566
+ function createAnthropicTokenProxyFetch(originalFetch) {
2567
+ return (async (input, init) => {
2568
+ if (!shouldProxyTokenRequest(input)) {
2569
+ return originalFetch(input, init);
2570
+ }
2571
+ const method = getRequestMethod(input, init).toUpperCase();
2572
+ if (method !== "POST") {
2573
+ throw buildRefreshRequestError(`Unsupported token endpoint method: ${method}`);
2574
+ }
2575
+ return await postAnthropicTokenViaNode(await getRequestBody(input, init));
2576
+ });
2577
+ }
2578
+ function ensureAnthropicTokenProxyFetchInstalled() {
2579
+ if (tokenProxyInstalled) return;
2580
+ tokenProxyOriginalFetch = globalThis.fetch;
2581
+ globalThis.fetch = createAnthropicTokenProxyFetch(tokenProxyOriginalFetch);
2582
+ tokenProxyInstalled = true;
2583
+ }
2584
+ async function withAnthropicTokenProxyFetch(operation) {
2585
+ ensureAnthropicTokenProxyFetchInstalled();
2586
+ return await tokenProxyContext.run(true, operation);
2587
+ }
2588
+ async function fetchProfileWithSingleRetry(accessToken) {
2589
+ let profileResult = await fetchProfile(accessToken);
2590
+ if (profileResult.ok) {
2591
+ return profileResult;
2592
+ }
2593
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
2594
+ profileResult = await fetchProfile(accessToken);
2595
+ return profileResult;
2596
+ }
2597
+ async function loginWithPiAi(callbacks) {
2598
+ const piCreds = await withAnthropicTokenProxyFetch(() => piAiOauth.loginAnthropic({
2599
+ onAuth: callbacks.onAuth,
2600
+ onPrompt: callbacks.onPrompt,
2601
+ onProgress: callbacks.onProgress,
2602
+ onManualCodeInput: callbacks.onManualCodeInput
2603
+ }));
2604
+ const base = fromPiAiCredentials(piCreds);
2605
+ const profileResult = await fetchProfileWithSingleRetry(piCreds.access);
2606
+ const profileData = profileResult.ok ? profileResult.data : void 0;
2607
+ return {
2608
+ ...base,
2609
+ email: profileData?.email,
2610
+ planTier: profileData?.planTier ?? "",
2611
+ addedAt: Date.now(),
2612
+ lastUsed: Date.now()
2613
+ };
2614
+ }
2615
+ async function refreshWithPiAi(currentRefreshToken) {
2616
+ const piCreds = await withAnthropicTokenProxyFetch(() => piAiOauth.refreshAnthropicToken(currentRefreshToken));
2617
+ return {
2618
+ accessToken: piCreds.access,
2619
+ refreshToken: piCreds.refresh,
2620
+ expiresAt: piCreds.expires
2621
+ };
2622
+ }
2623
+ var PI_AI_ADAPTER_SERVICE = ANTHROPIC_OAUTH_ADAPTER.serviceLogName;
2624
+
2625
+ // src/token.ts
2626
+ var PERMANENT_FAILURE_HTTP_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
2627
+ var PERMANENT_FAILURE_MESSAGE_PATTERNS = [
2628
+ /\binvalid_grant\b/i,
2629
+ /\binvalid_scope\b/i,
2630
+ /\bunauthorized_client\b/i,
2631
+ /\brefresh token\b.*\b(invalid|expired|revoked|no longer valid)\b/i,
2632
+ /\bauth(?:entication)?(?:[_\s-]+)?invalid\b/i
2633
+ ];
2634
+ var refreshMutexByAccountId = /* @__PURE__ */ new Map();
2635
+ function isTokenExpired(account) {
2636
+ if (!account.accessToken || !account.expiresAt) return true;
2637
+ return account.expiresAt <= Date.now() + TOKEN_EXPIRY_BUFFER_MS;
2638
+ }
2639
+ async function refreshToken(currentRefreshToken, accountId, client) {
2640
+ if (!currentRefreshToken) return { ok: false, permanent: true };
2641
+ const inFlightRefresh = refreshMutexByAccountId.get(accountId);
2642
+ if (inFlightRefresh) return inFlightRefresh;
2643
+ const refreshPromise = (async () => {
2644
+ try {
2645
+ const patch = await refreshWithPiAi(currentRefreshToken);
2646
+ return { ok: true, patch };
2647
+ } catch (error) {
2648
+ const message = error instanceof Error ? error.message : String(error);
2649
+ const statusMatch = message.match(/\b(400|401|403)\b/);
2650
+ const hasPermanentStatus = statusMatch !== null && PERMANENT_FAILURE_HTTP_STATUSES.has(Number(statusMatch[1]));
2651
+ const hasPermanentMessage = PERMANENT_FAILURE_MESSAGE_PATTERNS.some((pattern) => pattern.test(message));
2652
+ const isPermanent = hasPermanentStatus || hasPermanentMessage;
2653
+ await client.app.log({
2654
+ body: {
2655
+ service: ANTHROPIC_OAUTH_ADAPTER.serviceLogName,
2656
+ level: isPermanent ? "error" : "warn",
2657
+ message: `Token refresh failed: ${message}${isPermanent ? " (permanent)" : ""}`,
2658
+ extra: { accountId }
2659
+ }
2660
+ }).catch(() => {
2661
+ });
2662
+ return { ok: false, permanent: isPermanent };
2663
+ } finally {
2664
+ refreshMutexByAccountId.delete(accountId);
2665
+ }
2666
+ })();
2667
+ refreshMutexByAccountId.set(accountId, refreshPromise);
2668
+ return refreshPromise;
2669
+ }
2670
+
2671
+ // src/account-manager.ts
2672
+ var AccountManager = createAccountManagerForProvider({
2673
+ providerAuthId: "anthropic",
2674
+ isTokenExpired,
2675
+ refreshToken
2676
+ });
2677
+
2050
2678
  // src/rate-limit.ts
2051
2679
  var {
2052
2680
  fetchUsageLimits,
@@ -2061,8 +2689,92 @@ var {
2061
2689
  showToast
2062
2690
  });
2063
2691
 
2692
+ // src/pool-chain-executor.ts
2693
+ function buildCascadePrompt(input, init) {
2694
+ if (typeof init?.body === "string" && init.body.length > 0) {
2695
+ return init.body;
2696
+ }
2697
+ const method = init?.method ?? "GET";
2698
+ return `${method}:${String(input)}`;
2699
+ }
2700
+ function createQueueAwareManager(manager, queue, cascadeStateManager) {
2701
+ return Object.create(manager, {
2702
+ selectAccount: { value: async function selectAccount() {
2703
+ await manager.refresh();
2704
+ while (queue.length > 0) {
2705
+ const next = queue.shift();
2706
+ if (!next) break;
2707
+ const account = manager.getAccounts().find((candidate) => candidate.uuid === next.accountUuid);
2708
+ if (!account?.uuid) continue;
2709
+ if (!account.enabled || account.isAuthDisabled) continue;
2710
+ if (manager.isRateLimited(account)) continue;
2711
+ cascadeStateManager.markAttempted(account.uuid);
2712
+ if (next.chainIndex !== void 0) {
2713
+ cascadeStateManager.markVisitedChainIndex(next.chainIndex);
2714
+ }
2715
+ return account;
2716
+ }
2717
+ return manager.selectAccount();
2718
+ } }
2719
+ });
2720
+ }
2721
+ async function executeWithPoolChainRotation(manager, runtimeFactory, poolManager, cascadeStateManager, poolChainConfig, client, input, init) {
2722
+ const cascadePrompt = buildCascadePrompt(input, init);
2723
+ const currentAccountUuid = manager.getActiveAccount()?.uuid;
2724
+ cascadeStateManager.startTurn(cascadePrompt, currentAccountUuid);
2725
+ const queue = [];
2726
+ const queuedAccountUuids = /* @__PURE__ */ new Set();
2727
+ const queueAwareManager = createQueueAwareManager(manager, queue, cascadeStateManager);
2728
+ const { executeWithAccountRotation: executeWithAccountRotation2 } = createExecutorForProvider("Anthropic", {
2729
+ handleRateLimitResponse: async (rawManager, rawClient, account, response) => {
2730
+ await handleRateLimitResponse(
2731
+ rawManager,
2732
+ rawClient,
2733
+ account,
2734
+ response
2735
+ );
2736
+ if (!account.uuid) return;
2737
+ poolManager.markExhausted(account.uuid);
2738
+ cascadeStateManager.markAttempted(account.uuid);
2739
+ const cascadeState = cascadeStateManager.ensureCascadeState(cascadePrompt, account.uuid);
2740
+ const failoverPlan = await poolManager.buildFailoverPlan(
2741
+ account,
2742
+ poolChainConfig,
2743
+ manager,
2744
+ {
2745
+ attemptedAccounts: cascadeState.attemptedAccounts,
2746
+ visitedChainIndexes: cascadeState.visitedChainIndexes
2747
+ }
2748
+ );
2749
+ for (const candidate of failoverPlan.candidates) {
2750
+ if (queuedAccountUuids.has(candidate.accountUuid)) continue;
2751
+ queue.push({
2752
+ accountUuid: candidate.accountUuid,
2753
+ chainIndex: candidate.chainIndex
2754
+ });
2755
+ queuedAccountUuids.add(candidate.accountUuid);
2756
+ }
2757
+ },
2758
+ formatWaitTime,
2759
+ sleep,
2760
+ showToast,
2761
+ getAccountLabel
2762
+ });
2763
+ try {
2764
+ return await executeWithAccountRotation2(
2765
+ queueAwareManager,
2766
+ runtimeFactory,
2767
+ client,
2768
+ input,
2769
+ init
2770
+ );
2771
+ } finally {
2772
+ cascadeStateManager.clearCascadeState();
2773
+ }
2774
+ }
2775
+
2064
2776
  // src/executor.ts
2065
- var { executeWithAccountRotation } = createExecutorForProvider("Anthropic", {
2777
+ var { executeWithAccountRotation: executeWithCoreAccountRotation } = createExecutorForProvider("Anthropic", {
2066
2778
  handleRateLimitResponse: async (manager, client, account, response) => handleRateLimitResponse(
2067
2779
  manager,
2068
2780
  client,
@@ -2074,6 +2786,44 @@ var { executeWithAccountRotation } = createExecutorForProvider("Anthropic", {
2074
2786
  showToast,
2075
2787
  getAccountLabel
2076
2788
  });
2789
+ function isAllAccountsTerminalError(error) {
2790
+ if (!(error instanceof Error)) return false;
2791
+ return error.message.includes("All Anthropic accounts");
2792
+ }
2793
+ async function clearAuthIfNoUsableAccount(manager, client) {
2794
+ await manager.refresh();
2795
+ if (manager.hasAnyUsableAccount()) return;
2796
+ await client.auth.set({
2797
+ path: { id: ANTHROPIC_OAUTH_ADAPTER.authProviderId },
2798
+ body: getClearedOAuthBody()
2799
+ }).catch(() => {
2800
+ });
2801
+ }
2802
+ function hasPoolChainEntries(config) {
2803
+ return (config.pools?.length ?? 0) > 0 || (config.chains?.length ?? 0) > 0;
2804
+ }
2805
+ async function executeWithAccountRotation(manager, runtimeFactory, client, input, init, options) {
2806
+ try {
2807
+ if (!options || !hasPoolChainEntries(options.poolChainConfig)) {
2808
+ return await executeWithCoreAccountRotation(manager, runtimeFactory, client, input, init);
2809
+ }
2810
+ return await executeWithPoolChainRotation(
2811
+ manager,
2812
+ runtimeFactory,
2813
+ options.poolManager,
2814
+ options.cascadeStateManager,
2815
+ options.poolChainConfig,
2816
+ client,
2817
+ input,
2818
+ init
2819
+ );
2820
+ } catch (error) {
2821
+ if (isAllAccountsTerminalError(error)) {
2822
+ await clearAuthIfNoUsableAccount(manager, client);
2823
+ }
2824
+ throw error;
2825
+ }
2826
+ }
2077
2827
 
2078
2828
  // src/ui/auth-menu.ts
2079
2829
  function formatRelativeTime(timestamp) {
@@ -2294,23 +3044,77 @@ function makeFailedFlowResult(message) {
2294
3044
  callback: async () => ({ type: "failed" })
2295
3045
  };
2296
3046
  }
2297
- function delegateToOriginalAuth(originalAuth, manager, inputs) {
2298
- const originalMethod = originalAuth.methods?.[0];
2299
- if (!originalMethod?.authorize) {
2300
- return Promise.resolve(makeFailedFlowResult("Original OAuth method not available"));
3047
+ function toOAuthCredentials(result) {
3048
+ return { type: "oauth", refresh: result.refresh, access: result.access, expires: result.expires };
3049
+ }
3050
+ function asOAuthCallbackResponse(account) {
3051
+ if (!account.refreshToken || !account.accessToken || typeof account.expiresAt !== "number") {
3052
+ return { type: "failed" };
2301
3053
  }
2302
- return originalMethod.authorize(inputs).then(
2303
- (result) => wrapCallbackWithManagerSync(result, manager, originalAuth, inputs)
2304
- );
3054
+ return {
3055
+ type: "success",
3056
+ refresh: account.refreshToken,
3057
+ access: account.accessToken,
3058
+ expires: account.expiresAt
3059
+ };
2305
3060
  }
2306
- function delegateReauthForAccount(originalAuth, manager, targetAccount, inputs) {
2307
- const originalMethod = originalAuth.methods?.[0];
2308
- if (!originalMethod?.authorize) {
2309
- return Promise.resolve(makeFailedFlowResult("Original OAuth method not available"));
3061
+ function promptLine(message) {
3062
+ if (!isTTY()) return Promise.resolve("");
3063
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
3064
+ return new Promise((resolve) => {
3065
+ rl.question(message, (answer) => {
3066
+ rl.close();
3067
+ resolve(answer.trim());
3068
+ });
3069
+ });
3070
+ }
3071
+ function normalizePromptMessage(prompt) {
3072
+ return prompt.message ?? prompt.text ?? prompt.label ?? prompt.title ?? prompt.placeholder ?? "Continue authentication: ";
3073
+ }
3074
+ var ANTHROPIC_TOKEN_HOST = "platform.claude.com";
3075
+ async function startPiAiFlow() {
3076
+ const originalFetch = globalThis.fetch;
3077
+ globalThis.fetch = ((input, init) => {
3078
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3079
+ if (url.includes(ANTHROPIC_TOKEN_HOST)) {
3080
+ const headers = new Headers(init?.headers);
3081
+ headers.set("user-agent", CLAUDE_CLI_USER_AGENT);
3082
+ return originalFetch(input, { ...init, headers });
3083
+ }
3084
+ return originalFetch(input, init);
3085
+ });
3086
+ try {
3087
+ const completedAccount = await loginWithPiAi({
3088
+ onAuth: (info) => {
3089
+ if (info.url) {
3090
+ openBrowser(info.url);
3091
+ }
3092
+ const instruction = info.instructions ?? "Complete authorization in your browser.";
3093
+ const urlLine = info.url ? `
3094
+ Auth URL (manual fallback): ${info.url}` : "";
3095
+ console.log(`
3096
+ ${instruction}${urlLine}
3097
+ `);
3098
+ },
3099
+ onPrompt: async (prompt) => {
3100
+ const text = normalizePromptMessage(prompt);
3101
+ return promptLine(text.endsWith(":") || text.endsWith("?") ? `${text} ` : `${text}: `);
3102
+ }
3103
+ });
3104
+ const completedResult = asOAuthCallbackResponse(completedAccount);
3105
+ const accountEmail = completedAccount.email;
3106
+ return {
3107
+ url: "",
3108
+ instructions: "",
3109
+ method: "auto",
3110
+ callback: async () => completedResult,
3111
+ _email: accountEmail
3112
+ };
3113
+ } catch {
3114
+ return makeFailedFlowResult("Failed to start OAuth flow");
3115
+ } finally {
3116
+ globalThis.fetch = originalFetch;
2310
3117
  }
2311
- return originalMethod.authorize(inputs).then(
2312
- (result) => wrapCallbackWithAccountReplace(result, manager, targetAccount)
2313
- );
2314
3118
  }
2315
3119
  function wrapCallbackWithAccountReplace(result, manager, targetAccount) {
2316
3120
  const originalCallback = result.callback;
@@ -2319,126 +3123,53 @@ function wrapCallbackWithAccountReplace(result, manager, targetAccount) {
2319
3123
  callback: async function(code) {
2320
3124
  const callbackResult = await originalCallback(code);
2321
3125
  if (callbackResult?.type === "success" && callbackResult.refresh) {
2322
- const auth = {
2323
- type: "oauth",
2324
- refresh: callbackResult.refresh,
2325
- access: callbackResult.access,
2326
- expires: callbackResult.expires
2327
- };
2328
3126
  if (targetAccount.uuid) {
2329
- await manager.replaceAccountCredentials(targetAccount.uuid, auth);
3127
+ await manager.replaceAccountCredentials(targetAccount.uuid, toOAuthCredentials(callbackResult));
2330
3128
  }
2331
- const label = getAccountLabel(targetAccount);
2332
3129
  console.log(`
2333
- \u2705 ${label} re-authenticated successfully.
3130
+ \u2705 ${getAccountLabel(targetAccount)} re-authenticated successfully.
2334
3131
  `);
2335
3132
  }
2336
3133
  return callbackResult;
2337
3134
  }
2338
3135
  };
2339
3136
  }
2340
- function wrapCallbackWithManagerSync(result, manager, originalAuth, inputs) {
3137
+ function wrapCallbackWithManagerSync(result, manager) {
2341
3138
  const originalCallback = result.callback;
3139
+ const email = result._email;
2342
3140
  return {
2343
3141
  ...result,
2344
3142
  callback: async function(code) {
2345
3143
  const callbackResult = await originalCallback(code);
2346
3144
  if (callbackResult?.type === "success" && callbackResult.refresh) {
2347
- const auth = {
2348
- type: "oauth",
2349
- refresh: callbackResult.refresh,
2350
- access: callbackResult.access,
2351
- expires: callbackResult.expires
2352
- };
3145
+ const auth = toOAuthCredentials(callbackResult);
2353
3146
  if (manager) {
2354
3147
  const countBefore = manager.getAccounts().length;
2355
- await manager.addAccount(auth);
3148
+ await manager.addAccount(auth, email);
2356
3149
  const countAfter = manager.getAccounts().length;
2357
- if (countAfter > countBefore) {
2358
- console.log(`
3150
+ const added = countAfter > countBefore;
3151
+ console.log(added ? `
2359
3152
  \u2705 Account added to multi-auth pool (${countAfter} total).
2360
- `);
2361
- } else {
2362
- console.log(`
3153
+ ` : `
2363
3154
  \u2139\uFE0F Account already exists in multi-auth pool (${countAfter} total).
2364
3155
  `);
2365
- }
2366
- if (originalAuth && inputs && isTTY()) {
2367
- await addMoreAccountsLoop(manager, originalAuth, inputs);
2368
- }
2369
3156
  } else {
2370
3157
  await persistFallback(auth);
2371
- console.log(`
2372
- \u2705 Account saved.
2373
- `);
3158
+ console.log("\n\u2705 Account saved.\n");
2374
3159
  }
2375
3160
  }
2376
3161
  return callbackResult;
2377
3162
  }
2378
3163
  };
2379
3164
  }
2380
- function promptYesNo(message) {
2381
- if (!isTTY()) return Promise.resolve(false);
2382
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2383
- return new Promise((resolve) => {
2384
- rl.question(message, (answer) => {
2385
- rl.close();
2386
- resolve(answer.trim().toLowerCase() === "y");
2387
- });
2388
- });
2389
- }
2390
3165
  function openBrowser(url) {
2391
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3166
+ const commands = {
3167
+ darwin: "open",
3168
+ win32: "start"
3169
+ };
3170
+ const cmd = commands[process.platform] ?? "xdg-open";
2392
3171
  exec(`${cmd} ${JSON.stringify(url)}`);
2393
3172
  }
2394
- async function addMoreAccountsLoop(manager, originalAuth, inputs) {
2395
- const originalMethod = originalAuth.methods?.[0];
2396
- if (!originalMethod?.authorize) return;
2397
- while (true) {
2398
- const currentCount = manager.getAccounts().length;
2399
- const shouldAdd = await promptYesNo(`Add another account? (${currentCount} added) (y/n): `);
2400
- if (!shouldAdd) break;
2401
- let flow;
2402
- try {
2403
- flow = await originalMethod.authorize(inputs);
2404
- } catch {
2405
- console.log("\n\u274C Failed to start OAuth flow.\n");
2406
- break;
2407
- }
2408
- if (flow.url) {
2409
- openBrowser(flow.url);
2410
- }
2411
- let callbackResult;
2412
- try {
2413
- callbackResult = await flow.callback();
2414
- } catch {
2415
- console.log("\n\u274C Authentication failed.\n");
2416
- break;
2417
- }
2418
- if (callbackResult?.type !== "success" || !("refresh" in callbackResult)) {
2419
- console.log("\n\u274C Authentication failed.\n");
2420
- break;
2421
- }
2422
- const auth = {
2423
- type: "oauth",
2424
- refresh: callbackResult.refresh,
2425
- access: callbackResult.access,
2426
- expires: callbackResult.expires
2427
- };
2428
- const countBefore = manager.getAccounts().length;
2429
- await manager.addAccount(auth);
2430
- const countAfter = manager.getAccounts().length;
2431
- if (countAfter > countBefore) {
2432
- console.log(`
2433
- \u2705 Account added to multi-auth pool (${countAfter} total).
2434
- `);
2435
- } else {
2436
- console.log(`
2437
- \u2139\uFE0F Account already exists in multi-auth pool (${countAfter} total).
2438
- `);
2439
- }
2440
- }
2441
- }
2442
3173
  async function persistFallback(auth) {
2443
3174
  try {
2444
3175
  const store = new AccountStore();
@@ -2460,15 +3191,15 @@ async function persistFallback(auth) {
2460
3191
  } catch {
2461
3192
  }
2462
3193
  }
2463
- async function handleAuthorize(originalAuth, manager, inputs, client) {
3194
+ async function handleAuthorize(manager, inputs, client) {
2464
3195
  if (!inputs || !isTTY()) {
2465
- return delegateToOriginalAuth(originalAuth, manager, inputs);
3196
+ return wrapCallbackWithManagerSync(await startPiAiFlow(), manager);
2466
3197
  }
2467
3198
  const effectiveManager = manager ?? await loadManagerFromDisk(client);
2468
3199
  if (!effectiveManager || effectiveManager.getAccounts().length === 0) {
2469
- return delegateToOriginalAuth(originalAuth, manager, inputs);
3200
+ return wrapCallbackWithManagerSync(await startPiAiFlow(), manager);
2470
3201
  }
2471
- return runAccountManagementMenu(originalAuth, effectiveManager, inputs, client);
3202
+ return runAccountManagementMenu(effectiveManager, client);
2472
3203
  }
2473
3204
  async function loadManagerFromDisk(client) {
2474
3205
  const store = new AccountStore();
@@ -2478,13 +3209,13 @@ async function loadManagerFromDisk(client) {
2478
3209
  const mgr = await AccountManager.create(store, emptyAuth, client);
2479
3210
  return mgr;
2480
3211
  }
2481
- async function runAccountManagementMenu(originalAuth, manager, inputs, client) {
3212
+ async function runAccountManagementMenu(manager, client) {
2482
3213
  while (true) {
2483
3214
  const allAccounts = manager.getAccounts();
2484
3215
  const menuAction = await showAuthMenu(allAccounts);
2485
3216
  switch (menuAction.type) {
2486
3217
  case "add":
2487
- return delegateToOriginalAuth(originalAuth, manager, inputs);
3218
+ return wrapCallbackWithManagerSync(await startPiAiFlow(), manager);
2488
3219
  case "check-quotas":
2489
3220
  await handleCheckQuotas(manager, client);
2490
3221
  continue;
@@ -2493,7 +3224,7 @@ async function runAccountManagementMenu(originalAuth, manager, inputs, client) {
2493
3224
  if (result.action === "back" || result.action === "cancel") continue;
2494
3225
  const manageResult = await handleManageAction(manager, result.action, result.account, client);
2495
3226
  if (manageResult.triggerOAuth) {
2496
- return delegateReauthForAccount(originalAuth, manager, manageResult.account, inputs);
3227
+ return wrapCallbackWithAccountReplace(await startPiAiFlow(), manager, manageResult.account);
2497
3228
  }
2498
3229
  continue;
2499
3230
  }
@@ -2503,7 +3234,7 @@ async function runAccountManagementMenu(originalAuth, manager, inputs, client) {
2503
3234
  case "delete-all":
2504
3235
  await manager.clearAllAccounts();
2505
3236
  console.log("\nAll accounts deleted.\n");
2506
- return delegateToOriginalAuth(originalAuth, manager, inputs);
3237
+ return wrapCallbackWithManagerSync(await startPiAiFlow(), manager);
2507
3238
  case "cancel":
2508
3239
  return makeFailedFlowResult("Authentication cancelled");
2509
3240
  }
@@ -2518,46 +3249,56 @@ async function handleCheckQuotas(manager, client) {
2518
3249
  \u{1F4CA} Checking quotas for ${accounts.length} account(s)...
2519
3250
  `);
2520
3251
  for (const account of accounts) {
2521
- if (account.isAuthDisabled || !account.accessToken || isTokenExpired(account)) {
2522
- if (!account.uuid) {
2523
- printQuotaError(account, "Missing account UUID");
2524
- continue;
2525
- }
2526
- const result = await manager.ensureValidToken(account.uuid, effectiveClient);
2527
- if (!result.ok) {
2528
- printQuotaError(account, account.isAuthDisabled ? `${account.authDisabledReason ?? "Auth disabled"} (refresh failed)` : "Failed to refresh token");
2529
- continue;
2530
- }
2531
- await manager.refresh();
2532
- }
2533
- const freshAccounts = manager.getAccounts();
2534
- const freshAccount = freshAccounts.find((candidate) => candidate.uuid === account.uuid);
2535
- if (!freshAccount?.accessToken) {
2536
- printQuotaError(account, "No access token available");
2537
- continue;
3252
+ await checkAccountQuota(manager, account, effectiveClient);
3253
+ }
3254
+ }
3255
+ async function checkAccountQuota(manager, account, client) {
3256
+ if (account.isAuthDisabled || !account.accessToken || isTokenExpired(account)) {
3257
+ if (!account.uuid) {
3258
+ printQuotaError(account, "Missing account UUID");
3259
+ return;
2538
3260
  }
2539
- const usageResult = await fetchUsage(freshAccount.accessToken);
2540
- if (!usageResult.ok) {
2541
- printQuotaError(freshAccount, `Failed to fetch usage: ${usageResult.reason}`);
2542
- continue;
3261
+ const refreshResult = await manager.ensureValidToken(account.uuid, client);
3262
+ if (!refreshResult.ok) {
3263
+ await manager.markAuthFailure(account.uuid, refreshResult);
3264
+ await manager.refresh();
3265
+ const updatedAccount = manager.getAccounts().find((candidate) => candidate.uuid === account.uuid);
3266
+ if (!updatedAccount) {
3267
+ printQuotaError(account, refreshResult.permanent ? "Refresh failed permanently; account removed" : "Failed to refresh token");
3268
+ return;
3269
+ }
3270
+ printQuotaError(updatedAccount, updatedAccount.isAuthDisabled ? `${updatedAccount.authDisabledReason ?? "Auth disabled"} (refresh failed)` : "Failed to refresh token");
3271
+ return;
2543
3272
  }
3273
+ await manager.refresh();
3274
+ }
3275
+ const freshAccounts = manager.getAccounts();
3276
+ const freshAccount = freshAccounts.find((candidate) => candidate.uuid === account.uuid);
3277
+ if (!freshAccount?.accessToken) {
3278
+ printQuotaError(account, "No access token available");
3279
+ return;
3280
+ }
3281
+ const usageResult = await fetchUsage(freshAccount.accessToken);
3282
+ if (!usageResult.ok) {
3283
+ printQuotaError(freshAccount, `Failed to fetch usage: ${usageResult.reason}`);
3284
+ return;
3285
+ }
3286
+ if (freshAccount.uuid) {
3287
+ await manager.applyUsageCache(freshAccount.uuid, usageResult.data);
3288
+ }
3289
+ let reportAccount = freshAccount;
3290
+ const profileResult = await fetchProfile(freshAccount.accessToken);
3291
+ if (profileResult.ok) {
2544
3292
  if (freshAccount.uuid) {
2545
- await manager.applyUsageCache(freshAccount.uuid, usageResult.data);
2546
- }
2547
- let reportAccount = freshAccount;
2548
- const profileResult = await fetchProfile(freshAccount.accessToken);
2549
- if (profileResult.ok) {
2550
- if (freshAccount.uuid) {
2551
- await manager.applyProfileCache(freshAccount.uuid, profileResult.data);
2552
- }
2553
- reportAccount = {
2554
- ...freshAccount,
2555
- email: profileResult.data.email ?? freshAccount.email,
2556
- planTier: profileResult.data.planTier
2557
- };
3293
+ await manager.applyProfileCache(freshAccount.uuid, profileResult.data);
2558
3294
  }
2559
- printQuotaReport(reportAccount, usageResult.data);
3295
+ reportAccount = {
3296
+ ...freshAccount,
3297
+ email: profileResult.data.email ?? freshAccount.email,
3298
+ planTier: profileResult.data.planTier
3299
+ };
2560
3300
  }
3301
+ printQuotaReport(reportAccount, usageResult.data);
2561
3302
  }
2562
3303
  async function handleLoadBalancing() {
2563
3304
  const current = getConfig().account_selection_strategy;
@@ -2601,8 +3342,7 @@ Retrying authentication for ${label}...
2601
3342
  console.log(`\u2705 ${label} re-authenticated successfully.
2602
3343
  `);
2603
3344
  } else {
2604
- console.log(`Token refresh failed \u2014 starting OAuth flow...
2605
- `);
3345
+ console.log("Token refresh failed \u2014 starting OAuth flow...\n");
2606
3346
  return { triggerOAuth: true, account };
2607
3347
  }
2608
3348
  break;
@@ -2611,8 +3351,362 @@ Retrying authentication for ${label}...
2611
3351
  return { triggerOAuth: false };
2612
3352
  }
2613
3353
 
3354
+ // src/request-transform.ts
3355
+ import { createHash } from "node:crypto";
3356
+
3357
+ // src/anthropic-prompt.ts
3358
+ var SYSTEM_PROMPT = `You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
3359
+
3360
+ IMPORTANT: Assist with defensive security tasks only. Refuse to create, modify, or improve code that may be used maliciously. Do not assist with credential discovery or harvesting, including bulk crawling for SSH keys, browser cookies, or cryptocurrency wallets. Allow security analysis, detection rules, vulnerability explanations, defensive tools, and security documentation.
3361
+ IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
3362
+
3363
+ If the user asks for help or wants to give feedback inform them of the following:
3364
+ - /help: Get help with using Claude Code
3365
+ - To give feedback, users should report the issue at https://github.com/anthropics/claude-code/issues
3366
+
3367
+ When the user directly asks about Claude Code (eg. "can Claude Code do...", "does Claude Code have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific Claude Code feature (eg. implement a hook, or write a slash command), use the WebFetch tool to gather information to answer the question from Claude Code docs. The list of available docs is available at https://docs.claude.com/en/docs/claude-code/claude_code_docs_map.md.
3368
+
3369
+ # Tone and style
3370
+ You should be concise, direct, and to the point, while providing complete information and matching the level of detail you provide in your response with the level of complexity of the user's query or the work you have completed.
3371
+ A concise response is generally less than 4 lines, not including tool calls or code generated. You should provide more detail when the task is complex or when the user asks you to.
3372
+ IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
3373
+ IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
3374
+ Do not add additional code explanation summary unless requested by the user. After working on a file, briefly confirm that you have completed the task, rather than providing an explanation of what you did.
3375
+ Answer the user's question directly, avoiding any elaboration, explanation, introduction, conclusion, or excessive details. Brief answers are best, but be sure to provide complete information. You MUST avoid extra preamble before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".
3376
+
3377
+ Here are some examples to demonstrate appropriate verbosity:
3378
+ <example>
3379
+ user: 2 + 2
3380
+ assistant: 4
3381
+ </example>
3382
+
3383
+ <example>
3384
+ user: what is 2+2?
3385
+ assistant: 4
3386
+ </example>
3387
+
3388
+ <example>
3389
+ user: is 11 a prime number?
3390
+ assistant: Yes
3391
+ </example>
3392
+
3393
+ <example>
3394
+ user: what command should I run to list files in the current directory?
3395
+ assistant: ls
3396
+ </example>
3397
+
3398
+ <example>
3399
+ user: what command should I run to watch files in the current directory?
3400
+ assistant: [runs ls to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
3401
+ npm run dev
3402
+ </example>
3403
+
3404
+ <example>
3405
+ user: How many golf balls fit inside a jetta?
3406
+ assistant: 150000
3407
+ </example>
3408
+
3409
+ <example>
3410
+ user: what files are in the directory src/?
3411
+ assistant: [runs ls and sees foo.c, bar.c, baz.c]
3412
+ user: which file contains the implementation of foo?
3413
+ assistant: src/foo.c
3414
+ </example>
3415
+ When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
3416
+ Remember that your output will be displayed on a command line interface. Your responses can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
3417
+ Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
3418
+ If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
3419
+ Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
3420
+ IMPORTANT: Keep your responses short, since they will be displayed on a command line interface.
3421
+
3422
+ # Proactiveness
3423
+ You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between:
3424
+ - Doing the right thing when asked, including taking actions and follow-up actions
3425
+ - Not surprising the user with actions you take without asking
3426
+ For example, if the user asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions.
3427
+
3428
+ # Professional objectivity
3429
+ Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if Claude honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
3430
+
3431
+ # Task Management
3432
+ You have access to the TodoWrite tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
3433
+ These tools are also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.
3434
+
3435
+ It is critical that you mark todos as completed as soon as you are done with a task. Do not batch up multiple tasks before marking them as completed.
3436
+
3437
+ Examples:
3438
+
3439
+ <example>
3440
+ user: Run the build and fix any type errors
3441
+ assistant: I'm going to use the TodoWrite tool to write the following items to the todo list:
3442
+ - Run the build
3443
+ - Fix any type errors
3444
+
3445
+ I'm now going to run the build using Bash.
3446
+
3447
+ Looks like I found 10 type errors. I'm going to use the TodoWrite tool to write 10 items to the todo list.
3448
+
3449
+ marking the first todo as in_progress
3450
+
3451
+ Let me start working on the first item...
3452
+
3453
+ The first item has been fixed, let me mark the first todo as completed, and move on to the second item...
3454
+ ..
3455
+ ..
3456
+ </example>
3457
+ In the above example, the assistant completes all the tasks, including the 10 error fixes and running the build and fixing all errors.
3458
+
3459
+ <example>
3460
+ user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
3461
+
3462
+ assistant: I'll help you implement a usage metrics tracking and export feature. Let me first use the TodoWrite tool to plan this task.
3463
+ Adding the following todos to the todo list:
3464
+ 1. Research existing metrics tracking in the codebase
3465
+ 2. Design the metrics collection system
3466
+ 3. Implement core metrics tracking functionality
3467
+ 4. Create export functionality for different formats
3468
+
3469
+ Let me start by researching the existing codebase to understand what metrics we might already be tracking and how we can build on that.
3470
+
3471
+ I'm going to search for any existing metrics or telemetry code in the project.
3472
+
3473
+ I've found some existing telemetry code. Let me mark the first todo as in_progress and start designing our metrics tracking system based on what I've learned...
3474
+
3475
+ [Assistant continues implementing the feature step by step, marking todos as in_progress and completed as they go]
3476
+ </example>
3477
+
3478
+
3479
+ Users may configure 'hooks', shell commands that execute in response to events like tool calls, in settings. Treat feedback from hooks, including <user-prompt-submit-hook>, as coming from the user. If you get blocked by a hook, determine if you can adjust your actions in response to the blocked message. If not, ask the user to check their hooks configuration.
3480
+
3481
+ # Doing tasks
3482
+ The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
3483
+ - Use the TodoWrite tool to plan the task if required
3484
+
3485
+ - Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are automatically added by the system, and bear no direct relation to the specific tool results or user messages in which they appear.
3486
+
3487
+
3488
+ # Tool usage policy
3489
+ - When doing file search, prefer to use the Task tool in order to reduce context usage.
3490
+ - You should proactively use the Task tool with specialized agents when the task at hand matches the agent's description.
3491
+
3492
+ - When WebFetch returns a message about a redirect to a different host, you should immediately make a new WebFetch request with the redirect URL provided in the response.
3493
+ - You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. When making multiple bash tool calls, you MUST send a single message with multiple tools calls to run the calls in parallel. For example, if you need to run "git status" and "git diff", send a single message with two tool calls to run the calls in parallel.
3494
+ - If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to launch multiple agents in parallel, send a single message with multiple Task tool calls.
3495
+ - Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
3496
+
3497
+
3498
+ Here is useful information about the environment you are running in:
3499
+ <env>
3500
+ Working directory: /home/thdxr/dev/projects/anomalyco/opencode/packages/opencode
3501
+ Is directory a git repo: Yes
3502
+ Platform: linux
3503
+ OS Version: Linux 6.12.4-arch1-1
3504
+ Today's date: 2025-09-30
3505
+ </env>
3506
+ You are powered by the model named Sonnet 4.5. The exact model ID is claude-sonnet-4-5-20250929.
3507
+
3508
+ Assistant knowledge cutoff is January 2025.
3509
+
3510
+
3511
+ IMPORTANT: Assist with defensive security tasks only. Refuse to create, modify, or improve code that may be used maliciously. Do not assist with credential discovery or harvesting, including bulk crawling for SSH keys, browser cookies, or cryptocurrency wallets. Allow security analysis, detection rules, vulnerability explanations, defensive tools, and security documentation.
3512
+
3513
+
3514
+ IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation.
3515
+
3516
+ # Code References
3517
+
3518
+ When referencing specific functions or pieces of code include the pattern \`file_path:line_number\` to allow the user to easily navigate to the source code location.
3519
+
3520
+ <example>
3521
+ user: Where are errors from the client handled?
3522
+ assistant: Clients are marked as failed in the \`connectToServer\` function in src/services/process.ts:712.
3523
+ </example>`;
3524
+
3525
+ // src/request-transform.ts
3526
+ function getSystemPrompt() {
3527
+ return SYSTEM_PROMPT;
3528
+ }
3529
+ function sampleCodeUnits(text, indices) {
3530
+ return indices.map((i) => i < text.length ? text.charCodeAt(i).toString(16) : "30").join("");
3531
+ }
3532
+ function buildBillingHeader(firstUserMessage) {
3533
+ const version = ANTHROPIC_OAUTH_ADAPTER.cliVersion;
3534
+ const salt = ANTHROPIC_OAUTH_ADAPTER.billingSalt;
3535
+ if (!version || !salt) return "";
3536
+ const sampled = sampleCodeUnits(firstUserMessage, [4, 7, 20]);
3537
+ const hash = createHash("sha256").update(`${salt}${sampled}${version}`).digest("hex").slice(0, 3);
3538
+ return `x-anthropic-billing-header: cc_version=${version}.${hash}; cc_entrypoint=cli; cch=00000;`;
3539
+ }
3540
+ var OPENCODE_CAMEL_RE = /OpenCode/g;
3541
+ var OPENCODE_LOWER_RE = /(?<!\/)opencode/gi;
3542
+ var TOOL_PREFIX_RESPONSE_RE = /"name"\s*:\s*"mcp_([^"]+)"/g;
3543
+ function addToolPrefix(name) {
3544
+ if (!ANTHROPIC_OAUTH_ADAPTER.transform.addToolPrefix) {
3545
+ return name;
3546
+ }
3547
+ if (!name || name.startsWith(TOOL_PREFIX)) {
3548
+ return name;
3549
+ }
3550
+ return `${TOOL_PREFIX}${name}`;
3551
+ }
3552
+ function stripToolPrefixFromLine(line) {
3553
+ if (!ANTHROPIC_OAUTH_ADAPTER.transform.stripToolPrefixInResponse) {
3554
+ return line;
3555
+ }
3556
+ return line.replace(TOOL_PREFIX_RESPONSE_RE, '"name": "$1"');
3557
+ }
3558
+ function processCompleteLines(buffer) {
3559
+ const lines = buffer.split("\n");
3560
+ const remaining = lines.pop() ?? "";
3561
+ if (lines.length === 0) {
3562
+ return { output: "", remaining };
3563
+ }
3564
+ const output = `${lines.map(stripToolPrefixFromLine).join("\n")}
3565
+ `;
3566
+ return { output, remaining };
3567
+ }
3568
+ function buildRequestHeaders(input, init, accessToken) {
3569
+ const headers = new Headers();
3570
+ if (input instanceof Request) {
3571
+ input.headers.forEach((value, key) => {
3572
+ headers.set(key, value);
3573
+ });
3574
+ }
3575
+ if (init?.headers) {
3576
+ if (init.headers instanceof Headers) {
3577
+ init.headers.forEach((value, key) => {
3578
+ headers.set(key, value);
3579
+ });
3580
+ } else if (Array.isArray(init.headers)) {
3581
+ for (const [key, value] of init.headers) {
3582
+ if (value !== void 0) headers.set(key, String(value));
3583
+ }
3584
+ } else {
3585
+ for (const [key, value] of Object.entries(init.headers)) {
3586
+ if (value !== void 0) headers.set(key, String(value));
3587
+ }
3588
+ }
3589
+ }
3590
+ const incomingBetas = (headers.get("anthropic-beta") || "").split(",").map((b) => b.trim()).filter(Boolean);
3591
+ const mergedBetas = [.../* @__PURE__ */ new Set([
3592
+ ...ANTHROPIC_BETA_HEADER.split(","),
3593
+ ...incomingBetas
3594
+ ])].join(",");
3595
+ headers.set("authorization", `Bearer ${accessToken}`);
3596
+ headers.set("anthropic-beta", mergedBetas);
3597
+ headers.set("user-agent", CLAUDE_CLI_USER_AGENT);
3598
+ headers.set("anthropic-dangerous-direct-browser-access", "true");
3599
+ headers.set("x-app", "cli");
3600
+ headers.delete("x-api-key");
3601
+ return headers;
3602
+ }
3603
+ function transformRequestBody(body) {
3604
+ if (!body) return body;
3605
+ try {
3606
+ const parsed = JSON.parse(body);
3607
+ if (parsed.system && Array.isArray(parsed.system)) {
3608
+ parsed.system = parsed.system.map((systemEntry) => {
3609
+ if (ANTHROPIC_OAUTH_ADAPTER.transform.rewriteOpenCodeBranding && systemEntry.type === "text" && systemEntry.text) {
3610
+ return {
3611
+ ...systemEntry,
3612
+ text: systemEntry.text.replace(OPENCODE_CAMEL_RE, "Claude Code").replace(OPENCODE_LOWER_RE, "Claude")
3613
+ };
3614
+ }
3615
+ return systemEntry;
3616
+ });
3617
+ }
3618
+ if (parsed.tools && Array.isArray(parsed.tools)) {
3619
+ parsed.tools = parsed.tools.map((tool2) => ({
3620
+ ...tool2,
3621
+ name: addToolPrefix(tool2.name)
3622
+ }));
3623
+ }
3624
+ if (parsed.messages && Array.isArray(parsed.messages)) {
3625
+ parsed.messages = parsed.messages.map((message) => {
3626
+ if (message.content && Array.isArray(message.content)) {
3627
+ message.content = message.content.map((contentBlock) => {
3628
+ if (contentBlock.type === "tool_use" && contentBlock.name) {
3629
+ return { ...contentBlock, name: addToolPrefix(contentBlock.name) };
3630
+ }
3631
+ return contentBlock;
3632
+ });
3633
+ }
3634
+ return message;
3635
+ });
3636
+ }
3637
+ return JSON.stringify(parsed);
3638
+ } catch {
3639
+ return body;
3640
+ }
3641
+ }
3642
+ function transformRequestUrl(input) {
3643
+ let url = null;
3644
+ try {
3645
+ if (typeof input === "string" || input instanceof URL) {
3646
+ url = new URL(input.toString());
3647
+ } else if (input instanceof Request) {
3648
+ url = new URL(input.url);
3649
+ }
3650
+ } catch {
3651
+ return input;
3652
+ }
3653
+ if (ANTHROPIC_OAUTH_ADAPTER.transform.enableMessagesBetaQuery && url && url.pathname === "/v1/messages" && !url.searchParams.has("beta")) {
3654
+ url.searchParams.set("beta", "true");
3655
+ return input instanceof Request ? new Request(url.toString(), input) : url;
3656
+ }
3657
+ return input;
3658
+ }
3659
+ function createResponseStreamTransform(response) {
3660
+ if (!response.body) return response;
3661
+ const reader = response.body.getReader();
3662
+ const decoder = new TextDecoder();
3663
+ const encoder = new TextEncoder();
3664
+ let buffer = "";
3665
+ const stream = new ReadableStream({
3666
+ async pull(controller) {
3667
+ try {
3668
+ while (true) {
3669
+ const { done, value } = await reader.read();
3670
+ if (done) {
3671
+ buffer += decoder.decode();
3672
+ if (buffer) {
3673
+ controller.enqueue(encoder.encode(stripToolPrefixFromLine(buffer)));
3674
+ buffer = "";
3675
+ }
3676
+ controller.close();
3677
+ return;
3678
+ }
3679
+ buffer += decoder.decode(value, { stream: true });
3680
+ const { output, remaining } = processCompleteLines(buffer);
3681
+ buffer = remaining;
3682
+ if (output) {
3683
+ controller.enqueue(encoder.encode(output));
3684
+ return;
3685
+ }
3686
+ }
3687
+ } catch (error) {
3688
+ try {
3689
+ reader.cancel().catch(() => {
3690
+ });
3691
+ } catch {
3692
+ }
3693
+ controller.error(error);
3694
+ }
3695
+ },
3696
+ async cancel(reason) {
3697
+ await reader.cancel(reason);
3698
+ }
3699
+ });
3700
+ return new Response(stream, {
3701
+ status: response.status,
3702
+ statusText: response.statusText,
3703
+ headers: response.headers
3704
+ });
3705
+ }
3706
+
2614
3707
  // src/proactive-refresh.ts
2615
3708
  var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
3709
+ providerAuthId: "anthropic",
2616
3710
  getConfig,
2617
3711
  isTokenExpired,
2618
3712
  refreshToken,
@@ -2620,13 +3714,11 @@ var ProactiveRefreshQueue = createProactiveRefreshQueueForProvider({
2620
3714
  });
2621
3715
 
2622
3716
  // src/runtime-factory.ts
2623
- import { AnthropicAuthPlugin } from "opencode-anthropic-auth";
3717
+ var TOKEN_REFRESH_PERMANENT_FAILURE_STATUS = 401;
2624
3718
  var AccountRuntimeFactory = class {
2625
- constructor(pluginCtx, store, client, provider) {
2626
- this.pluginCtx = pluginCtx;
3719
+ constructor(store, client) {
2627
3720
  this.store = store;
2628
3721
  this.client = client;
2629
- this.provider = provider;
2630
3722
  }
2631
3723
  runtimes = /* @__PURE__ */ new Map();
2632
3724
  initLocks = /* @__PURE__ */ new Map();
@@ -2651,77 +3743,116 @@ var AccountRuntimeFactory = class {
2651
3743
  invalidateAll() {
2652
3744
  this.runtimes.clear();
2653
3745
  }
2654
- async createRuntime(uuid) {
2655
- const scopedClient = this.createScopedClient(uuid);
2656
- const scopedCtx = {
2657
- ...this.pluginCtx,
2658
- client: scopedClient
2659
- };
2660
- const hooks = await AnthropicAuthPlugin(scopedCtx);
2661
- const auth = hooks.auth;
2662
- if (!auth?.loader) {
2663
- throw new Error(`Base plugin loader unavailable for account ${uuid}`);
2664
- }
2665
- const scopedGetAuth = this.createScopedGetAuth(uuid);
2666
- const result = await auth.loader(scopedGetAuth, this.provider);
2667
- if (!result?.fetch) {
2668
- throw new Error(`Base plugin returned no fetch for account ${uuid}`);
3746
+ async ensureFreshToken(storedAccount, uuid) {
3747
+ const refreshed = await refreshToken(storedAccount.refreshToken, uuid, this.client);
3748
+ if (!refreshed.ok) {
3749
+ throw new TokenRefreshError(
3750
+ refreshed.permanent,
3751
+ refreshed.permanent ? TOKEN_REFRESH_PERMANENT_FAILURE_STATUS : void 0
3752
+ );
2669
3753
  }
2670
- debugLog(this.client, `Runtime created for account ${uuid.slice(0, 8)}`);
2671
- return { fetch: result.fetch };
2672
- }
2673
- createScopedGetAuth(uuid) {
2674
- const store = this.store;
2675
- return async () => {
2676
- const credentials = await store.readCredentials(uuid);
2677
- if (!credentials) {
2678
- return { type: "oauth", refresh: "", access: "", expires: 0 };
2679
- }
2680
- return {
3754
+ await this.store.mutateAccount(uuid, (account) => {
3755
+ account.accessToken = refreshed.patch.accessToken;
3756
+ account.expiresAt = refreshed.patch.expiresAt;
3757
+ if (refreshed.patch.refreshToken) account.refreshToken = refreshed.patch.refreshToken;
3758
+ if (refreshed.patch.uuid) account.uuid = refreshed.patch.uuid;
3759
+ if (refreshed.patch.email) account.email = refreshed.patch.email;
3760
+ account.consecutiveAuthFailures = 0;
3761
+ account.isAuthDisabled = false;
3762
+ account.authDisabledReason = void 0;
3763
+ });
3764
+ this.client.auth.set({
3765
+ path: { id: ANTHROPIC_OAUTH_ADAPTER.authProviderId },
3766
+ body: {
2681
3767
  type: "oauth",
2682
- refresh: credentials.refreshToken,
2683
- access: credentials.accessToken ?? "",
2684
- expires: credentials.expiresAt ?? 0
2685
- };
2686
- };
3768
+ refresh: refreshed.patch.refreshToken ?? storedAccount.refreshToken,
3769
+ access: refreshed.patch.accessToken,
3770
+ expires: refreshed.patch.expiresAt
3771
+ }
3772
+ }).catch(() => {
3773
+ });
3774
+ return { accessToken: refreshed.patch.accessToken, expiresAt: refreshed.patch.expiresAt };
3775
+ }
3776
+ async executeTransformedFetch(input, init, accessToken) {
3777
+ const transformedInput = transformRequestUrl(input);
3778
+ const headers = buildRequestHeaders(transformedInput, init, accessToken);
3779
+ const transformedBody = typeof init?.body === "string" ? transformRequestBody(init.body) : init?.body;
3780
+ const response = await fetch(transformedInput, {
3781
+ ...init,
3782
+ headers,
3783
+ body: transformedBody
3784
+ });
3785
+ return createResponseStreamTransform(response);
2687
3786
  }
2688
- createScopedClient(uuid) {
2689
- const store = this.store;
2690
- const originalClient = this.client;
2691
- return {
2692
- auth: {
2693
- async set(params) {
2694
- const { body } = params;
2695
- await store.mutateAccount(uuid, (account) => {
2696
- account.accessToken = body.access;
2697
- account.expiresAt = body.expires;
2698
- if (body.refresh) account.refreshToken = body.refresh;
2699
- account.consecutiveAuthFailures = 0;
2700
- account.isAuthDisabled = false;
2701
- account.authDisabledReason = void 0;
2702
- });
2703
- originalClient.auth.set(params).catch(() => {
2704
- });
2705
- }
2706
- },
2707
- tui: originalClient.tui,
2708
- app: originalClient.app
3787
+ async createRuntime(uuid) {
3788
+ const fetchWithAccount = async (input, init) => {
3789
+ const storage = await this.store.load();
3790
+ const storedAccount = storage.accounts.find((account) => account.uuid === uuid);
3791
+ if (!storedAccount) {
3792
+ throw new Error(`No credentials found for account ${uuid}`);
3793
+ }
3794
+ let accessToken = storedAccount.accessToken;
3795
+ let expiresAt = storedAccount.expiresAt;
3796
+ if (!accessToken || !expiresAt || isTokenExpired({ accessToken, expiresAt })) {
3797
+ ({ accessToken, expiresAt } = await this.ensureFreshToken(storedAccount, uuid));
3798
+ }
3799
+ if (!accessToken) {
3800
+ throw new Error(`No access token available for account ${uuid}`);
3801
+ }
3802
+ return this.executeTransformedFetch(input, init, accessToken);
2709
3803
  };
3804
+ debugLog(this.client, `Runtime created for account ${uuid.slice(0, 8)}`);
3805
+ return { fetch: fetchWithAccount };
2710
3806
  }
2711
3807
  };
2712
3808
 
2713
3809
  // src/index.ts
3810
+ function extractFirstUserText(input) {
3811
+ try {
3812
+ const raw = input;
3813
+ const messages = raw.messages ?? raw.request?.messages;
3814
+ if (!Array.isArray(messages)) return "";
3815
+ for (const msg of messages) {
3816
+ if (msg.role !== "user") continue;
3817
+ if (typeof msg.content === "string") return msg.content;
3818
+ if (Array.isArray(msg.content)) {
3819
+ for (const block of msg.content) {
3820
+ if (block.type === "text" && block.text) return block.text;
3821
+ }
3822
+ }
3823
+ }
3824
+ } catch {
3825
+ }
3826
+ return "";
3827
+ }
3828
+ function injectSystemPrompt(output) {
3829
+ const systemPrompt = getSystemPrompt();
3830
+ if (!Array.isArray(output.system)) {
3831
+ output.system = [systemPrompt];
3832
+ return;
3833
+ }
3834
+ if (!output.system.includes(systemPrompt)) {
3835
+ output.system.unshift(systemPrompt);
3836
+ }
3837
+ }
2714
3838
  var ClaudeMultiAuthPlugin = async (ctx) => {
2715
3839
  const { client } = ctx;
2716
3840
  await loadConfig();
2717
- const originalHooks = await AnthropicAuthPlugin2(ctx);
2718
- const originalAuth = originalHooks.auth;
2719
3841
  const store = new AccountStore();
2720
3842
  let manager = null;
2721
3843
  let runtimeFactory = null;
2722
3844
  let refreshQueue = null;
3845
+ let poolManager = null;
3846
+ let cascadeStateManager = null;
3847
+ let poolChainConfig = { pools: [], chains: [] };
2723
3848
  return {
2724
- "experimental.chat.system.transform": originalHooks["experimental.chat.system.transform"],
3849
+ "experimental.chat.system.transform": (input, output) => {
3850
+ injectSystemPrompt(output);
3851
+ const billingHeader = buildBillingHeader(extractFirstUserText(input));
3852
+ if (billingHeader && !output.system?.includes(billingHeader)) {
3853
+ output.system?.unshift(billingHeader);
3854
+ }
3855
+ },
2725
3856
  tool: {
2726
3857
  [ANTHROPIC_OAUTH_ADAPTER.statusToolName]: tool({
2727
3858
  description: "Show status of all multi-auth accounts including rate limits and usage.",
@@ -2749,9 +3880,13 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
2749
3880
  if (account.isAuthDisabled) statusParts.push(`AUTH DISABLED: ${account.authDisabledReason}`);
2750
3881
  else if (!account.enabled) statusParts.push("disabled");
2751
3882
  else statusParts.push("enabled");
2752
- if (account.rateLimitResetAt && account.rateLimitResetAt > Date.now()) {
2753
- const remaining = formatWaitTime(account.rateLimitResetAt - Date.now());
2754
- statusParts.push(`RATE LIMITED (resets in ${remaining})`);
3883
+ if (account.rateLimitResetAt) {
3884
+ if (account.rateLimitResetAt > Date.now()) {
3885
+ const remaining = formatWaitTime(account.rateLimitResetAt - Date.now());
3886
+ statusParts.push(`RATE LIMITED (resets in ${remaining})`);
3887
+ } else {
3888
+ statusParts.push("RATE LIMIT RESET");
3889
+ }
2755
3890
  }
2756
3891
  if (account.cachedUsage) {
2757
3892
  const now = Date.now();
@@ -2779,7 +3914,7 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
2779
3914
  type: "oauth",
2780
3915
  async authorize() {
2781
3916
  const inputs = arguments.length > 0 ? arguments[0] : void 0;
2782
- return handleAuthorize(originalAuth, manager, inputs, client);
3917
+ return handleAuthorize(manager, inputs, client);
2783
3918
  }
2784
3919
  },
2785
3920
  { type: "api", label: "Create an API Key" },
@@ -2788,7 +3923,7 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
2788
3923
  async loader(getAuth, provider) {
2789
3924
  const auth = await getAuth();
2790
3925
  if (auth.type !== "oauth") {
2791
- return originalAuth.loader(getAuth, provider);
3926
+ return { apiKey: "", fetch };
2792
3927
  }
2793
3928
  for (const model of Object.values(provider.models ?? {})) {
2794
3929
  if (model) {
@@ -2798,8 +3933,12 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
2798
3933
  const credentials = auth;
2799
3934
  await migrateFromAuthJson("anthropic", store);
2800
3935
  manager = await AccountManager.create(store, credentials, client);
2801
- runtimeFactory = new AccountRuntimeFactory(ctx, store, client, provider);
3936
+ runtimeFactory = new AccountRuntimeFactory(store, client);
2802
3937
  manager.setRuntimeFactory(runtimeFactory);
3938
+ poolChainConfig = await loadPoolChainConfig();
3939
+ poolManager = new PoolManager();
3940
+ poolManager.loadPools(poolChainConfig.pools);
3941
+ cascadeStateManager = new CascadeStateManager();
2803
3942
  if (manager.getAccountCount() > 0) {
2804
3943
  const activeLabel = manager.getActiveAccount() ? getAccountLabel(manager.getActiveAccount()) : "none";
2805
3944
  void showToast(
@@ -2822,12 +3961,21 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
2822
3961
  refreshQueue = new ProactiveRefreshQueue(
2823
3962
  client,
2824
3963
  store,
2825
- (uuid) => runtimeFactory?.invalidate(uuid)
3964
+ (uuid) => {
3965
+ runtimeFactory?.invalidate(uuid);
3966
+ void manager?.refresh();
3967
+ }
2826
3968
  );
2827
3969
  refreshQueue.start();
2828
3970
  }
2829
3971
  return {
2830
3972
  apiKey: "",
3973
+ "chat.headers": async (input, output) => {
3974
+ if (input.provider?.info?.id !== ANTHROPIC_OAUTH_ADAPTER.authProviderId) return;
3975
+ output.headers["user-agent"] = CLAUDE_CLI_USER_AGENT;
3976
+ output.headers["anthropic-beta"] = ANTHROPIC_BETA_HEADER;
3977
+ output.headers["x-app"] = "cli";
3978
+ },
2831
3979
  async fetch(input, init) {
2832
3980
  if (!manager || !runtimeFactory) {
2833
3981
  return fetch(input, init);
@@ -2837,7 +3985,23 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
2837
3985
  "No Anthropic accounts configured. Run `opencode auth login` to add an account."
2838
3986
  );
2839
3987
  }
2840
- return executeWithAccountRotation(manager, runtimeFactory, client, input, init);
3988
+ if (!poolManager || !cascadeStateManager) {
3989
+ poolManager = new PoolManager();
3990
+ poolManager.loadPools(poolChainConfig.pools);
3991
+ cascadeStateManager = new CascadeStateManager();
3992
+ }
3993
+ return executeWithAccountRotation(
3994
+ manager,
3995
+ runtimeFactory,
3996
+ client,
3997
+ input,
3998
+ init,
3999
+ {
4000
+ poolManager,
4001
+ cascadeStateManager,
4002
+ poolChainConfig
4003
+ }
4004
+ );
2841
4005
  }
2842
4006
  };
2843
4007
  }