opencode-multi-account-core 0.2.3 → 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,6 @@ 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]);
1129
1174
  function isAbortError(error) {
1130
1175
  return error instanceof Error && error.name === "AbortError";
1131
1176
  }
@@ -1139,80 +1184,49 @@ function createExecutorForProvider(providerName, dependencies) {
1139
1184
  } = dependencies;
1140
1185
  async function executeWithAccountRotation(manager, runtimeFactory, client, input, init) {
1141
1186
  const maxRetries = Math.max(MIN_MAX_RETRIES, manager.getAccountCount() * RETRIES_PER_ACCOUNT);
1142
- let retries = 0;
1143
1187
  let previousAccountUuid;
1144
- while (true) {
1145
- if (++retries > maxRetries) {
1146
- throw new Error(
1147
- `Exhausted ${maxRetries} retries across all accounts. All attempts failed due to auth errors, rate limits, or token issues.`
1148
- );
1149
- }
1150
- await manager.refresh();
1151
- const account = await resolveAccount(manager, client);
1152
- const accountUuid = account.uuid;
1153
- if (!accountUuid) continue;
1154
- if (previousAccountUuid && accountUuid !== previousAccountUuid && manager.getAccountCount() > 1) {
1155
- void showToast2(client, `Switched to ${getAccountLabel2(account)}`, "info");
1156
- }
1157
- previousAccountUuid = accountUuid;
1158
- let runtime;
1159
- let response;
1160
- try {
1161
- runtime = await runtimeFactory.getRuntime(accountUuid);
1162
- response = await runtime.fetch(input, init);
1163
- } catch (error) {
1164
- if (isAbortError(error)) throw error;
1165
- if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1166
- 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;
1167
1203
  }
1168
- void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1169
- continue;
1204
+ if (retryResponse.status < 500) return retryResponse;
1170
1205
  }
1206
+ return null;
1207
+ }
1208
+ const dispatchResponseStatus = async (account, accountUuid, runtime, response, allow401Retry, from401RefreshRetry) => {
1171
1209
  if (response.status >= 500) {
1172
- let serverResponse = response;
1173
- let networkErrorDuringServerRetry = false;
1174
- let authFailureDuringServerRetry = false;
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);
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);
1179
1219
  try {
1180
- 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);
1181
1223
  } catch (error) {
1182
1224
  if (isAbortError(error)) throw error;
1183
1225
  if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1184
- authFailureDuringServerRetry = true;
1185
- break;
1226
+ return { type: "retryOuter" };
1186
1227
  }
1187
- networkErrorDuringServerRetry = true;
1188
- void showToast2(client, `${getAccountLabel2(account)} network error \u2014 switching`, "warning");
1189
- break;
1228
+ return { type: "retryOuter" };
1190
1229
  }
1191
- if (serverResponse.status < 500) break;
1192
- }
1193
- if (authFailureDuringServerRetry) {
1194
- continue;
1195
- }
1196
- if (networkErrorDuringServerRetry || serverResponse.status >= 500) {
1197
- continue;
1198
- }
1199
- response = serverResponse;
1200
- }
1201
- if (response.status === 401) {
1202
- runtimeFactory.invalidate(accountUuid);
1203
- try {
1204
- const retryRuntime = await runtimeFactory.getRuntime(accountUuid);
1205
- const retryResponse = await retryRuntime.fetch(input, init);
1206
- if (retryResponse.status !== 401) {
1207
- await manager.markSuccess(accountUuid);
1208
- return retryResponse;
1209
- }
1210
- } catch (error) {
1211
- if (isAbortError(error)) throw error;
1212
- if (await handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error)) {
1213
- continue;
1214
- }
1215
- continue;
1216
1230
  }
1217
1231
  await manager.markAuthFailure(accountUuid, { ok: false, permanent: false });
1218
1232
  await manager.refresh();
@@ -1223,7 +1237,7 @@ function createExecutorForProvider(providerName, dependencies) {
1223
1237
  );
1224
1238
  }
1225
1239
  void showToast2(client, `${getAccountLabel2(account)} auth failed \u2014 switching to next account.`, "warning");
1226
- continue;
1240
+ return { type: "retryOuter" };
1227
1241
  }
1228
1242
  if (response.status === 403) {
1229
1243
  const revoked = await isRevokedTokenResponse(response);
@@ -1240,26 +1254,62 @@ function createExecutorForProvider(providerName, dependencies) {
1240
1254
  `All ${providerName} accounts have been revoked or disabled. Re-authenticate with \`opencode auth login\`.`
1241
1255
  );
1242
1256
  }
1243
- continue;
1257
+ return { type: "retryOuter" };
1258
+ }
1259
+ if (from401RefreshRetry) {
1260
+ return { type: "handled", response };
1244
1261
  }
1245
1262
  }
1246
1263
  if (response.status === 429) {
1247
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
+ }
1248
1296
  continue;
1249
1297
  }
1250
1298
  await manager.markSuccess(accountUuid);
1251
- return response;
1299
+ return transition.response;
1252
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
+ );
1253
1304
  }
1254
1305
  async function handleRuntimeFetchFailure(manager, runtimeFactory, client, account, error) {
1255
- const refreshFailureStatus = getRefreshFailureStatus(error);
1256
- if (refreshFailureStatus === void 0) return false;
1306
+ if (!isTokenRefreshError(error)) return false;
1257
1307
  if (!account.uuid) return false;
1258
1308
  const accountUuid = account.uuid;
1259
1309
  runtimeFactory.invalidate(accountUuid);
1260
1310
  await manager.markAuthFailure(accountUuid, {
1261
1311
  ok: false,
1262
- permanent: PERMANENT_AUTH_FAILURE_STATUSES.has(refreshFailureStatus)
1312
+ permanent: error.permanent
1263
1313
  });
1264
1314
  await manager.refresh();
1265
1315
  if (!manager.hasAnyUsableAccount()) {
@@ -1304,13 +1354,6 @@ function createExecutorForProvider(providerName, dependencies) {
1304
1354
  executeWithAccountRotation
1305
1355
  };
1306
1356
  }
1307
- function getRefreshFailureStatus(error) {
1308
- if (!(error instanceof Error)) return void 0;
1309
- const matched = error.message.match(/Token refresh failed:\s*(\d{3})/);
1310
- if (!matched) return void 0;
1311
- const status = Number(matched[1]);
1312
- return Number.isFinite(status) ? status : void 0;
1313
- }
1314
1357
  async function isRevokedTokenResponse(response) {
1315
1358
  try {
1316
1359
  const cloned = response.clone();
@@ -1325,6 +1368,7 @@ async function isRevokedTokenResponse(response) {
1325
1368
  var INITIAL_DELAY_MS = 5e3;
1326
1369
  function createProactiveRefreshQueueForProvider(dependencies) {
1327
1370
  const {
1371
+ providerAuthId,
1328
1372
  getConfig: getConfig2,
1329
1373
  refreshToken,
1330
1374
  isTokenExpired,
@@ -1343,6 +1387,10 @@ function createProactiveRefreshQueueForProvider(dependencies) {
1343
1387
  const config = getConfig2();
1344
1388
  if (!config.proactive_refresh) return;
1345
1389
  this.runToken++;
1390
+ if (this.timeoutHandle) {
1391
+ clearTimeout(this.timeoutHandle);
1392
+ this.timeoutHandle = null;
1393
+ }
1346
1394
  this.scheduleNext(this.runToken, INITIAL_DELAY_MS);
1347
1395
  debugLog2(this.client, "Proactive refresh started", {
1348
1396
  intervalSeconds: config.proactive_refresh_interval_seconds,
@@ -1417,23 +1465,41 @@ function createProactiveRefreshQueueForProvider(dependencies) {
1417
1465
  }
1418
1466
  async persistFailure(account, permanent) {
1419
1467
  try {
1420
- await this.store.mutateAccount(account.uuid, (target) => {
1421
- 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) {
1422
1486
  target.isAuthDisabled = true;
1423
- target.authDisabledReason = "Token permanently rejected (proactive refresh)";
1424
- } else {
1425
- target.consecutiveAuthFailures = (target.consecutiveAuthFailures ?? 0) + 1;
1426
- const maxFailures = getConfig2().max_consecutive_auth_failures;
1427
- if (target.consecutiveAuthFailures >= maxFailures) {
1428
- target.isAuthDisabled = true;
1429
- target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1430
- }
1487
+ target.authDisabledReason = `${maxFailures} consecutive auth failures (proactive refresh)`;
1431
1488
  }
1432
1489
  });
1433
1490
  } catch {
1434
1491
  debugLog2(this.client, `Failed to persist auth failure for ${account.uuid}`);
1435
1492
  }
1436
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
+ }
1437
1503
  };
1438
1504
  }
1439
1505
 
@@ -1782,6 +1848,8 @@ var anthropicOAuthAdapter = {
1782
1848
  oauthBetaHeader: "oauth-2025-04-20",
1783
1849
  requestBetaHeader: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
1784
1850
  cliUserAgent: "claude-cli/2.1.2 (external, cli)",
1851
+ cliVersion: "2.1.80",
1852
+ billingSalt: "59cf53e54c78",
1785
1853
  toolPrefix: "mcp_",
1786
1854
  accountStorageFilename: "anthropic-multi-account-accounts.json",
1787
1855
  transform: {
@@ -1814,6 +1882,8 @@ var openAIOAuthAdapter = {
1814
1882
  oauthBetaHeader: "",
1815
1883
  requestBetaHeader: "",
1816
1884
  cliUserAgent: "opencode/1.1.53",
1885
+ cliVersion: "",
1886
+ billingSalt: "",
1817
1887
  toolPrefix: "mcp_",
1818
1888
  accountStorageFilename: "openai-multi-account-accounts.json",
1819
1889
  transform: {
@@ -1830,16 +1900,414 @@ var openAIOAuthAdapter = {
1830
1900
  },
1831
1901
  supported: true
1832
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
+ };
1833
2294
  export {
1834
2295
  ACCOUNTS_FILENAME,
1835
2296
  ANSI,
1836
2297
  AccountSelectionStrategySchema,
1837
2298
  AccountStorageSchema,
1838
2299
  AccountStore,
2300
+ CascadeStateManager,
2301
+ ChainConfigSchema,
2302
+ ChainEntryConfigSchema,
1839
2303
  CredentialRefreshPatchSchema,
1840
2304
  OAuthCredentialsSchema,
1841
2305
  PluginConfigSchema,
2306
+ PoolChainConfigSchema,
2307
+ PoolConfigSchema,
2308
+ PoolManager,
1842
2309
  StoredAccountSchema,
2310
+ TokenRefreshError,
1843
2311
  UsageLimitEntrySchema,
1844
2312
  UsageLimitsSchema,
1845
2313
  anthropicOAuthAdapter,
@@ -1853,14 +2321,17 @@ export {
1853
2321
  deduplicateAccounts,
1854
2322
  formatWaitTime,
1855
2323
  getAccountLabel,
2324
+ getClearedOAuthBody,
1856
2325
  getConfig,
1857
2326
  getConfigDir2 as getConfigDir,
1858
2327
  getErrorCode,
1859
2328
  initCoreConfig,
1860
2329
  isClaimedByOther,
1861
2330
  isTTY,
2331
+ isTokenRefreshError,
1862
2332
  loadAccounts,
1863
2333
  loadConfig,
2334
+ loadPoolChainConfig,
1864
2335
  migrateFromAuthJson,
1865
2336
  openAIOAuthAdapter,
1866
2337
  parseKey,
@@ -1868,6 +2339,7 @@ export {
1868
2339
  readStorageFromDisk,
1869
2340
  releaseClaim,
1870
2341
  resetConfigCache,
2342
+ savePoolChainConfig,
1871
2343
  select,
1872
2344
  setAccountsFilename,
1873
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.3",
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",