@victor-software-house/pi-multicodex 1.0.3 → 1.0.6

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 kim0
3
+ Copyright (c) 2026 Victor Software House
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  ![MultiCodex](./assets/multicodex.png)
4
4
 
5
- `@victor-software-house/pi-multicodex` is a pi extension for rotating multiple ChatGPT Codex OAuth accounts when using the `openai-codex-responses` API.
5
+ `@victor-software-house/pi-multicodex` is a pi extension that rotates multiple ChatGPT Codex OAuth accounts for the `openai-codex-responses` API.
6
6
 
7
- Current behavior:
7
+ ## What it does
8
8
 
9
- - rotates on quota and rate-limit failures
9
+ - rotates accounts on quota and rate-limit failures
10
10
  - prefers untouched accounts when usage data is available
11
11
  - otherwise prefers the account whose weekly window resets first
12
- - stays focused on Codex only
12
+ - keeps the implementation focused on Codex account rotation
13
13
 
14
14
  ## Install
15
15
 
@@ -21,7 +21,7 @@ Restart `pi` after installation.
21
21
 
22
22
  ## Local development
23
23
 
24
- This repo uses `mise` to pin tool versions and `pnpm` for dependency management.
24
+ This repo uses `mise` to pin tools and `pnpm` for dependency management.
25
25
 
26
26
  ```bash
27
27
  mise install
@@ -51,21 +51,31 @@ pi -e ./index.ts
51
51
  - Select an account manually for the current session.
52
52
  - `/multicodex-status`
53
53
  - Show account state and cached usage information.
54
+ - `/multicodex-footer`
55
+ - Open an interactive panel to configure footer fields and ordering.
54
56
 
55
- ## Status
57
+ ## Project direction
56
58
 
57
- This package is being turned into an independent fork with deliberate breaking changes.
59
+ This project is maintained as its own package and release line.
58
60
 
59
61
  Current direction:
60
62
 
61
63
  - package name: `@victor-software-house/pi-multicodex`
62
- - hard break from previous storage compatibility
63
64
  - Codex-only scope
64
- - independent implementation roadmap tracked in `fork-plan.md`
65
+ - local state stored at `~/.pi/agent/codex-accounts.json`
66
+ - internal logic split into focused modules
67
+ - current roadmap tracked in `ROADMAP.md`
65
68
 
66
- ## Release validation
69
+ Current next step:
70
+
71
+ - add active-account usage visibility in pi for this extension's managed Codex accounts
72
+ - mirror the existing codex usage footer style, including support for displaying both reset countdowns
73
+ - show footer usage only when the selected model uses the `multicodex` provider override
74
+ - show the active account identifier beside the 5h and 7d usage metrics
75
+ - configure footer fields and ordering through an interactive panel
76
+ - refresh the footer from the active managed account without polling aggressively
67
77
 
68
- Local development uses pnpm, but published package output must remain npm-compatible.
78
+ ## Release validation
69
79
 
70
80
  Minimum release checks:
71
81
 
@@ -74,22 +84,34 @@ pnpm check
74
84
  npm pack --dry-run
75
85
  ```
76
86
 
77
- Recommended release flow:
87
+ Release flow:
78
88
 
79
- 1. For the first publish of a brand-new package, publish manually from a trusted local machine.
80
- 2. After the package exists on npm, configure npm trusted publishing for `.github/workflows/publish.yml`.
81
- 3. Publish subsequent releases by pushing a matching `v*` git tag.
89
+ 1. Prepare the release locally.
90
+ 2. Commit the version bump.
91
+ 3. Create and push a matching `v*` tag.
92
+ 4. Let GitHub Actions publish through trusted publishing.
82
93
 
83
- Bootstrap publish:
94
+ Prepare locally:
84
95
 
85
96
  ```bash
86
- npm run publish:dry -- <version>
87
- npm publish --access public --otp=<code>
97
+ npm run release:prepare -- <version>
88
98
  ```
89
99
 
90
- Trusted publishing flow after bootstrap:
100
+ The helper updates `package.json` with `bun pm pkg set` and then runs the release checks.
101
+
102
+ Example:
91
103
 
92
104
  ```bash
105
+ git add package.json
106
+ git commit -m "release: v<version>"
93
107
  git tag v<version>
94
- git push origin v<version>
108
+ git push origin main --tags
95
109
  ```
110
+
111
+ Do not use local `npm publish` for normal releases in this repo.
112
+
113
+ ## Acknowledgment
114
+
115
+ This project descends from earlier MultiCodex work. Thanks to the original creator for the starting point that made this package possible.
116
+
117
+ The active-account usage footer work also draws on ideas from `calesennett/pi-codex-usage`. Thanks to its author for the reference implementation and footer design.
package/abort-utils.ts ADDED
@@ -0,0 +1,24 @@
1
+ export function createLinkedAbortController(
2
+ signal?: AbortSignal,
3
+ ): AbortController {
4
+ const controller = new AbortController();
5
+ if (signal?.aborted) {
6
+ controller.abort();
7
+ return controller;
8
+ }
9
+
10
+ signal?.addEventListener("abort", () => controller.abort(), { once: true });
11
+ return controller;
12
+ }
13
+
14
+ export function createTimeoutController(
15
+ signal: AbortSignal | undefined,
16
+ timeoutMs: number,
17
+ ): { controller: AbortController; clear: () => void } {
18
+ const controller = createLinkedAbortController(signal);
19
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
20
+ return {
21
+ controller,
22
+ clear: () => clearTimeout(timeout),
23
+ };
24
+ }
@@ -0,0 +1,262 @@
1
+ import {
2
+ type OAuthCredentials,
3
+ refreshOpenAICodexToken,
4
+ } from "@mariozechner/pi-ai/oauth";
5
+ import { isAccountAvailable, pickBestAccount } from "./selection";
6
+ import {
7
+ type Account,
8
+ loadStorage,
9
+ type StorageData,
10
+ saveStorage,
11
+ } from "./storage";
12
+ import { type CodexUsageSnapshot, getNextResetAt } from "./usage";
13
+ import { fetchCodexUsage } from "./usage-client";
14
+
15
+ const USAGE_CACHE_TTL_MS = 5 * 60 * 1000;
16
+ const USAGE_REQUEST_TIMEOUT_MS = 10 * 1000;
17
+ const QUOTA_COOLDOWN_MS = 60 * 60 * 1000;
18
+
19
+ type WarningHandler = (message: string) => void;
20
+
21
+ function getErrorMessage(error: unknown): string {
22
+ if (error instanceof Error) return error.message;
23
+ return typeof error === "string" ? error : JSON.stringify(error);
24
+ }
25
+
26
+ export class AccountManager {
27
+ private data: StorageData;
28
+ private usageCache = new Map<string, CodexUsageSnapshot>();
29
+ private warningHandler?: WarningHandler;
30
+ private manualEmail?: string;
31
+
32
+ constructor() {
33
+ this.data = loadStorage();
34
+ }
35
+
36
+ private save(): void {
37
+ saveStorage(this.data);
38
+ }
39
+
40
+ getAccounts(): Account[] {
41
+ return this.data.accounts;
42
+ }
43
+
44
+ getAccount(email: string): Account | undefined {
45
+ return this.data.accounts.find((a) => a.email === email);
46
+ }
47
+
48
+ setWarningHandler(handler?: WarningHandler): void {
49
+ this.warningHandler = handler;
50
+ }
51
+
52
+ addOrUpdateAccount(email: string, creds: OAuthCredentials): void {
53
+ const existing = this.getAccount(email);
54
+ const accountId =
55
+ typeof creds.accountId === "string" ? creds.accountId : undefined;
56
+ if (existing) {
57
+ existing.accessToken = creds.access;
58
+ existing.refreshToken = creds.refresh;
59
+ existing.expiresAt = creds.expires;
60
+ if (accountId) {
61
+ existing.accountId = accountId;
62
+ }
63
+ } else {
64
+ this.data.accounts.push({
65
+ email,
66
+ accessToken: creds.access,
67
+ refreshToken: creds.refresh,
68
+ expiresAt: creds.expires,
69
+ accountId,
70
+ });
71
+ }
72
+ this.setActiveAccount(email);
73
+ this.save();
74
+ }
75
+
76
+ getActiveAccount(): Account | undefined {
77
+ const manual = this.getManualAccount();
78
+ if (manual) return manual;
79
+ if (this.data.activeEmail) {
80
+ return this.getAccount(this.data.activeEmail);
81
+ }
82
+ return this.data.accounts[0];
83
+ }
84
+
85
+ getManualAccount(): Account | undefined {
86
+ if (!this.manualEmail) return undefined;
87
+ const account = this.getAccount(this.manualEmail);
88
+ if (!account) {
89
+ this.manualEmail = undefined;
90
+ return undefined;
91
+ }
92
+ return account;
93
+ }
94
+
95
+ hasManualAccount(): boolean {
96
+ return Boolean(this.manualEmail);
97
+ }
98
+
99
+ setActiveAccount(email: string): void {
100
+ this.data.activeEmail = email;
101
+ this.save();
102
+ }
103
+
104
+ setManualAccount(email: string): void {
105
+ const account = this.getAccount(email);
106
+ if (!account) return;
107
+ this.manualEmail = email;
108
+ account.lastUsed = Date.now();
109
+ }
110
+
111
+ clearManualAccount(): void {
112
+ this.manualEmail = undefined;
113
+ }
114
+
115
+ getAvailableManualAccount(options?: {
116
+ excludeEmails?: Set<string>;
117
+ now?: number;
118
+ }): Account | undefined {
119
+ const manual = this.getManualAccount();
120
+ if (!manual) return undefined;
121
+ const now = options?.now ?? Date.now();
122
+ if (!isAccountAvailable(manual, now)) return undefined;
123
+ if (options?.excludeEmails?.has(manual.email)) return undefined;
124
+ return manual;
125
+ }
126
+
127
+ markExhausted(email: string, until: number): void {
128
+ const account = this.getAccount(email);
129
+ if (account) {
130
+ account.quotaExhaustedUntil = until;
131
+ this.save();
132
+ }
133
+ }
134
+
135
+ getCachedUsage(email: string): CodexUsageSnapshot | undefined {
136
+ return this.usageCache.get(email);
137
+ }
138
+
139
+ async refreshUsageForAccount(
140
+ account: Account,
141
+ options?: { force?: boolean; signal?: AbortSignal },
142
+ ): Promise<CodexUsageSnapshot | undefined> {
143
+ const cached = this.usageCache.get(account.email);
144
+ const now = Date.now();
145
+ if (
146
+ cached &&
147
+ !options?.force &&
148
+ now - cached.fetchedAt < USAGE_CACHE_TTL_MS
149
+ ) {
150
+ return cached;
151
+ }
152
+
153
+ try {
154
+ const token = await this.ensureValidToken(account);
155
+ const usage = await fetchCodexUsage(token, account.accountId, {
156
+ signal: options?.signal,
157
+ timeoutMs: USAGE_REQUEST_TIMEOUT_MS,
158
+ });
159
+ this.usageCache.set(account.email, usage);
160
+ return usage;
161
+ } catch (error) {
162
+ this.warningHandler?.(
163
+ `Multicodex: failed to fetch usage for ${account.email}: ${getErrorMessage(
164
+ error,
165
+ )}`,
166
+ );
167
+ return undefined;
168
+ }
169
+ }
170
+
171
+ async refreshUsageForAllAccounts(options?: {
172
+ force?: boolean;
173
+ signal?: AbortSignal;
174
+ }): Promise<void> {
175
+ const accounts = this.getAccounts();
176
+ await Promise.all(
177
+ accounts.map((account) => this.refreshUsageForAccount(account, options)),
178
+ );
179
+ }
180
+
181
+ async refreshUsageIfStale(
182
+ accounts: Account[],
183
+ options?: { signal?: AbortSignal },
184
+ ): Promise<void> {
185
+ const now = Date.now();
186
+ const stale = accounts.filter((account) => {
187
+ const cached = this.usageCache.get(account.email);
188
+ return !cached || now - cached.fetchedAt >= USAGE_CACHE_TTL_MS;
189
+ });
190
+ if (stale.length === 0) return;
191
+ await Promise.all(
192
+ stale.map((account) =>
193
+ this.refreshUsageForAccount(account, { force: true, ...options }),
194
+ ),
195
+ );
196
+ }
197
+
198
+ async activateBestAccount(options?: {
199
+ excludeEmails?: Set<string>;
200
+ signal?: AbortSignal;
201
+ }): Promise<Account | undefined> {
202
+ const now = Date.now();
203
+ this.clearExpiredExhaustion(now);
204
+ const accounts = this.data.accounts;
205
+ await this.refreshUsageIfStale(accounts, options);
206
+
207
+ const selected = pickBestAccount(accounts, this.usageCache, {
208
+ excludeEmails: options?.excludeEmails,
209
+ now,
210
+ });
211
+ if (selected) {
212
+ this.setActiveAccount(selected.email);
213
+ }
214
+ return selected;
215
+ }
216
+
217
+ async handleQuotaExceeded(
218
+ account: Account,
219
+ options?: { signal?: AbortSignal },
220
+ ): Promise<void> {
221
+ const usage = await this.refreshUsageForAccount(account, {
222
+ force: true,
223
+ signal: options?.signal,
224
+ });
225
+ const now = Date.now();
226
+ const resetAt = getNextResetAt(usage);
227
+ const fallback = now + QUOTA_COOLDOWN_MS;
228
+ const until = resetAt && resetAt > now ? resetAt : fallback;
229
+ this.markExhausted(account.email, until);
230
+ }
231
+
232
+ private clearExpiredExhaustion(now: number): void {
233
+ let changed = false;
234
+ for (const account of this.data.accounts) {
235
+ if (account.quotaExhaustedUntil && account.quotaExhaustedUntil <= now) {
236
+ account.quotaExhaustedUntil = undefined;
237
+ changed = true;
238
+ }
239
+ }
240
+ if (changed) {
241
+ this.save();
242
+ }
243
+ }
244
+
245
+ async ensureValidToken(account: Account): Promise<string> {
246
+ if (Date.now() < account.expiresAt - 5 * 60 * 1000) {
247
+ return account.accessToken;
248
+ }
249
+
250
+ const result = await refreshOpenAICodexToken(account.refreshToken);
251
+ account.accessToken = result.access;
252
+ account.refreshToken = result.refresh;
253
+ account.expiresAt = result.expires;
254
+ const accountId =
255
+ typeof result.accountId === "string" ? result.accountId : undefined;
256
+ if (accountId) {
257
+ account.accountId = accountId;
258
+ }
259
+ this.save();
260
+ return account.accessToken;
261
+ }
262
+ }
package/browser.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ } from "@mariozechner/pi-coding-agent";
5
+
6
+ export async function openLoginInBrowser(
7
+ pi: ExtensionAPI,
8
+ ctx: ExtensionCommandContext,
9
+ url: string,
10
+ ): Promise<void> {
11
+ let command: string;
12
+ let args: string[];
13
+
14
+ if (process.platform === "darwin") {
15
+ command = "open";
16
+ args = [url];
17
+ } else if (process.platform === "win32") {
18
+ command = "cmd";
19
+ args = ["/c", "start", "", url];
20
+ } else {
21
+ command = "xdg-open";
22
+ args = [url];
23
+ }
24
+
25
+ try {
26
+ await pi.exec(command, args);
27
+ } catch (error) {
28
+ ctx.ui.notify(
29
+ "Could not open a browser automatically. Please open the login URL manually.",
30
+ "warning",
31
+ );
32
+ console.warn("[multicodex] Failed to open browser:", error);
33
+ }
34
+ }
package/commands.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { loginOpenAICodex } from "@mariozechner/pi-ai/oauth";
2
+ import type {
3
+ ExtensionAPI,
4
+ ExtensionCommandContext,
5
+ } from "@mariozechner/pi-coding-agent";
6
+ import type { AccountManager } from "./account-manager";
7
+ import { openLoginInBrowser } from "./browser";
8
+ import type { createUsageStatusController } from "./status";
9
+ import { formatResetAt, isUsageUntouched } from "./usage";
10
+
11
+ function getErrorMessage(error: unknown): string {
12
+ if (error instanceof Error) return error.message;
13
+ return typeof error === "string" ? error : JSON.stringify(error);
14
+ }
15
+
16
+ export function registerCommands(
17
+ pi: ExtensionAPI,
18
+ accountManager: AccountManager,
19
+ statusController: ReturnType<typeof createUsageStatusController>,
20
+ ): void {
21
+ pi.registerCommand("multicodex-login", {
22
+ description: "Login to an OpenAI Codex account for the rotation pool",
23
+ handler: async (
24
+ args: string,
25
+ ctx: ExtensionCommandContext,
26
+ ): Promise<void> => {
27
+ const email = args.trim();
28
+ if (!email) {
29
+ ctx.ui.notify(
30
+ "Please provide an email/identifier: /multicodex-login my@email.com",
31
+ "error",
32
+ );
33
+ return;
34
+ }
35
+
36
+ try {
37
+ ctx.ui.notify(
38
+ `Starting login for ${email}... Check your browser.`,
39
+ "info",
40
+ );
41
+
42
+ const creds = await loginOpenAICodex({
43
+ onAuth: ({ url }) => {
44
+ void openLoginInBrowser(pi, ctx, url);
45
+ ctx.ui.notify(`Please open this URL to login: ${url}`, "info");
46
+ console.log(`[multicodex] Login URL: ${url}`);
47
+ },
48
+ onPrompt: async ({ message }) => (await ctx.ui.input(message)) || "",
49
+ });
50
+
51
+ accountManager.addOrUpdateAccount(email, creds);
52
+ ctx.ui.notify(`Successfully logged in as ${email}`, "info");
53
+ } catch (error) {
54
+ ctx.ui.notify(`Login failed: ${getErrorMessage(error)}`, "error");
55
+ }
56
+ },
57
+ });
58
+
59
+ pi.registerCommand("multicodex-use", {
60
+ description: "Switch active Codex account for this session",
61
+ handler: async (
62
+ _args: string,
63
+ ctx: ExtensionCommandContext,
64
+ ): Promise<void> => {
65
+ const accounts = accountManager.getAccounts();
66
+ if (accounts.length === 0) {
67
+ ctx.ui.notify(
68
+ "No accounts logged in. Use /multicodex-login first.",
69
+ "warning",
70
+ );
71
+ return;
72
+ }
73
+
74
+ const options = accounts.map(
75
+ (account) =>
76
+ account.email +
77
+ (account.quotaExhaustedUntil &&
78
+ account.quotaExhaustedUntil > Date.now()
79
+ ? " (Quota)"
80
+ : ""),
81
+ );
82
+ const selected = await ctx.ui.select("Select Account", options);
83
+ if (!selected) return;
84
+
85
+ const email = selected.split(" ")[0];
86
+ accountManager.setManualAccount(email);
87
+ ctx.ui.notify(`Switched to ${email}`, "info");
88
+ },
89
+ });
90
+
91
+ pi.registerCommand("multicodex-status", {
92
+ description: "Show all Codex accounts and active status",
93
+ handler: async (
94
+ _args: string,
95
+ ctx: ExtensionCommandContext,
96
+ ): Promise<void> => {
97
+ await accountManager.refreshUsageForAllAccounts();
98
+ const accounts = accountManager.getAccounts();
99
+ if (accounts.length === 0) {
100
+ ctx.ui.notify(
101
+ "No accounts logged in. Use /multicodex-login first.",
102
+ "warning",
103
+ );
104
+ return;
105
+ }
106
+
107
+ const active = accountManager.getActiveAccount();
108
+ const options = accounts.map((account) => {
109
+ const usage = accountManager.getCachedUsage(account.email);
110
+ const isActive = active?.email === account.email;
111
+ const quotaHit =
112
+ account.quotaExhaustedUntil &&
113
+ account.quotaExhaustedUntil > Date.now();
114
+ const untouched = isUsageUntouched(usage) ? "untouched" : null;
115
+ const tags = [
116
+ isActive ? "active" : null,
117
+ quotaHit ? "quota" : null,
118
+ untouched,
119
+ ]
120
+ .filter(Boolean)
121
+ .join(", ");
122
+ const suffix = tags ? ` (${tags})` : "";
123
+ const primaryUsed = usage?.primary?.usedPercent;
124
+ const secondaryUsed = usage?.secondary?.usedPercent;
125
+ const primaryReset = usage?.primary?.resetAt;
126
+ const secondaryReset = usage?.secondary?.resetAt;
127
+ const primaryLabel =
128
+ primaryUsed === undefined ? "unknown" : `${Math.round(primaryUsed)}%`;
129
+ const secondaryLabel =
130
+ secondaryUsed === undefined
131
+ ? "unknown"
132
+ : `${Math.round(secondaryUsed)}%`;
133
+ const usageSummary = `5h ${primaryLabel} reset:${formatResetAt(primaryReset)} | weekly ${secondaryLabel} reset:${formatResetAt(secondaryReset)}`;
134
+ return `${isActive ? "•" : " "} ${account.email}${suffix} - ${usageSummary}`;
135
+ });
136
+
137
+ await ctx.ui.select("MultiCodex Accounts", options);
138
+ },
139
+ });
140
+
141
+ pi.registerCommand("multicodex-footer", {
142
+ description: "Configure the MultiCodex usage footer",
143
+ handler: async (
144
+ _args: string,
145
+ ctx: ExtensionCommandContext,
146
+ ): Promise<void> => {
147
+ await statusController.openPreferencesPanel(ctx);
148
+ },
149
+ });
150
+ }
package/extension.ts ADDED
@@ -0,0 +1,63 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@mariozechner/pi-coding-agent";
5
+ import { AccountManager } from "./account-manager";
6
+ import { registerCommands } from "./commands";
7
+ import { handleNewSessionSwitch, handleSessionStart } from "./hooks";
8
+ import { buildMulticodexProviderConfig, PROVIDER_ID } from "./provider";
9
+ import { createUsageStatusController } from "./status";
10
+
11
+ export default function multicodexExtension(pi: ExtensionAPI) {
12
+ const accountManager = new AccountManager();
13
+ const statusController = createUsageStatusController(accountManager);
14
+ let lastContext: ExtensionContext | undefined;
15
+
16
+ accountManager.setWarningHandler((message) => {
17
+ if (lastContext) {
18
+ lastContext.ui.notify(message, "warning");
19
+ }
20
+ });
21
+
22
+ pi.registerProvider(
23
+ PROVIDER_ID,
24
+ buildMulticodexProviderConfig(accountManager),
25
+ );
26
+
27
+ registerCommands(pi, accountManager, statusController);
28
+
29
+ pi.on("session_start", (_event: unknown, ctx: ExtensionContext) => {
30
+ lastContext = ctx;
31
+ handleSessionStart(accountManager);
32
+ statusController.startAutoRefresh();
33
+ void (async () => {
34
+ await statusController.loadPreferences(ctx);
35
+ await statusController.refreshFor(ctx);
36
+ })();
37
+ });
38
+
39
+ pi.on(
40
+ "session_switch",
41
+ (event: { reason?: string }, ctx: ExtensionContext) => {
42
+ lastContext = ctx;
43
+ if (event.reason === "new") {
44
+ handleNewSessionSwitch(accountManager);
45
+ }
46
+ void statusController.refreshFor(ctx);
47
+ },
48
+ );
49
+
50
+ pi.on("turn_end", (_event: unknown, ctx: ExtensionContext) => {
51
+ lastContext = ctx;
52
+ void statusController.refreshFor(ctx);
53
+ });
54
+
55
+ pi.on("model_select", (_event: unknown, ctx: ExtensionContext) => {
56
+ lastContext = ctx;
57
+ void statusController.refreshFor(ctx);
58
+ });
59
+
60
+ pi.on("session_shutdown", (_event: unknown, ctx: ExtensionContext) => {
61
+ statusController.stopAutoRefresh(ctx);
62
+ });
63
+ }
package/hooks.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { AccountManager } from "./account-manager";
2
+
3
+ async function refreshAndActivateBestAccount(
4
+ accountManager: AccountManager,
5
+ ): Promise<void> {
6
+ await accountManager.refreshUsageForAllAccounts({ force: true });
7
+ const manual = accountManager.getAvailableManualAccount();
8
+ if (manual) return;
9
+ if (accountManager.hasManualAccount()) {
10
+ accountManager.clearManualAccount();
11
+ }
12
+ await accountManager.activateBestAccount();
13
+ }
14
+
15
+ export function handleSessionStart(accountManager: AccountManager): void {
16
+ if (accountManager.getAccounts().length === 0) return;
17
+ void refreshAndActivateBestAccount(accountManager);
18
+ }
19
+
20
+ export function handleNewSessionSwitch(accountManager: AccountManager): void {
21
+ void refreshAndActivateBestAccount(accountManager);
22
+ }