opencode-multi-account-core 0.2.2 → 0.2.4

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.
@@ -35,7 +35,7 @@ export interface AccountManagerInstance {
35
35
  validateNonActiveTokens(client: PluginClient): Promise<void>;
36
36
  removeAccount(index: number): Promise<boolean>;
37
37
  clearAllAccounts(): Promise<void>;
38
- addAccount(auth: OAuthCredentials): Promise<void>;
38
+ addAccount(auth: OAuthCredentials, email?: string): Promise<void>;
39
39
  toggleEnabled(uuid: string): Promise<void>;
40
40
  replaceAccountCredentials(uuid: string, auth: OAuthCredentials): Promise<void>;
41
41
  retryAuth(uuid: string, client: PluginClient): Promise<TokenRefreshResult>;
@@ -19,6 +19,8 @@ export interface OAuthAdapter {
19
19
  oauthBetaHeader: string;
20
20
  requestBetaHeader: string;
21
21
  cliUserAgent: string;
22
+ cliVersion: string;
23
+ billingSalt: string;
22
24
  toolPrefix: string;
23
25
  accountStorageFilename: string;
24
26
  transform: OAuthAdapterTransformConfig;
@@ -0,0 +1,11 @@
1
+ import type { CascadeState } from "./pool-types";
2
+ export declare class CascadeStateManager {
3
+ suppressNextStartTurn: boolean;
4
+ private cascadeState;
5
+ startTurn(prompt: string, currentAccountUuid?: string): CascadeState;
6
+ ensureCascadeState(prompt: string, currentAccountUuid?: string): CascadeState;
7
+ markAttempted(accountUuid: string): void;
8
+ markVisitedChainIndex(index: number): void;
9
+ clearCascadeState(): void;
10
+ getSnapshot(): CascadeState | null;
11
+ }
@@ -1,4 +1,4 @@
1
- import type { ManagedAccount, PluginClient, TokenRefreshResult } from "./types";
1
+ import { type ManagedAccount, type PluginClient, type TokenRefreshResult } from "./types";
2
2
  export interface ExecutorAccountManager {
3
3
  getAccountCount(): number;
4
4
  refresh(): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -14,3 +14,7 @@ export * from "./ui/ansi";
14
14
  export * from "./ui/confirm";
15
15
  export * from "./ui/select";
16
16
  export * from "./adapters";
17
+ export * from "./pool-types";
18
+ export * from "./pool-config-store";
19
+ export * from "./pool-manager";
20
+ export * from "./cascade-state";
package/dist/index.js CHANGED
@@ -81,6 +81,23 @@ var PluginConfigSchema = v.object({
81
81
  quiet_mode: v.optional(v.boolean(), false),
82
82
  debug: v.optional(v.boolean(), false)
83
83
  });
84
+ var TokenRefreshError = class _TokenRefreshError extends Error {
85
+ status;
86
+ permanent;
87
+ constructor(permanent, status) {
88
+ super(status === void 0 ? "Token refresh failed" : `Token refresh failed: ${status}`);
89
+ this.name = "TokenRefreshError";
90
+ this.status = status;
91
+ this.permanent = permanent;
92
+ Object.setPrototypeOf(this, _TokenRefreshError.prototype);
93
+ }
94
+ };
95
+ function isTokenRefreshError(error) {
96
+ if (error instanceof TokenRefreshError) return true;
97
+ if (!(error instanceof Error)) return false;
98
+ const candidate = error;
99
+ return candidate.name === "TokenRefreshError" && typeof candidate.permanent === "boolean" && (candidate.status === void 0 || typeof candidate.status === "number");
100
+ }
84
101
 
85
102
  // src/config.ts
86
103
  var DEFAULT_CONFIG_FILENAME = "multiauth-config.json";
@@ -224,6 +241,14 @@ function createMinimalClient() {
224
241
  }
225
242
  };
226
243
  }
244
+ function getClearedOAuthBody() {
245
+ return {
246
+ type: "oauth",
247
+ refresh: "",
248
+ access: "",
249
+ expires: 0
250
+ };
251
+ }
227
252
 
228
253
  // src/claims.ts
229
254
  var CLAIMS_FILENAME = "multiauth-claims.json";
@@ -661,13 +686,7 @@ function createAccountManagerForProvider(dependencies) {
661
686
  });
662
687
  }
663
688
  async markRevoked(uuid) {
664
- await this.store.mutateAccount(uuid, (account) => {
665
- account.isAuthDisabled = true;
666
- account.authDisabledReason = "OAuth token revoked (403)";
667
- account.accessToken = void 0;
668
- account.expiresAt = void 0;
669
- });
670
- this.runtimeFactory?.invalidate(uuid);
689
+ await this.removeAccountByUuid(uuid);
671
690
  }
672
691
  async markSuccess(uuid) {
673
692
  this.last429Map.delete(uuid);
@@ -690,15 +709,32 @@ function createAccountManagerForProvider(dependencies) {
690
709
  }).catch(() => {
691
710
  });
692
711
  }
712
+ async clearOpenCodeAuthIfNoAccountsRemain() {
713
+ if (!this.client) return;
714
+ const storage = await this.store.load();
715
+ if (storage.accounts.length > 0) return;
716
+ await this.client.auth.set({
717
+ path: { id: providerAuthId },
718
+ body: getClearedOAuthBody()
719
+ }).catch(() => {
720
+ });
721
+ }
722
+ async removeAccountByUuid(uuid) {
723
+ const removed = await this.store.removeAccount(uuid);
724
+ if (!removed) return;
725
+ this.last429Map.delete(uuid);
726
+ this.runtimeFactory?.invalidate(uuid);
727
+ await this.refresh();
728
+ await this.clearOpenCodeAuthIfNoAccountsRemain();
729
+ }
693
730
  async markAuthFailure(uuid, result) {
731
+ if (!result.ok && result.permanent) {
732
+ await this.removeAccountByUuid(uuid);
733
+ return;
734
+ }
694
735
  await this.store.mutateStorage((storage) => {
695
736
  const account = storage.accounts.find((entry) => entry.uuid === uuid);
696
737
  if (!account) return;
697
- if (!result.ok && result.permanent) {
698
- account.isAuthDisabled = true;
699
- account.authDisabledReason = "Token permanently rejected (400/401/403)";
700
- return;
701
- }
702
738
  account.consecutiveAuthFailures = (account.consecutiveAuthFailures ?? 0) + 1;
703
739
  const maxFailures = getConfig().max_consecutive_auth_failures;
704
740
  const usableCount = storage.accounts.filter(
@@ -795,11 +831,21 @@ function createAccountManagerForProvider(dependencies) {
795
831
  this.cached = [];
796
832
  this.activeAccountUuid = void 0;
797
833
  }
798
- async addAccount(auth) {
834
+ async addAccount(auth, email) {
799
835
  if (!auth.refresh) return;
800
- const existing = this.cached.find((account) => account.refreshToken === auth.refresh);
801
- if (existing) return;
836
+ const existingByToken = this.cached.find((account) => account.refreshToken === auth.refresh);
837
+ if (existingByToken) return;
838
+ if (email) {
839
+ const existingByEmail = this.cached.find(
840
+ (account) => account.email && account.email === email
841
+ );
842
+ if (existingByEmail?.uuid) {
843
+ await this.replaceAccountCredentials(existingByEmail.uuid, auth);
844
+ return;
845
+ }
846
+ }
802
847
  const newAccount = this.createNewAccount(auth, Date.now());
848
+ if (email) newAccount.email = email;
803
849
  await this.store.addAccount(newAccount);
804
850
  this.activeAccountUuid = newAccount.uuid;
805
851
  await this.store.setActiveUuid(newAccount.uuid);
@@ -1125,7 +1171,9 @@ var MAX_SERVER_RETRIES_PER_ATTEMPT = 2;
1125
1171
  var MAX_RESOLVE_ATTEMPTS = 10;
1126
1172
  var SERVER_RETRY_BASE_MS = 1e3;
1127
1173
  var SERVER_RETRY_MAX_MS = 4e3;
1128
- var PERMANENT_AUTH_FAILURE_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
1174
+ function isAbortError(error) {
1175
+ return error instanceof Error && error.name === "AbortError";
1176
+ }
1129
1177
  function createExecutorForProvider(providerName, dependencies) {
1130
1178
  const {
1131
1179
  handleRateLimitResponse,
@@ -1136,77 +1184,49 @@ function createExecutorForProvider(providerName, dependencies) {
1136
1184
  } = dependencies;
1137
1185
  async function executeWithAccountRotation(manager, runtimeFactory, client, input, init) {
1138
1186
  const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
1139
- let retries = 0;
1140
1187
  let previousAccountUuid;
1141
- while (true) {
1142
- if (++retries > maxRetries) {
1143
- throw new Error(
1144
- `Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
1145
- );
1146
- }
1147
- await manager.refresh();
1148
- const account = await resolveAccount(manager, client);
1149
- const accountUuid = account.uuid;
1150
- if (!accountUuid) continue;
1151
- if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
1152
- void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
1153
- }
1154
- previousAccountUuid = accountUuid;
1155
- let runtime;
1156
- let response;
1157
- try {
1158
- runtime = await runtimeFactory.getRuntime(accountUuid);
1159
- response = await runtime.fetch(input, init);
1160
- } catch (error) {
1161
- if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1162
- continue;
1188
+ async function retryServerErrors(account, runtime) {
1189
+ for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
1190
+ const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
1191
+ const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
1192
+ await sleep2(jitteredBackoff);
1193
+ let retryResponse;
1194
+ try {
1195
+ retryResponse = await runtime.fetch(input, init);
1196
+ } catch (error) {
1197
+ if (isAbortError(error)) throw error;
1198
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1199
+ return null;
1200
+ }
1201
+ void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1202
+ return null;
1163
1203
  }
1164
- void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1165
- continue;
1204
+ if (retryResponse.status < 500) return retryResponse;
1166
1205
  }
1206
+ return null;
1207
+ }
1208
+ const dispatchResponseStatus = async (account, accountUuid, runtime, response, allow401Retry, from401RefreshRetry) => {
1167
1209
  if (response.status >= 500) {
1168
- let serverResponse = response;
1169
- let networkErrorDuringServerRetry = false;
1170
- let authFailureDuringServerRetry = false;
1171
- for (let attempt = 0; attempt < MAX_SERVER_RETRIES_PER_ATTEMPT; attempt++) {
1172
- const backoff = Math.min(SERVER_RETRY_BASE_MS * 2 ** attempt, SERVER_RETRY_MAX_MS);
1173
- const jitteredBackoff = backoff * (0.5 + Math.random() * 0.5);
1174
- await sleep2(jitteredBackoff);
1210
+ const recovered = await retryServerErrors(account, runtime);
1211
+ if (recovered === null) {
1212
+ return { type: "retryOuter" };
1213
+ }
1214
+ response = recovered;
1215
+ }
1216
+ if (response.status === 401) {
1217
+ if (allow401Retry) {
1218
+ runtimeFactory.invalidate(accountUuid);
1175
1219
  try {
1176
- serverResponse = await runtime.fetch(input, init);
1220
+ const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
1221
+ const retryResponse = await retryRuntime.fetch(input, init);
1222
+ return dispatchResponseStatus(account, accountUuid, retryRuntime, retryResponse, false, true);
1177
1223
  } catch (error) {
1224
+ if (isAbortError(error)) throw error;
1178
1225
  if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1179
- authFailureDuringServerRetry = true;
1180
- break;
1226
+ return { type: "retryOuter" };
1181
1227
  }
1182
- networkErrorDuringServerRetry = true;
1183
- void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1184
- break;
1228
+ return { type: "retryOuter" };
1185
1229
  }
1186
- if (serverResponse.status < 500) break;
1187
- }
1188
- if (authFailureDuringServerRetry) {
1189
- continue;
1190
- }
1191
- if (networkErrorDuringServerRetry || serverResponse.status >= 500) {
1192
- continue;
1193
- }
1194
- response = serverResponse;
1195
- }
1196
- if (response.status === 401) {
1197
- runtimeFactory.invalidate(accountUuid);
1198
- try {
1199
- const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
1200
- const retryResponse = await retryRuntime.fetch(input, init);
1201
- if (retryResponse.status !== 401) {
1202
- await manager.markSuccess(accountUuid);
1203
- return retryResponse;
1204
- }
1205
- } catch (error) {
1206
- if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1207
- continue;
1208
- }
1209
- continue;
1210
1230
  }
1211
1231
  await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
1212
1232
  await manager.refresh();
@@ -1217,7 +1237,7 @@ function createExecutorForProvider(providerName, dependencies) {
1217
1237
  );
1218
1238
  }
1219
1239
  void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1220
- continue;
1240
+ return { type: "retryOuter" };
1221
1241
  }
1222
1242
  if (response.status === 403) {
1223
1243
  const revoked = await isRevokedTokenResponse(response);
@@ -1234,26 +1254,62 @@ function createExecutorForProvider(providerName, dependencies) {
1234
1254
  `All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
1235
1255
  );
1236
1256
  }
1237
- continue;
1257
+ return { type: "retryOuter" };
1258
+ }
1259
+ if (from401RefreshRetry) {
1260
+ return { type: "handled", response };
1238
1261
  }
1239
1262
  }
1240
1263
  if (response.status === 429) {
1241
1264
  await handleRateLimitResponse(manager, client, account, response);
1265
+ return { type: "handled" };
1266
+ }
1267
+ return { type: "success", response };
1268
+ };
1269
+ for (let retries = 1; retries <= maxRetries; retries++) {
1270
+ await manager.refresh();
1271
+ const account = await resolveAccount(manager, client);
1272
+ const accountUuid = account.uuid;
1273
+ if (!accountUuid) continue;
1274
+ if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
1275
+ void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
1276
+ }
1277
+ previousAccountUuid = accountUuid;
1278
+ let runtime;
1279
+ let response;
1280
+ try {
1281
+ runtime = await runtimeFactory.getRuntime(accountUuid);
1282
+ response = await runtime.fetch(input, init);
1283
+ } catch (error) {
1284
+ if (isAbortError(error)) throw error;
1285
+ if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1286
+ continue;
1287
+ }
1288
+ void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1289
+ continue;
1290
+ }
1291
+ const transition = await dispatchResponseStatus(account, accountUuid, runtime, response, true, false);
1292
+ if (transition.type === "retryOuter" || transition.type === "handled") {
1293
+ if (transition.type === "handled" && transition.response) {
1294
+ return transition.response;
1295
+ }
1242
1296
  continue;
1243
1297
  }
1244
1298
  await manager.markSuccess(accountUuid);
1245
- return response;
1299
+ return transition.response;
1246
1300
  }
1301
+ throw new Error(
1302
+ `Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
1303
+ );
1247
1304
  }
1248
1305
  async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
1249
- const refreshFailureStatus = getRefreshFailureStatus(error);
1250
- if (refreshFailureStatus === void 0) return false;
1306
+ if (!isTokenRefreshError(error)) return false;
1251
1307
  if (!account.uuid) return false;
1252
1308
  const accountUuid = account.uuid;
1253
1309
  runtimeFactory.invalidate(accountUuid);
1254
1310
  await manager.markAuthFailure(accountUuid, {
1255
1311
  ok: false,
1256
- permanent: PERMANENT_AUTH_FAILURE_STATUSES.has(refreshFailureStatus)
1312
+ permanent: error.permanent
1257
1313
  });
1258
1314
  await manager.refresh();
1259
1315
  if (!manager.hasAnyUsableAccount()) {
@@ -1298,13 +1354,6 @@ function createExecutorForProvider(providerName, dependencies) {
1298
1354
  executeWithAccountRotation
1299
1355
  };
1300
1356
  }
1301
- function getRefreshFailureStatus(error) {
1302
- if (!(error instanceof Error)) return void 0;
1303
- const matched = error.message.match(/Token refresh failed:\s*(\d{3})/);
1304
- if (!matched) return void 0;
1305
- const status = Number(matched[1]);
1306
- return Number.isFinite(status) ? status : void 0;
1307
- }
1308
1357
  async function isRevokedTokenResponse(response) {
1309
1358
  try {
1310
1359
  const cloned = response.clone();
@@ -1319,6 +1368,7 @@ async function isRevokedTokenResponse(response) {
1319
1368
  var INITIAL_DELAY_MS = 5e3;
1320
1369
  function createProactiveRefreshQueueForProvider(dependencies) {
1321
1370
  const {
1371
+ providerAuthId,
1322
1372
  getConfig: getConfig2,
1323
1373
  refreshToken,
1324
1374
  isTokenExpired,
@@ -1337,6 +1387,10 @@ function createProactiveRefreshQueueForProvider(dependencies) {
1337
1387
  const config = getConfig2();
1338
1388
  if (!config.proactive_refresh) return;
1339
1389
  this.runToken++;
1390
+ if (this.timeoutHandle) {
1391
+ clearTimeout(this.timeoutHandle);
1392
+ this.timeoutHandle = null;
1393
+ }
1340
1394
  this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
1341
1395
  debugLog2(this.client, "Proactive refresh started", {
1342
1396
  intervalSeconds: config.proactive_refresh_interval_seconds,
@@ -1411,23 +1465,41 @@ function createProactiveRefreshQueueForProvider(dependencies) {
1411
1465
  }
1412
1466
  async persistFailure(account, permanent) {
1413
1467
  try {
1414
- await this.store.mutateAccount(account.uuid, (target) => {
1415
- if (permanent) {
1468
+ const accountUuid = account.uuid;
1469
+ if (!accountUuid) return;
1470
+ if (permanent) {
1471
+ const removed = await this.store.removeAccount(accountUuid);
1472
+ if (!removed) return;
1473
+ this.onInvalidate?.(accountUuid);
1474
+ await this.clearOpenCodeAuthIfNoAccountsRemain();
1475
+ return;
1476
+ }
1477
+ await this.store.mutateStorage((storage) => {
1478
+ const target = storage.accounts.find((entry) => entry.uuid === accountUuid);
1479
+ if (!target) return;
1480
+ target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1481
+ const maxFailures = getConfig2().max_consecutive_auth_failures;
1482
+ const usableCount = storage.accounts.filter(
1483
+ (entry) => entry.enabled && !entry.isAuthDisabled && entry.uuid !== accountUuid
1484
+ ).length;
1485
+ if (target.consecutiveAuthFailures >= maxFailures && usableCount > 0) {
1416
1486
  target.isAuthDisabled = true;
1417
- target.authDisabledReason = "Token permanently rejected (proactive refresh)";
1418
- } else {
1419
- target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1420
- const maxFailures = getConfig2().max_consecutive_auth_failures;
1421
- if (target.consecutiveAuthFailures >= maxFailures) {
1422
- target.isAuthDisabled = true;
1423
- target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1424
- }
1487
+ target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1425
1488
  }
1426
1489
  });
1427
1490
  } catch {
1428
1491
  debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
1429
1492
  }
1430
1493
  }
1494
+ async clearOpenCodeAuthIfNoAccountsRemain() {
1495
+ const storage = await this.store.load();
1496
+ if (storage.accounts.length > 0) return;
1497
+ await this.client.auth.set({
1498
+ path: { id: providerAuthId },
1499
+ body: getClearedOAuthBody()
1500
+ }).catch(() => {
1501
+ });
1502
+ }
1431
1503
  };
1432
1504
  }
1433
1505
 
@@ -1776,6 +1848,8 @@ var anthropicOAuthAdapter = {
1776
1848
  oauthBetaHeader: "oauth-2025-04-20",
1777
1849
  requestBetaHeader: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
1778
1850
  cliUserAgent: "claude-cli/2.1.2 (external, cli)",
1851
+ cliVersion: "2.1.80",
1852
+ billingSalt: "59cf53e54c78",
1779
1853
  toolPrefix: "mcp_",
1780
1854
  accountStorageFilename: "anthropic-multi-account-accounts.json",
1781
1855
  transform: {
@@ -1808,6 +1882,8 @@ var openAIOAuthAdapter = {
1808
1882
  oauthBetaHeader: "",
1809
1883
  requestBetaHeader: "",
1810
1884
  cliUserAgent: "opencode/1.1.53",
1885
+ cliVersion: "",
1886
+ billingSalt: "",
1811
1887
  toolPrefix: "mcp_",
1812
1888
  accountStorageFilename: "openai-multi-account-accounts.json",
1813
1889
  transform: {
@@ -1824,16 +1900,414 @@ var openAIOAuthAdapter = {
1824
1900
  },
1825
1901
  supported: true
1826
1902
  };
1903
+
1904
+ // src/pool-types.ts
1905
+ import * as v5 from "valibot";
1906
+ var PoolConfigSchema = v5.object({
1907
+ name: v5.string(),
1908
+ baseProvider: v5.string(),
1909
+ members: v5.array(v5.string()),
1910
+ enabled: v5.boolean()
1911
+ });
1912
+ var ChainEntryConfigSchema = v5.object({
1913
+ pool: v5.string(),
1914
+ model: v5.optional(v5.string()),
1915
+ enabled: v5.boolean()
1916
+ });
1917
+ var ChainConfigSchema = v5.object({
1918
+ name: v5.string(),
1919
+ entries: v5.array(ChainEntryConfigSchema),
1920
+ enabled: v5.boolean()
1921
+ });
1922
+ var PoolChainConfigSchema = v5.object({
1923
+ pools: v5.optional(v5.array(PoolConfigSchema), []),
1924
+ chains: v5.optional(v5.array(ChainConfigSchema), [])
1925
+ });
1926
+
1927
+ // src/pool-config-store.ts
1928
+ import { promises as fs6 } from "node:fs";
1929
+ import { randomBytes as randomBytes4 } from "node:crypto";
1930
+ import { dirname as dirname5, join as join7 } from "node:path";
1931
+ import lockfile2 from "proper-lockfile";
1932
+ import * as v6 from "valibot";
1933
+ var POOL_CONFIG_FILENAME = "multiauth-pools.json";
1934
+ var FILE_MODE2 = 384;
1935
+ var LOCK_OPTIONS2 = {
1936
+ stale: 1e4,
1937
+ retries: { retries: 10, minTimeout: 50, maxTimeout: 2e3, factor: 2 }
1938
+ };
1939
+ function createEmptyConfig() {
1940
+ return { pools: [], chains: [] };
1941
+ }
1942
+ function getGlobalConfigPath() {
1943
+ return join7(getConfigDir2(), POOL_CONFIG_FILENAME);
1944
+ }
1945
+ function buildTempPath2(targetPath) {
1946
+ return `${targetPath}.${randomBytes4(8).toString("hex")}.tmp`;
1947
+ }
1948
+ async function resolveConfigPath() {
1949
+ const projectPath = join7(process.cwd(), ".opencode", POOL_CONFIG_FILENAME);
1950
+ try {
1951
+ await fs6.access(projectPath);
1952
+ return projectPath;
1953
+ } catch {
1954
+ }
1955
+ return getGlobalConfigPath();
1956
+ }
1957
+ async function ensureConfigFileExists(targetPath) {
1958
+ await fs6.mkdir(dirname5(targetPath), { recursive: true });
1959
+ const emptyContent = `${JSON.stringify(createEmptyConfig(), null, 2)}
1960
+ `;
1961
+ try {
1962
+ await fs6.writeFile(targetPath, emptyContent, { flag: "wx", mode: FILE_MODE2 });
1963
+ } catch (error) {
1964
+ if (getErrorCode(error) !== "EEXIST") throw error;
1965
+ }
1966
+ }
1967
+ async function writeAtomicText2(targetPath, content) {
1968
+ await fs6.mkdir(dirname5(targetPath), { recursive: true });
1969
+ const tempPath = buildTempPath2(targetPath);
1970
+ try {
1971
+ await fs6.writeFile(tempPath, content, { encoding: "utf-8", mode: FILE_MODE2 });
1972
+ await fs6.chmod(tempPath, FILE_MODE2);
1973
+ await fs6.rename(tempPath, targetPath);
1974
+ await fs6.chmod(targetPath, FILE_MODE2);
1975
+ } catch (error) {
1976
+ try {
1977
+ await fs6.unlink(tempPath);
1978
+ } catch {
1979
+ }
1980
+ throw error;
1981
+ }
1982
+ }
1983
+ async function withConfigLock(fn) {
1984
+ const configPath = await resolveConfigPath();
1985
+ await ensureConfigFileExists(configPath);
1986
+ let release = null;
1987
+ try {
1988
+ release = await lockfile2.lock(configPath, LOCK_OPTIONS2);
1989
+ return await fn(configPath);
1990
+ } finally {
1991
+ if (release) {
1992
+ try {
1993
+ await release();
1994
+ } catch {
1995
+ }
1996
+ }
1997
+ }
1998
+ }
1999
+ function parsePoolChainConfig(content) {
2000
+ let parsed;
2001
+ try {
2002
+ parsed = JSON.parse(content);
2003
+ } catch {
2004
+ return null;
2005
+ }
2006
+ const validation = v6.safeParse(PoolChainConfigSchema, parsed);
2007
+ return validation.success ? validation.output : null;
2008
+ }
2009
+ async function loadPoolChainConfig() {
2010
+ const path = await resolveConfigPath();
2011
+ try {
2012
+ const content = await fs6.readFile(path, "utf-8");
2013
+ return parsePoolChainConfig(content) ?? createEmptyConfig();
2014
+ } catch {
2015
+ return createEmptyConfig();
2016
+ }
2017
+ }
2018
+ async function savePoolChainConfig(config) {
2019
+ await withConfigLock(async (configPath) => {
2020
+ const validation = v6.safeParse(PoolChainConfigSchema, config);
2021
+ if (!validation.success) {
2022
+ throw new Error("Invalid pool/chain config payload");
2023
+ }
2024
+ await writeAtomicText2(configPath, `${JSON.stringify(validation.output, null, 2)}
2025
+ `);
2026
+ });
2027
+ }
2028
+
2029
+ // src/pool-manager.ts
2030
+ var DEFAULT_EXHAUSTED_COOLDOWN_MS = 5 * 60 * 1e3;
2031
+ var PoolManager = class {
2032
+ poolsByName = /* @__PURE__ */ new Map();
2033
+ exhaustedUntilByAccount = /* @__PURE__ */ new Map();
2034
+ exhaustedCooldownMs;
2035
+ constructor(options) {
2036
+ this.exhaustedCooldownMs = options?.exhaustedCooldownMs ?? DEFAULT_EXHAUSTED_COOLDOWN_MS;
2037
+ }
2038
+ loadPools(configs) {
2039
+ this.poolsByName.clear();
2040
+ for (const pool of configs) {
2041
+ this.poolsByName.set(pool.name, pool);
2042
+ }
2043
+ }
2044
+ getPoolForAccount(accountUuid) {
2045
+ for (const pool of this.poolsByName.values()) {
2046
+ if (!pool.enabled) continue;
2047
+ if (pool.members.includes(accountUuid)) return pool;
2048
+ }
2049
+ return null;
2050
+ }
2051
+ getAvailableMembers(pool, accountManager) {
2052
+ if (!pool.enabled) return [];
2053
+ this.clearExpiredExhausted();
2054
+ const accountsByUuid = /* @__PURE__ */ new Map();
2055
+ for (const account of accountManager.getAccounts()) {
2056
+ if (!account.uuid) continue;
2057
+ accountsByUuid.set(account.uuid, account);
2058
+ }
2059
+ return pool.members.filter((accountUuid) => {
2060
+ const account = accountsByUuid.get(accountUuid);
2061
+ if (!account) return false;
2062
+ if (!account.enabled || account.isAuthDisabled) return false;
2063
+ if (this.isExhausted(accountUuid)) return false;
2064
+ if (accountManager.isRateLimited(account)) return false;
2065
+ return true;
2066
+ });
2067
+ }
2068
+ markExhausted(accountUuid) {
2069
+ this.exhaustedUntilByAccount.set(accountUuid, Date.now() + this.exhaustedCooldownMs);
2070
+ }
2071
+ async getNextMember(pool, currentUuid, accountManager) {
2072
+ const availableMembers = this.getAvailableMembers(pool, accountManager);
2073
+ if (availableMembers.length === 0) return null;
2074
+ const excluded = /* @__PURE__ */ new Set();
2075
+ if (currentUuid) excluded.add(currentUuid);
2076
+ const preferred = await this.selectPreferredMember(availableMembers, excluded, accountManager);
2077
+ if (preferred) return preferred;
2078
+ for (const candidate of availableMembers) {
2079
+ if (candidate !== currentUuid) return candidate;
2080
+ }
2081
+ return null;
2082
+ }
2083
+ async buildFailoverPlan(currentAccount, config, accountManager, options) {
2084
+ this.loadPools(config.pools ?? []);
2085
+ if ((config.pools?.length ?? 0) === 0 && (config.chains?.length ?? 0) === 0) {
2086
+ return { candidates: [], skips: [] };
2087
+ }
2088
+ const attemptedAccounts = options?.attemptedAccounts ?? /* @__PURE__ */ new Set();
2089
+ const visitedChainIndexes = options?.visitedChainIndexes ?? /* @__PURE__ */ new Set();
2090
+ const currentUuid = currentAccount?.uuid;
2091
+ const candidates = [];
2092
+ const skips = [];
2093
+ const addedCandidateUuids = /* @__PURE__ */ new Set();
2094
+ const appendPoolCandidates = async (poolName, source, chainIndex) => {
2095
+ const pool = this.poolsByName.get(poolName);
2096
+ if (!pool || !pool.enabled) {
2097
+ skips.push({
2098
+ type: "chain_disabled",
2099
+ poolName,
2100
+ reason: "Pool is missing or disabled"
2101
+ });
2102
+ return;
2103
+ }
2104
+ const available = this.getAvailableMembers(pool, accountManager);
2105
+ if (available.length === 0) {
2106
+ skips.push({
2107
+ type: "pool_exhausted",
2108
+ poolName,
2109
+ reason: "No available members"
2110
+ });
2111
+ return;
2112
+ }
2113
+ const poolExclusions = /* @__PURE__ */ new Set();
2114
+ if (currentUuid) poolExclusions.add(currentUuid);
2115
+ while (poolExclusions.size < available.length + (currentUuid ? 1 : 0)) {
2116
+ const nextMember = await this.selectPreferredMember(available, poolExclusions, accountManager);
2117
+ if (!nextMember) break;
2118
+ poolExclusions.add(nextMember);
2119
+ if (attemptedAccounts.has(nextMember)) {
2120
+ skips.push({
2121
+ type: "account_attempted",
2122
+ poolName,
2123
+ reason: "Already attempted in this cascade",
2124
+ detail: nextMember
2125
+ });
2126
+ continue;
2127
+ }
2128
+ if (addedCandidateUuids.has(nextMember)) continue;
2129
+ candidates.push({
2130
+ poolName,
2131
+ accountUuid: nextMember,
2132
+ source,
2133
+ chainIndex
2134
+ });
2135
+ addedCandidateUuids.add(nextMember);
2136
+ }
2137
+ for (const memberUuid of available) {
2138
+ if (poolExclusions.has(memberUuid)) continue;
2139
+ if (attemptedAccounts.has(memberUuid)) {
2140
+ skips.push({
2141
+ type: "account_attempted",
2142
+ poolName,
2143
+ reason: "Already attempted in this cascade",
2144
+ detail: memberUuid
2145
+ });
2146
+ continue;
2147
+ }
2148
+ if (addedCandidateUuids.has(memberUuid)) continue;
2149
+ candidates.push({
2150
+ poolName,
2151
+ accountUuid: memberUuid,
2152
+ source,
2153
+ chainIndex
2154
+ });
2155
+ addedCandidateUuids.add(memberUuid);
2156
+ }
2157
+ };
2158
+ if (currentUuid) {
2159
+ const currentPool = this.getPoolForAccount(currentUuid);
2160
+ if (currentPool) {
2161
+ await appendPoolCandidates(currentPool.name, "pool");
2162
+ }
2163
+ }
2164
+ let flattenedChainIndex = 0;
2165
+ for (const chain of config.chains ?? []) {
2166
+ if (!chain.enabled) {
2167
+ for (let i = 0; i < chain.entries.length; i++) {
2168
+ skips.push({
2169
+ type: "chain_disabled",
2170
+ poolName: chain.entries[i]?.pool ?? chain.name,
2171
+ reason: `Chain '${chain.name}' is disabled`
2172
+ });
2173
+ flattenedChainIndex += 1;
2174
+ }
2175
+ continue;
2176
+ }
2177
+ for (const entry of chain.entries) {
2178
+ if (visitedChainIndexes.has(flattenedChainIndex)) {
2179
+ skips.push({
2180
+ type: "chain_disabled",
2181
+ poolName: entry.pool,
2182
+ reason: "Chain entry already visited in this cascade",
2183
+ detail: `${flattenedChainIndex}`
2184
+ });
2185
+ flattenedChainIndex += 1;
2186
+ continue;
2187
+ }
2188
+ if (!entry.enabled) {
2189
+ skips.push({
2190
+ type: "chain_disabled",
2191
+ poolName: entry.pool,
2192
+ reason: "Chain entry is disabled",
2193
+ detail: `${flattenedChainIndex}`
2194
+ });
2195
+ flattenedChainIndex += 1;
2196
+ continue;
2197
+ }
2198
+ await appendPoolCandidates(entry.pool, "chain", flattenedChainIndex);
2199
+ flattenedChainIndex += 1;
2200
+ }
2201
+ }
2202
+ return { candidates, skips };
2203
+ }
2204
+ isExhausted(accountUuid) {
2205
+ const exhaustedUntil = this.exhaustedUntilByAccount.get(accountUuid);
2206
+ if (!exhaustedUntil) return false;
2207
+ if (Date.now() >= exhaustedUntil) {
2208
+ this.exhaustedUntilByAccount.delete(accountUuid);
2209
+ return false;
2210
+ }
2211
+ return true;
2212
+ }
2213
+ clearExpiredExhausted() {
2214
+ const now = Date.now();
2215
+ for (const [accountUuid, exhaustedUntil] of this.exhaustedUntilByAccount.entries()) {
2216
+ if (now >= exhaustedUntil) this.exhaustedUntilByAccount.delete(accountUuid);
2217
+ }
2218
+ }
2219
+ async selectPreferredMember(availableMembers, excludedMembers, accountManager) {
2220
+ const availableSet = new Set(availableMembers);
2221
+ const maxAttempts = Math.max(availableMembers.length * 2, 6);
2222
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
2223
+ const selected = await accountManager.selectAccount();
2224
+ if (!selected?.uuid) continue;
2225
+ if (!availableSet.has(selected.uuid)) continue;
2226
+ if (excludedMembers.has(selected.uuid)) continue;
2227
+ return selected.uuid;
2228
+ }
2229
+ for (const memberUuid of availableMembers) {
2230
+ if (!excludedMembers.has(memberUuid)) return memberUuid;
2231
+ }
2232
+ return null;
2233
+ }
2234
+ };
2235
+
2236
+ // src/cascade-state.ts
2237
+ function createCascadeState(prompt, currentAccountUuid) {
2238
+ const attemptedAccounts = /* @__PURE__ */ new Set();
2239
+ if (currentAccountUuid) {
2240
+ attemptedAccounts.add(currentAccountUuid);
2241
+ }
2242
+ return {
2243
+ prompt,
2244
+ attemptedAccounts,
2245
+ visitedChainIndexes: /* @__PURE__ */ new Set()
2246
+ };
2247
+ }
2248
+ var CascadeStateManager = class {
2249
+ suppressNextStartTurn = false;
2250
+ cascadeState = null;
2251
+ startTurn(prompt, currentAccountUuid) {
2252
+ if (this.suppressNextStartTurn) {
2253
+ this.suppressNextStartTurn = false;
2254
+ return this.ensureCascadeState(prompt, currentAccountUuid);
2255
+ }
2256
+ const shouldReset = !this.cascadeState || this.cascadeState.prompt !== prompt;
2257
+ if (shouldReset) {
2258
+ this.cascadeState = createCascadeState(prompt, currentAccountUuid);
2259
+ return this.cascadeState;
2260
+ }
2261
+ return this.ensureCascadeState(prompt, currentAccountUuid);
2262
+ }
2263
+ ensureCascadeState(prompt, currentAccountUuid) {
2264
+ if (!this.cascadeState || this.cascadeState.prompt !== prompt) {
2265
+ this.cascadeState = createCascadeState(prompt, currentAccountUuid);
2266
+ return this.cascadeState;
2267
+ }
2268
+ if (currentAccountUuid) {
2269
+ this.cascadeState.attemptedAccounts.add(currentAccountUuid);
2270
+ }
2271
+ return this.cascadeState;
2272
+ }
2273
+ markAttempted(accountUuid) {
2274
+ if (!this.cascadeState) return;
2275
+ this.cascadeState.attemptedAccounts.add(accountUuid);
2276
+ }
2277
+ markVisitedChainIndex(index) {
2278
+ if (!this.cascadeState) return;
2279
+ this.cascadeState.visitedChainIndexes.add(index);
2280
+ }
2281
+ clearCascadeState() {
2282
+ this.cascadeState = null;
2283
+ this.suppressNextStartTurn = false;
2284
+ }
2285
+ getSnapshot() {
2286
+ if (!this.cascadeState) return null;
2287
+ return {
2288
+ prompt: this.cascadeState.prompt,
2289
+ attemptedAccounts: new Set(this.cascadeState.attemptedAccounts),
2290
+ visitedChainIndexes: new Set(this.cascadeState.visitedChainIndexes)
2291
+ };
2292
+ }
2293
+ };
1827
2294
  export {
1828
2295
  ACCOUNTS_FILENAME,
1829
2296
  ANSI,
1830
2297
  AccountSelectionStrategySchema,
1831
2298
  AccountStorageSchema,
1832
2299
  AccountStore,
2300
+ CascadeStateManager,
2301
+ ChainConfigSchema,
2302
+ ChainEntryConfigSchema,
1833
2303
  CredentialRefreshPatchSchema,
1834
2304
  OAuthCredentialsSchema,
1835
2305
  PluginConfigSchema,
2306
+ PoolChainConfigSchema,
2307
+ PoolConfigSchema,
2308
+ PoolManager,
1836
2309
  StoredAccountSchema,
2310
+ TokenRefreshError,
1837
2311
  UsageLimitEntrySchema,
1838
2312
  UsageLimitsSchema,
1839
2313
  anthropicOAuthAdapter,
@@ -1847,14 +2321,17 @@ export {
1847
2321
  deduplicateAccounts,
1848
2322
  formatWaitTime,
1849
2323
  getAccountLabel,
2324
+ getClearedOAuthBody,
1850
2325
  getConfig,
1851
2326
  getConfigDir2 as getConfigDir,
1852
2327
  getErrorCode,
1853
2328
  initCoreConfig,
1854
2329
  isClaimedByOther,
1855
2330
  isTTY,
2331
+ isTokenRefreshError,
1856
2332
  loadAccounts,
1857
2333
  loadConfig,
2334
+ loadPoolChainConfig,
1858
2335
  migrateFromAuthJson,
1859
2336
  openAIOAuthAdapter,
1860
2337
  parseKey,
@@ -1862,6 +2339,7 @@ export {
1862
2339
  readStorageFromDisk,
1863
2340
  releaseClaim,
1864
2341
  resetConfigCache,
2342
+ savePoolChainConfig,
1865
2343
  select,
1866
2344
  setAccountsFilename,
1867
2345
  setConfigGetter,
@@ -0,0 +1,3 @@
1
+ import type { PoolChainConfig } from "./pool-types";
2
+ export declare function loadPoolChainConfig(): Promise<PoolChainConfig>;
3
+ export declare function savePoolChainConfig(config: PoolChainConfig): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import type { ManagedAccount } from "./types";
2
+ import type { FailoverCandidate, FailoverSkip, PoolChainConfig, PoolConfig } from "./pool-types";
3
+ interface PoolAwareAccountManager {
4
+ getAccounts(): ManagedAccount[];
5
+ isRateLimited(account: ManagedAccount): boolean;
6
+ selectAccount(): Promise<ManagedAccount | null>;
7
+ }
8
+ export interface BuildFailoverPlanOptions {
9
+ attemptedAccounts?: Set<string>;
10
+ visitedChainIndexes?: Set<number>;
11
+ }
12
+ export interface FailoverPlan {
13
+ candidates: FailoverCandidate[];
14
+ skips: FailoverSkip[];
15
+ }
16
+ export declare class PoolManager {
17
+ private poolsByName;
18
+ private exhaustedUntilByAccount;
19
+ private exhaustedCooldownMs;
20
+ constructor(options?: {
21
+ exhaustedCooldownMs?: number;
22
+ });
23
+ loadPools(configs: PoolConfig[]): void;
24
+ getPoolForAccount(accountUuid: string): PoolConfig | null;
25
+ getAvailableMembers(pool: PoolConfig, accountManager: PoolAwareAccountManager): string[];
26
+ markExhausted(accountUuid: string): void;
27
+ getNextMember(pool: PoolConfig, currentUuid: string | undefined, accountManager: PoolAwareAccountManager): Promise<string | null>;
28
+ buildFailoverPlan(currentAccount: Pick<ManagedAccount, "uuid" | "accountId"> | null, config: PoolChainConfig, accountManager: PoolAwareAccountManager, options?: BuildFailoverPlanOptions): Promise<FailoverPlan>;
29
+ private isExhausted;
30
+ private clearExpiredExhausted;
31
+ private selectPreferredMember;
32
+ }
33
+ export {};
@@ -0,0 +1,59 @@
1
+ import * as v from "valibot";
2
+ export declare const PoolConfigSchema: v.ObjectSchema<{
3
+ readonly name: v.StringSchema<undefined>;
4
+ readonly baseProvider: v.StringSchema<undefined>;
5
+ readonly members: v.ArraySchema<v.StringSchema<undefined>, undefined>;
6
+ readonly enabled: v.BooleanSchema<undefined>;
7
+ }, undefined>;
8
+ export declare const ChainEntryConfigSchema: v.ObjectSchema<{
9
+ readonly pool: v.StringSchema<undefined>;
10
+ readonly model: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
11
+ readonly enabled: v.BooleanSchema<undefined>;
12
+ }, undefined>;
13
+ export declare const ChainConfigSchema: v.ObjectSchema<{
14
+ readonly name: v.StringSchema<undefined>;
15
+ readonly entries: v.ArraySchema<v.ObjectSchema<{
16
+ readonly pool: v.StringSchema<undefined>;
17
+ readonly model: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
18
+ readonly enabled: v.BooleanSchema<undefined>;
19
+ }, undefined>, undefined>;
20
+ readonly enabled: v.BooleanSchema<undefined>;
21
+ }, undefined>;
22
+ export declare const PoolChainConfigSchema: v.ObjectSchema<{
23
+ readonly pools: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
24
+ readonly name: v.StringSchema<undefined>;
25
+ readonly baseProvider: v.StringSchema<undefined>;
26
+ readonly members: v.ArraySchema<v.StringSchema<undefined>, undefined>;
27
+ readonly enabled: v.BooleanSchema<undefined>;
28
+ }, undefined>, undefined>, readonly []>;
29
+ readonly chains: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
30
+ readonly name: v.StringSchema<undefined>;
31
+ readonly entries: v.ArraySchema<v.ObjectSchema<{
32
+ readonly pool: v.StringSchema<undefined>;
33
+ readonly model: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
34
+ readonly enabled: v.BooleanSchema<undefined>;
35
+ }, undefined>, undefined>;
36
+ readonly enabled: v.BooleanSchema<undefined>;
37
+ }, undefined>, undefined>, readonly []>;
38
+ }, undefined>;
39
+ export type PoolConfig = v.InferOutput<typeof PoolConfigSchema>;
40
+ export type ChainEntryConfig = v.InferOutput<typeof ChainEntryConfigSchema>;
41
+ export type ChainConfig = v.InferOutput<typeof ChainConfigSchema>;
42
+ export type PoolChainConfig = v.InferOutput<typeof PoolChainConfigSchema>;
43
+ export interface CascadeState {
44
+ prompt: string;
45
+ attemptedAccounts: Set<string>;
46
+ visitedChainIndexes: Set<number>;
47
+ }
48
+ export interface FailoverCandidate {
49
+ poolName: string;
50
+ accountUuid: string;
51
+ source: "pool" | "chain";
52
+ chainIndex?: number;
53
+ }
54
+ export interface FailoverSkip {
55
+ type: "pool_exhausted" | "chain_disabled" | "account_attempted" | "account_unavailable";
56
+ poolName: string;
57
+ reason: string;
58
+ detail?: string;
59
+ }
@@ -1,6 +1,7 @@
1
1
  import { AccountStore } from "./account-store";
2
2
  import type { PluginClient, PluginConfig, StoredAccount, TokenRefreshResult } from "./types";
3
3
  export interface ProactiveRefreshDependencies {
4
+ providerAuthId: string;
4
5
  getConfig: () => PluginConfig;
5
6
  refreshToken: (currentRefreshToken: string, accountId: string, client: PluginClient) => Promise<TokenRefreshResult>;
6
7
  isTokenExpired: (account: Pick<StoredAccount, "accessToken" | "expiresAt">) => boolean;
package/dist/types.d.ts CHANGED
@@ -130,6 +130,12 @@ export type TokenRefreshResult = {
130
130
  permanent: boolean;
131
131
  status?: number;
132
132
  };
133
+ export declare class TokenRefreshError extends Error {
134
+ readonly status?: number;
135
+ readonly permanent: boolean;
136
+ constructor(permanent: boolean, status?: number);
137
+ }
138
+ export declare function isTokenRefreshError(error: unknown): error is TokenRefreshError;
133
139
  export interface ManagedAccount {
134
140
  index: number;
135
141
  uuid?: string;
package/dist/utils.d.ts CHANGED
@@ -7,3 +7,9 @@ export declare function sleep(ms: number): Promise<void>;
7
7
  export declare function showToast(client: PluginClient, message: string, variant: "info" | "warning" | "success" | "error"): Promise<void>;
8
8
  export declare function debugLog(client: PluginClient, message: string, extra?: Record<string, unknown>): void;
9
9
  export declare function createMinimalClient(): PluginClient;
10
+ export declare function getClearedOAuthBody(): {
11
+ type: "oauth";
12
+ refresh: string;
13
+ access: string;
14
+ expires: number;
15
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-multi-account-core",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Shared core for multi-account OpenCode plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",