@victor-software-house/pi-multicodex 1.0.11 → 2.0.0
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/README.md +97 -36
- package/account-manager.ts +34 -0
- package/commands.ts +570 -91
- package/package.json +11 -3
- package/stream-wrapper.ts +1 -1
package/README.md
CHANGED
|
@@ -45,15 +45,55 @@ Run the extension directly during local development:
|
|
|
45
45
|
pi -e ./index.ts
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
48
|
+
## Command family
|
|
49
|
+
|
|
50
|
+
The extension now uses one command family:
|
|
51
|
+
|
|
52
|
+
- `/multicodex`
|
|
53
|
+
- open the main interactive UI
|
|
54
|
+
- `/multicodex show`
|
|
55
|
+
- show managed account status and cached usage
|
|
56
|
+
- `/multicodex use [identifier]`
|
|
57
|
+
- with an identifier, activate existing auth or trigger login
|
|
58
|
+
- with no identifier, open the account picker
|
|
59
|
+
- in the picker, `Backspace` removes the highlighted account after confirmation
|
|
60
|
+
- `/multicodex footer`
|
|
61
|
+
- open footer settings in interactive mode
|
|
62
|
+
- show footer settings summary in non-interactive mode
|
|
63
|
+
- `/multicodex rotation`
|
|
64
|
+
- show current hard-coded rotation policy
|
|
65
|
+
- `/multicodex verify`
|
|
66
|
+
- verify writable local paths and report runtime summary
|
|
67
|
+
- `/multicodex path`
|
|
68
|
+
- show storage and settings file paths
|
|
69
|
+
- `/multicodex reset [manual|quota|all]`
|
|
70
|
+
- reset manual override state, quota cooldown state, or both
|
|
71
|
+
- `/multicodex help`
|
|
72
|
+
- print compact usage text
|
|
73
|
+
|
|
74
|
+
Dynamic autocomplete is available for subcommands and for `/multicodex use <identifier>`.
|
|
75
|
+
|
|
76
|
+
## Architecture overview
|
|
77
|
+
|
|
78
|
+
The implementation is currently organized around these modules:
|
|
79
|
+
|
|
80
|
+
- `provider.ts`
|
|
81
|
+
- overrides the normal `openai-codex` provider path
|
|
82
|
+
- mirrors Codex models and installs the managed stream wrapper
|
|
83
|
+
- `stream-wrapper.ts`
|
|
84
|
+
- account selection, retry, and quota-rotation path during streaming
|
|
85
|
+
- `account-manager.ts`
|
|
86
|
+
- managed account storage, token refresh, usage cache, activation logic, and auth import sync
|
|
87
|
+
- `auth.ts`
|
|
88
|
+
- reads pi's `~/.pi/agent/auth.json` and extracts importable `openai-codex` OAuth state
|
|
89
|
+
- `status.ts`
|
|
90
|
+
- footer rendering, footer settings persistence, footer settings panel, and footer status refresh logic
|
|
91
|
+
- `commands.ts`
|
|
92
|
+
- `/multicodex` command-family routing, autocomplete, and account-selection flows
|
|
93
|
+
- `hooks.ts`
|
|
94
|
+
- session-start and session-switch refresh behavior
|
|
95
|
+
- `storage.ts`
|
|
96
|
+
- persisted account state in `~/.pi/agent/codex-accounts.json`
|
|
57
97
|
|
|
58
98
|
## Project direction
|
|
59
99
|
|
|
@@ -63,15 +103,17 @@ Current direction:
|
|
|
63
103
|
|
|
64
104
|
- package name: `@victor-software-house/pi-multicodex`
|
|
65
105
|
- Codex-only scope
|
|
66
|
-
- local state stored at `~/.pi/agent/codex-accounts.json`
|
|
67
|
-
-
|
|
106
|
+
- local account state stored at `~/.pi/agent/codex-accounts.json`
|
|
107
|
+
- footer and future extension settings stored under `pi-multicodex` in `~/.pi/agent/settings.json`
|
|
108
|
+
- internal logic split into focused modules today, with a broader shared controller planned next
|
|
68
109
|
- current roadmap tracked in `ROADMAP.md`
|
|
69
110
|
|
|
70
|
-
Current next
|
|
111
|
+
Current next milestones:
|
|
71
112
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
113
|
+
1. Persist footer settings immediately instead of waiting for panel close.
|
|
114
|
+
2. Add configurable rotation settings and document the rotation behavior contract.
|
|
115
|
+
3. Broaden the current footer controller into a shared MultiCodex controller.
|
|
116
|
+
4. Improve imported-account labels by deriving email identity safely when possible.
|
|
75
117
|
|
|
76
118
|
## Behavior contract
|
|
77
119
|
|
|
@@ -79,7 +121,7 @@ The current runtime behavior is:
|
|
|
79
121
|
|
|
80
122
|
### Account selection priority
|
|
81
123
|
|
|
82
|
-
1. Use the manual account selected with `/multicodex
|
|
124
|
+
1. Use the manual account selected with `/multicodex use` when it is still available.
|
|
83
125
|
2. Otherwise clear the stale manual override and select the best available managed account.
|
|
84
126
|
3. Best-account selection prefers:
|
|
85
127
|
- untouched accounts with usage data
|
|
@@ -96,13 +138,13 @@ The current runtime behavior is:
|
|
|
96
138
|
### Retry policy
|
|
97
139
|
|
|
98
140
|
- MultiCodex retries account rotation up to 5 times for a single request.
|
|
99
|
-
- Retries only happen for quota
|
|
141
|
+
- Retries only happen for quota and rate-limit style failures that occur before output is forwarded.
|
|
100
142
|
- Once output has started streaming, the original error is surfaced instead of rotating.
|
|
101
143
|
|
|
102
144
|
### Manual override behavior
|
|
103
145
|
|
|
104
|
-
- `/multicodex
|
|
105
|
-
- `/multicodex
|
|
146
|
+
- `/multicodex use <identifier>` sets the manual account override immediately.
|
|
147
|
+
- `/multicodex use` with no argument opens the account picker and sets the selected manual override.
|
|
106
148
|
- Manual override is session-local state.
|
|
107
149
|
- Manual override clears automatically when the selected account is no longer available or when it hits quota during rotation.
|
|
108
150
|
|
|
@@ -132,38 +174,57 @@ pnpm check
|
|
|
132
174
|
npm pack --dry-run
|
|
133
175
|
```
|
|
134
176
|
|
|
135
|
-
Release
|
|
177
|
+
## Release process
|
|
178
|
+
|
|
179
|
+
This repository uses `semantic-release` with npm trusted publishing.
|
|
180
|
+
|
|
181
|
+
Maintainer flow:
|
|
182
|
+
|
|
183
|
+
1. Write Conventional Commits.
|
|
184
|
+
2. The local `commit-msg` hook validates commit messages with Lefthook + commitlint.
|
|
185
|
+
3. CI validates commit messages again and runs release checks.
|
|
186
|
+
4. Merge to `main`.
|
|
187
|
+
5. GitHub Actions runs `semantic-release` from `.github/workflows/publish.yml`.
|
|
188
|
+
6. `semantic-release` computes the next version, creates the git tag and GitHub release, updates `package.json` and `CHANGELOG.md`, and publishes to npm through trusted publishing.
|
|
136
189
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
190
|
+
Local verification:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pnpm check
|
|
194
|
+
npm pack --dry-run
|
|
195
|
+
pnpm release:dry
|
|
196
|
+
```
|
|
141
197
|
|
|
142
198
|
Local push protection:
|
|
143
199
|
|
|
144
200
|
- `lefthook` runs `mise run pre-push`
|
|
145
|
-
- the `pre-push` mise task runs the same core validations as
|
|
201
|
+
- the `pre-push` mise task runs the same core validations as CI:
|
|
146
202
|
- `pnpm check`
|
|
147
203
|
- `npm pack --dry-run`
|
|
148
204
|
|
|
149
|
-
|
|
205
|
+
Do not use local `npm publish` for normal releases in this repo.
|
|
150
206
|
|
|
151
|
-
|
|
152
|
-
npm run release:prepare -- <version>
|
|
153
|
-
```
|
|
207
|
+
## npm trusted publishing setup
|
|
154
208
|
|
|
155
|
-
|
|
209
|
+
npm-side setup is required in addition to the workflow.
|
|
156
210
|
|
|
157
|
-
|
|
211
|
+
Trusted publisher mapping:
|
|
212
|
+
|
|
213
|
+
- package: `@victor-software-house/pi-multicodex`
|
|
214
|
+
- repository: `victor-founder/pi-multicodex`
|
|
215
|
+
- workflow file: `.github/workflows/publish.yml`
|
|
216
|
+
|
|
217
|
+
Useful commands:
|
|
158
218
|
|
|
159
219
|
```bash
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
git tag v<version>
|
|
163
|
-
git push origin main --tags
|
|
220
|
+
npm trust list @victor-software-house/pi-multicodex
|
|
221
|
+
script -q /dev/null bash -lc 'npm trust github @victor-software-house/pi-multicodex --repository victor-founder/pi-multicodex --file publish.yml --yes'
|
|
164
222
|
```
|
|
165
223
|
|
|
166
|
-
|
|
224
|
+
## Related docs
|
|
225
|
+
|
|
226
|
+
- `ROADMAP.md` for planned milestones and acceptance criteria
|
|
227
|
+
- `AGENTS.md` for repository-specific agent guidance
|
|
167
228
|
|
|
168
229
|
## Acknowledgment
|
|
169
230
|
|
package/account-manager.ts
CHANGED
|
@@ -195,6 +195,40 @@ export class AccountManager {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
clearAllQuotaExhaustion(): number {
|
|
199
|
+
let cleared = 0;
|
|
200
|
+
for (const account of this.data.accounts) {
|
|
201
|
+
if (account.quotaExhaustedUntil) {
|
|
202
|
+
account.quotaExhaustedUntil = undefined;
|
|
203
|
+
cleared += 1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (cleared > 0) {
|
|
207
|
+
this.save();
|
|
208
|
+
this.notifyStateChanged();
|
|
209
|
+
}
|
|
210
|
+
return cleared;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
removeAccount(email: string): boolean {
|
|
214
|
+
const index = this.data.accounts.findIndex(
|
|
215
|
+
(account) => account.email === email,
|
|
216
|
+
);
|
|
217
|
+
if (index < 0) return false;
|
|
218
|
+
|
|
219
|
+
this.data.accounts.splice(index, 1);
|
|
220
|
+
this.usageCache.delete(email);
|
|
221
|
+
if (this.manualEmail === email) {
|
|
222
|
+
this.manualEmail = undefined;
|
|
223
|
+
}
|
|
224
|
+
if (this.data.activeEmail === email) {
|
|
225
|
+
this.data.activeEmail = this.data.accounts[0]?.email;
|
|
226
|
+
}
|
|
227
|
+
this.save();
|
|
228
|
+
this.notifyStateChanged();
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
198
232
|
getCachedUsage(email: string): CodexUsageSnapshot | undefined {
|
|
199
233
|
return this.usageCache.get(email);
|
|
200
234
|
}
|
package/commands.ts
CHANGED
|
@@ -1,18 +1,181 @@
|
|
|
1
|
+
import { promises as fs, constants as fsConstants } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { loginOpenAICodex } from "@mariozechner/pi-ai/oauth";
|
|
2
5
|
import type {
|
|
3
6
|
ExtensionAPI,
|
|
4
7
|
ExtensionCommandContext,
|
|
5
8
|
} from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { getSelectListTheme } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
type AutocompleteItem,
|
|
12
|
+
Container,
|
|
13
|
+
Key,
|
|
14
|
+
matchesKey,
|
|
15
|
+
SelectList,
|
|
16
|
+
Text,
|
|
17
|
+
} from "@mariozechner/pi-tui";
|
|
6
18
|
import type { AccountManager } from "./account-manager";
|
|
7
19
|
import { openLoginInBrowser } from "./browser";
|
|
8
20
|
import type { createUsageStatusController } from "./status";
|
|
21
|
+
import { STORAGE_FILE } from "./storage";
|
|
9
22
|
import { formatResetAt, isUsageUntouched } from "./usage";
|
|
10
23
|
|
|
24
|
+
const SETTINGS_FILE = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
25
|
+
const NO_ACCOUNTS_MESSAGE =
|
|
26
|
+
"No managed accounts found. Use /multicodex use <identifier> first.";
|
|
27
|
+
const HELP_TEXT =
|
|
28
|
+
"Usage: /multicodex [show|use [identifier]|footer|rotation|verify|path|reset [manual|quota|all]|help]";
|
|
29
|
+
const SUBCOMMANDS = [
|
|
30
|
+
"show",
|
|
31
|
+
"use",
|
|
32
|
+
"footer",
|
|
33
|
+
"rotation",
|
|
34
|
+
"verify",
|
|
35
|
+
"path",
|
|
36
|
+
"reset",
|
|
37
|
+
"help",
|
|
38
|
+
] as const;
|
|
39
|
+
const RESET_TARGETS = ["manual", "quota", "all"] as const;
|
|
40
|
+
|
|
41
|
+
type Subcommand = (typeof SUBCOMMANDS)[number];
|
|
42
|
+
type ResetTarget = (typeof RESET_TARGETS)[number];
|
|
43
|
+
|
|
44
|
+
type AccountPanelResult =
|
|
45
|
+
| { action: "select"; email: string }
|
|
46
|
+
| { action: "remove"; email: string }
|
|
47
|
+
| undefined;
|
|
48
|
+
|
|
11
49
|
function getErrorMessage(error: unknown): string {
|
|
12
50
|
if (error instanceof Error) return error.message;
|
|
13
51
|
return typeof error === "string" ? error : JSON.stringify(error);
|
|
14
52
|
}
|
|
15
53
|
|
|
54
|
+
function toAutocompleteItems(values: readonly string[]): AutocompleteItem[] {
|
|
55
|
+
return values.map((value) => ({ value, label: value }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseCommandArgs(args: string): {
|
|
59
|
+
subcommand: string | undefined;
|
|
60
|
+
rest: string;
|
|
61
|
+
} {
|
|
62
|
+
const trimmed = args.trim();
|
|
63
|
+
if (!trimmed) {
|
|
64
|
+
return { subcommand: undefined, rest: "" };
|
|
65
|
+
}
|
|
66
|
+
const firstSpaceIndex = trimmed.indexOf(" ");
|
|
67
|
+
if (firstSpaceIndex < 0) {
|
|
68
|
+
return { subcommand: trimmed.toLowerCase(), rest: "" };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
subcommand: trimmed.slice(0, firstSpaceIndex).toLowerCase(),
|
|
72
|
+
rest: trimmed.slice(firstSpaceIndex + 1).trim(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isSubcommand(value: string): value is Subcommand {
|
|
77
|
+
return SUBCOMMANDS.some((subcommand) => subcommand === value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseResetTarget(value: string): ResetTarget | undefined {
|
|
81
|
+
if (value === "manual" || value === "quota" || value === "all") {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getAccountLabel(email: string, quotaExhaustedUntil?: number): string {
|
|
88
|
+
if (!quotaExhaustedUntil || quotaExhaustedUntil <= Date.now()) {
|
|
89
|
+
return email;
|
|
90
|
+
}
|
|
91
|
+
return `${email} (Quota)`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatAccountStatusLine(
|
|
95
|
+
accountManager: AccountManager,
|
|
96
|
+
email: string,
|
|
97
|
+
): string {
|
|
98
|
+
const account = accountManager.getAccount(email);
|
|
99
|
+
if (!account) return email;
|
|
100
|
+
const usage = accountManager.getCachedUsage(account.email);
|
|
101
|
+
const active = accountManager.getActiveAccount();
|
|
102
|
+
const manual = accountManager.getManualAccount();
|
|
103
|
+
const quotaHit =
|
|
104
|
+
account.quotaExhaustedUntil && account.quotaExhaustedUntil > Date.now();
|
|
105
|
+
const untouched = isUsageUntouched(usage) ? "untouched" : null;
|
|
106
|
+
const imported = account.importSource ? "imported" : null;
|
|
107
|
+
const tags = [
|
|
108
|
+
active?.email === account.email ? "active" : null,
|
|
109
|
+
manual?.email === account.email ? "manual" : null,
|
|
110
|
+
quotaHit ? "quota" : null,
|
|
111
|
+
untouched,
|
|
112
|
+
imported,
|
|
113
|
+
]
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
.join(", ");
|
|
116
|
+
const suffix = tags ? ` (${tags})` : "";
|
|
117
|
+
const primaryUsed = usage?.primary?.usedPercent;
|
|
118
|
+
const secondaryUsed = usage?.secondary?.usedPercent;
|
|
119
|
+
const primaryReset = usage?.primary?.resetAt;
|
|
120
|
+
const secondaryReset = usage?.secondary?.resetAt;
|
|
121
|
+
const primaryLabel =
|
|
122
|
+
primaryUsed === undefined ? "unknown" : `${Math.round(primaryUsed)}%`;
|
|
123
|
+
const secondaryLabel =
|
|
124
|
+
secondaryUsed === undefined ? "unknown" : `${Math.round(secondaryUsed)}%`;
|
|
125
|
+
const usageSummary = `5h ${primaryLabel} reset:${formatResetAt(primaryReset)} | weekly ${secondaryLabel} reset:${formatResetAt(secondaryReset)}`;
|
|
126
|
+
return `${account.email}${suffix} - ${usageSummary}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getSubcommandCompletions(prefix: string): AutocompleteItem[] | null {
|
|
130
|
+
const matches = SUBCOMMANDS.filter((value) => value.startsWith(prefix));
|
|
131
|
+
return matches.length > 0 ? toAutocompleteItems(matches) : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getUseCompletions(
|
|
135
|
+
prefix: string,
|
|
136
|
+
accountManager: AccountManager,
|
|
137
|
+
): AutocompleteItem[] | null {
|
|
138
|
+
const matches = accountManager
|
|
139
|
+
.getAccounts()
|
|
140
|
+
.map((account) => account.email)
|
|
141
|
+
.filter((value) => value.startsWith(prefix));
|
|
142
|
+
if (matches.length === 0) return null;
|
|
143
|
+
return matches.map((value) => ({ value: `use ${value}`, label: value }));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getResetCompletions(prefix: string): AutocompleteItem[] | null {
|
|
147
|
+
const matches = RESET_TARGETS.filter((value) => value.startsWith(prefix));
|
|
148
|
+
if (matches.length === 0) return null;
|
|
149
|
+
return matches.map((value) => ({ value: `reset ${value}`, label: value }));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getCommandCompletions(
|
|
153
|
+
argumentPrefix: string,
|
|
154
|
+
accountManager: AccountManager,
|
|
155
|
+
): AutocompleteItem[] | null {
|
|
156
|
+
const trimmedStart = argumentPrefix.trimStart();
|
|
157
|
+
if (!trimmedStart) {
|
|
158
|
+
return toAutocompleteItems(SUBCOMMANDS);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const firstSpaceIndex = trimmedStart.indexOf(" ");
|
|
162
|
+
if (firstSpaceIndex < 0) {
|
|
163
|
+
return getSubcommandCompletions(trimmedStart.toLowerCase());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const subcommand = trimmedStart.slice(0, firstSpaceIndex).toLowerCase();
|
|
167
|
+
const rest = trimmedStart.slice(firstSpaceIndex + 1).trimStart();
|
|
168
|
+
|
|
169
|
+
if (subcommand === "use") {
|
|
170
|
+
return getUseCompletions(rest, accountManager);
|
|
171
|
+
}
|
|
172
|
+
if (subcommand === "reset") {
|
|
173
|
+
return getResetCompletions(rest);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
16
179
|
async function loginAndActivateAccount(
|
|
17
180
|
pi: ExtensionAPI,
|
|
18
181
|
ctx: ExtensionCommandContext,
|
|
@@ -68,113 +231,429 @@ async function useOrLoginAccount(
|
|
|
68
231
|
await loginAndActivateAccount(pi, ctx, accountManager, identifier);
|
|
69
232
|
}
|
|
70
233
|
|
|
234
|
+
async function openAccountSelectionPanel(
|
|
235
|
+
ctx: ExtensionCommandContext,
|
|
236
|
+
accountManager: AccountManager,
|
|
237
|
+
): Promise<AccountPanelResult> {
|
|
238
|
+
const accounts = accountManager.getAccounts();
|
|
239
|
+
const items = accounts.map((account) => ({
|
|
240
|
+
value: account.email,
|
|
241
|
+
label: getAccountLabel(account.email, account.quotaExhaustedUntil),
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
return ctx.ui.custom<AccountPanelResult>((_tui, theme, _kb, done) => {
|
|
245
|
+
const container = new Container();
|
|
246
|
+
container.addChild(
|
|
247
|
+
new Text(theme.fg("accent", theme.bold("Select Account")), 1, 0),
|
|
248
|
+
);
|
|
249
|
+
container.addChild(
|
|
250
|
+
new Text(
|
|
251
|
+
theme.fg("dim", "Enter: use Backspace: remove account Esc: cancel"),
|
|
252
|
+
1,
|
|
253
|
+
0,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const selectList = new SelectList(items, 10, getSelectListTheme());
|
|
258
|
+
selectList.onSelect = (item) => {
|
|
259
|
+
done({ action: "select", email: item.value });
|
|
260
|
+
};
|
|
261
|
+
selectList.onCancel = () => done(undefined);
|
|
262
|
+
container.addChild(selectList);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
render: (width: number) => container.render(width),
|
|
266
|
+
invalidate: () => container.invalidate(),
|
|
267
|
+
handleInput: (data: string) => {
|
|
268
|
+
if (matchesKey(data, Key.backspace)) {
|
|
269
|
+
const selected = selectList.getSelectedItem();
|
|
270
|
+
if (selected) {
|
|
271
|
+
done({ action: "remove", email: selected.value });
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
selectList.handleInput(data);
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function openAccountSelectionFlow(
|
|
282
|
+
ctx: ExtensionCommandContext,
|
|
283
|
+
accountManager: AccountManager,
|
|
284
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
285
|
+
): Promise<void> {
|
|
286
|
+
while (true) {
|
|
287
|
+
const accounts = accountManager.getAccounts();
|
|
288
|
+
if (accounts.length === 0) {
|
|
289
|
+
ctx.ui.notify(NO_ACCOUNTS_MESSAGE, "warning");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const result = await openAccountSelectionPanel(ctx, accountManager);
|
|
294
|
+
if (!result) return;
|
|
295
|
+
|
|
296
|
+
if (result.action === "select") {
|
|
297
|
+
accountManager.setManualAccount(result.email);
|
|
298
|
+
ctx.ui.notify(`Now using ${result.email}`, "info");
|
|
299
|
+
await statusController.refreshFor(ctx);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const accountToRemove = accountManager.getAccount(result.email);
|
|
304
|
+
if (!accountToRemove) continue;
|
|
305
|
+
|
|
306
|
+
const active = accountManager.getActiveAccount();
|
|
307
|
+
const isActive = active?.email === result.email;
|
|
308
|
+
const message = isActive
|
|
309
|
+
? `Remove ${result.email}? This account is currently active and MultiCodex will switch to another account.`
|
|
310
|
+
: `Remove ${result.email}?`;
|
|
311
|
+
const confirmed = await ctx.ui.confirm("Remove account", message);
|
|
312
|
+
if (!confirmed) continue;
|
|
313
|
+
|
|
314
|
+
const removed = accountManager.removeAccount(result.email);
|
|
315
|
+
if (!removed) continue;
|
|
316
|
+
|
|
317
|
+
ctx.ui.notify(`Removed ${result.email}`, "info");
|
|
318
|
+
await statusController.refreshFor(ctx);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function runShowSubcommand(
|
|
323
|
+
ctx: ExtensionCommandContext,
|
|
324
|
+
accountManager: AccountManager,
|
|
325
|
+
): Promise<void> {
|
|
326
|
+
await accountManager.syncImportedOpenAICodexAuth();
|
|
327
|
+
await accountManager.refreshUsageForAllAccounts();
|
|
328
|
+
const accounts = accountManager.getAccounts();
|
|
329
|
+
if (accounts.length === 0) {
|
|
330
|
+
ctx.ui.notify(NO_ACCOUNTS_MESSAGE, "warning");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!ctx.hasUI) {
|
|
335
|
+
const active = accountManager.getActiveAccount()?.email ?? "none";
|
|
336
|
+
const manual = accountManager.getManualAccount()?.email ?? "none";
|
|
337
|
+
ctx.ui.notify(
|
|
338
|
+
`multicodex: accounts=${accounts.length} active=${active} manual=${manual}`,
|
|
339
|
+
"info",
|
|
340
|
+
);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const options = accounts.map((account) =>
|
|
345
|
+
formatAccountStatusLine(accountManager, account.email),
|
|
346
|
+
);
|
|
347
|
+
await ctx.ui.select("MultiCodex Accounts", options);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function runFooterSubcommand(
|
|
351
|
+
ctx: ExtensionCommandContext,
|
|
352
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
if (!ctx.hasUI) {
|
|
355
|
+
await statusController.loadPreferences(ctx);
|
|
356
|
+
const preferences = statusController.getPreferences();
|
|
357
|
+
ctx.ui.notify(
|
|
358
|
+
`footer: usageMode=${preferences.usageMode} resetWindow=${preferences.resetWindow} showAccount=${preferences.showAccount ? "on" : "off"} showReset=${preferences.showReset ? "on" : "off"} order=${preferences.order}`,
|
|
359
|
+
"info",
|
|
360
|
+
);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
await statusController.openPreferencesPanel(ctx);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function runRotationSubcommand(
|
|
368
|
+
ctx: ExtensionCommandContext,
|
|
369
|
+
): Promise<void> {
|
|
370
|
+
const lines = [
|
|
371
|
+
"Rotation settings are not configurable yet.",
|
|
372
|
+
"Current policy: manual account, then untouched accounts, then earliest weekly reset, then random fallback.",
|
|
373
|
+
"Quota cooldown uses next known reset time, with 1 hour fallback when unknown.",
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
if (!ctx.hasUI) {
|
|
377
|
+
ctx.ui.notify(
|
|
378
|
+
"rotation: manual->untouched->earliest-weekly-reset->random, cooldown=next-reset-or-1h",
|
|
379
|
+
"info",
|
|
380
|
+
);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
await ctx.ui.select("MultiCodex Rotation", lines);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function isWritableDirectoryFor(filePath: string): Promise<boolean> {
|
|
388
|
+
try {
|
|
389
|
+
const directory = path.dirname(filePath);
|
|
390
|
+
await fs.mkdir(directory, { recursive: true });
|
|
391
|
+
await fs.access(directory, fsConstants.R_OK | fsConstants.W_OK);
|
|
392
|
+
return true;
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function runVerifySubcommand(
|
|
399
|
+
ctx: ExtensionCommandContext,
|
|
400
|
+
accountManager: AccountManager,
|
|
401
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
402
|
+
): Promise<void> {
|
|
403
|
+
const storageWritable = await isWritableDirectoryFor(STORAGE_FILE);
|
|
404
|
+
const settingsWritable = await isWritableDirectoryFor(SETTINGS_FILE);
|
|
405
|
+
const authImported = await accountManager.syncImportedOpenAICodexAuth();
|
|
406
|
+
await statusController.loadPreferences(ctx);
|
|
407
|
+
const accounts = accountManager.getAccounts().length;
|
|
408
|
+
const active = accountManager.getActiveAccount()?.email ?? "none";
|
|
409
|
+
const ok = storageWritable && settingsWritable;
|
|
410
|
+
|
|
411
|
+
if (!ctx.hasUI) {
|
|
412
|
+
ctx.ui.notify(
|
|
413
|
+
`verify: ${ok ? "PASS" : "WARN"} storage=${storageWritable ? "ok" : "fail"} settings=${settingsWritable ? "ok" : "fail"} accounts=${accounts} active=${active} authImport=${authImported ? "updated" : "unchanged"}`,
|
|
414
|
+
ok ? "info" : "warning",
|
|
415
|
+
);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const lines = [
|
|
420
|
+
`storage directory writable: ${storageWritable ? "yes" : "no"}`,
|
|
421
|
+
`settings directory writable: ${settingsWritable ? "yes" : "no"}`,
|
|
422
|
+
`managed accounts: ${accounts}`,
|
|
423
|
+
`active account: ${active}`,
|
|
424
|
+
`auth import changed state: ${authImported ? "yes" : "no"}`,
|
|
425
|
+
];
|
|
426
|
+
await ctx.ui.select(`MultiCodex Verify (${ok ? "PASS" : "WARN"})`, lines);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function runPathSubcommand(ctx: ExtensionCommandContext): Promise<void> {
|
|
430
|
+
if (!ctx.hasUI) {
|
|
431
|
+
ctx.ui.notify(
|
|
432
|
+
`paths: storage=${STORAGE_FILE} settings=${SETTINGS_FILE}`,
|
|
433
|
+
"info",
|
|
434
|
+
);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
await ctx.ui.select("MultiCodex Paths", [
|
|
439
|
+
`Managed account storage: ${STORAGE_FILE}`,
|
|
440
|
+
`Extension settings: ${SETTINGS_FILE}`,
|
|
441
|
+
]);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function chooseResetTarget(
|
|
445
|
+
ctx: ExtensionCommandContext,
|
|
446
|
+
argument: string,
|
|
447
|
+
): Promise<ResetTarget | undefined> {
|
|
448
|
+
const explicitTarget = parseResetTarget(argument.toLowerCase());
|
|
449
|
+
if (explicitTarget) {
|
|
450
|
+
return explicitTarget;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (argument) {
|
|
454
|
+
ctx.ui.notify(
|
|
455
|
+
"Unknown reset target. Use: /multicodex reset [manual|quota|all]",
|
|
456
|
+
"warning",
|
|
457
|
+
);
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!ctx.hasUI) {
|
|
462
|
+
return "all";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const options = [
|
|
466
|
+
"manual - clear manual account override",
|
|
467
|
+
"quota - clear quota cooldown markers",
|
|
468
|
+
"all - clear manual override and quota cooldown markers",
|
|
469
|
+
];
|
|
470
|
+
const selected = await ctx.ui.select("Reset MultiCodex State", options);
|
|
471
|
+
if (!selected) return undefined;
|
|
472
|
+
if (selected.startsWith("manual")) return "manual";
|
|
473
|
+
if (selected.startsWith("quota")) return "quota";
|
|
474
|
+
return "all";
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function runResetSubcommand(
|
|
478
|
+
ctx: ExtensionCommandContext,
|
|
479
|
+
accountManager: AccountManager,
|
|
480
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
481
|
+
rest: string,
|
|
482
|
+
): Promise<void> {
|
|
483
|
+
const target = await chooseResetTarget(ctx, rest);
|
|
484
|
+
if (!target) return;
|
|
485
|
+
|
|
486
|
+
if (target === "all" && ctx.hasUI) {
|
|
487
|
+
const confirmed = await ctx.ui.confirm(
|
|
488
|
+
"Reset MultiCodex state",
|
|
489
|
+
"Clear manual account override and all quota cooldown markers?",
|
|
490
|
+
);
|
|
491
|
+
if (!confirmed) return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const hadManual = accountManager.hasManualAccount();
|
|
495
|
+
if (target === "manual" || target === "all") {
|
|
496
|
+
accountManager.clearManualAccount();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let clearedQuota = 0;
|
|
500
|
+
if (target === "quota" || target === "all") {
|
|
501
|
+
clearedQuota = accountManager.clearAllQuotaExhaustion();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const manualCleared = hadManual && !accountManager.hasManualAccount();
|
|
505
|
+
ctx.ui.notify(
|
|
506
|
+
`reset: target=${target} manualCleared=${manualCleared ? "yes" : "no"} quotaCleared=${clearedQuota}`,
|
|
507
|
+
"info",
|
|
508
|
+
);
|
|
509
|
+
await statusController.refreshFor(ctx);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function runHelpSubcommand(ctx: ExtensionCommandContext): void {
|
|
513
|
+
ctx.ui.notify(HELP_TEXT, "info");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function runUseSubcommand(
|
|
517
|
+
pi: ExtensionAPI,
|
|
518
|
+
ctx: ExtensionCommandContext,
|
|
519
|
+
accountManager: AccountManager,
|
|
520
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
521
|
+
rest: string,
|
|
522
|
+
): Promise<void> {
|
|
523
|
+
await accountManager.syncImportedOpenAICodexAuth();
|
|
524
|
+
|
|
525
|
+
if (rest) {
|
|
526
|
+
await useOrLoginAccount(pi, ctx, accountManager, rest);
|
|
527
|
+
await statusController.refreshFor(ctx);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!ctx.hasUI) {
|
|
532
|
+
ctx.ui.notify(
|
|
533
|
+
"/multicodex use requires an identifier in non-interactive mode.",
|
|
534
|
+
"warning",
|
|
535
|
+
);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
await openAccountSelectionFlow(ctx, accountManager, statusController);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function runSubcommand(
|
|
543
|
+
subcommand: Subcommand,
|
|
544
|
+
rest: string,
|
|
545
|
+
pi: ExtensionAPI,
|
|
546
|
+
ctx: ExtensionCommandContext,
|
|
547
|
+
accountManager: AccountManager,
|
|
548
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
549
|
+
): Promise<void> {
|
|
550
|
+
if (subcommand === "show") {
|
|
551
|
+
await runShowSubcommand(ctx, accountManager);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (subcommand === "use") {
|
|
555
|
+
await runUseSubcommand(pi, ctx, accountManager, statusController, rest);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (subcommand === "footer") {
|
|
559
|
+
await runFooterSubcommand(ctx, statusController);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (subcommand === "rotation") {
|
|
563
|
+
await runRotationSubcommand(ctx);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (subcommand === "verify") {
|
|
567
|
+
await runVerifySubcommand(ctx, accountManager, statusController);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (subcommand === "path") {
|
|
571
|
+
await runPathSubcommand(ctx);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (subcommand === "reset") {
|
|
575
|
+
await runResetSubcommand(ctx, accountManager, statusController, rest);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
runHelpSubcommand(ctx);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async function openMainPanel(
|
|
583
|
+
pi: ExtensionAPI,
|
|
584
|
+
ctx: ExtensionCommandContext,
|
|
585
|
+
accountManager: AccountManager,
|
|
586
|
+
statusController: ReturnType<typeof createUsageStatusController>,
|
|
587
|
+
): Promise<void> {
|
|
588
|
+
const actions = [
|
|
589
|
+
"use: select, activate, or remove managed account",
|
|
590
|
+
"show: managed account and usage summary",
|
|
591
|
+
"footer: footer settings panel",
|
|
592
|
+
"rotation: current rotation behavior",
|
|
593
|
+
"verify: runtime health checks",
|
|
594
|
+
"path: storage and settings locations",
|
|
595
|
+
"reset: clear manual or quota state",
|
|
596
|
+
"help: command usage",
|
|
597
|
+
];
|
|
598
|
+
|
|
599
|
+
const selected = await ctx.ui.select("MultiCodex", actions);
|
|
600
|
+
if (!selected) return;
|
|
601
|
+
|
|
602
|
+
const subcommandText = selected.split(":")[0]?.trim() ?? "";
|
|
603
|
+
if (!isSubcommand(subcommandText)) {
|
|
604
|
+
ctx.ui.notify(`Unknown subcommand: ${subcommandText}`, "warning");
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
await runSubcommand(
|
|
608
|
+
subcommandText,
|
|
609
|
+
"",
|
|
610
|
+
pi,
|
|
611
|
+
ctx,
|
|
612
|
+
accountManager,
|
|
613
|
+
statusController,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
71
617
|
export function registerCommands(
|
|
72
618
|
pi: ExtensionAPI,
|
|
73
619
|
accountManager: AccountManager,
|
|
74
620
|
statusController: ReturnType<typeof createUsageStatusController>,
|
|
75
621
|
): void {
|
|
76
|
-
pi.registerCommand("multicodex
|
|
77
|
-
description:
|
|
78
|
-
|
|
622
|
+
pi.registerCommand("multicodex", {
|
|
623
|
+
description: "Manage MultiCodex accounts, rotation, and footer settings",
|
|
624
|
+
getArgumentCompletions: (argumentPrefix: string) =>
|
|
625
|
+
getCommandCompletions(argumentPrefix, accountManager),
|
|
79
626
|
handler: async (
|
|
80
627
|
args: string,
|
|
81
628
|
ctx: ExtensionCommandContext,
|
|
82
629
|
): Promise<void> => {
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
630
|
+
const parsed = parseCommandArgs(args);
|
|
631
|
+
if (!parsed.subcommand) {
|
|
632
|
+
if (!ctx.hasUI) {
|
|
633
|
+
ctx.ui.notify(
|
|
634
|
+
"/multicodex requires a subcommand in non-interactive mode. Use /multicodex help.",
|
|
635
|
+
"warning",
|
|
636
|
+
);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
await openMainPanel(pi, ctx, accountManager, statusController);
|
|
87
640
|
return;
|
|
88
641
|
}
|
|
89
642
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
ctx.ui.notify(
|
|
94
|
-
"No managed accounts found. Use /login or /multicodex-use <identifier> first.",
|
|
95
|
-
"warning",
|
|
96
|
-
);
|
|
643
|
+
if (!isSubcommand(parsed.subcommand)) {
|
|
644
|
+
ctx.ui.notify(`Unknown subcommand: ${parsed.subcommand}`, "warning");
|
|
645
|
+
runHelpSubcommand(ctx);
|
|
97
646
|
return;
|
|
98
647
|
}
|
|
99
648
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
649
|
+
await runSubcommand(
|
|
650
|
+
parsed.subcommand,
|
|
651
|
+
parsed.rest,
|
|
652
|
+
pi,
|
|
653
|
+
ctx,
|
|
654
|
+
accountManager,
|
|
655
|
+
statusController,
|
|
107
656
|
);
|
|
108
|
-
const selected = await ctx.ui.select("Select Account", options);
|
|
109
|
-
if (!selected) return;
|
|
110
|
-
|
|
111
|
-
const email = selected.split(" (")[0] ?? selected;
|
|
112
|
-
accountManager.setManualAccount(email);
|
|
113
|
-
ctx.ui.notify(`Now using ${email}`, "info");
|
|
114
|
-
await statusController.refreshFor(ctx);
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
pi.registerCommand("multicodex-status", {
|
|
119
|
-
description: "Show all Codex accounts and active status",
|
|
120
|
-
handler: async (
|
|
121
|
-
_args: string,
|
|
122
|
-
ctx: ExtensionCommandContext,
|
|
123
|
-
): Promise<void> => {
|
|
124
|
-
await accountManager.syncImportedOpenAICodexAuth();
|
|
125
|
-
await accountManager.refreshUsageForAllAccounts();
|
|
126
|
-
const accounts = accountManager.getAccounts();
|
|
127
|
-
if (accounts.length === 0) {
|
|
128
|
-
ctx.ui.notify(
|
|
129
|
-
"No managed accounts found. Use /login or /multicodex-use <identifier> first.",
|
|
130
|
-
"warning",
|
|
131
|
-
);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const active = accountManager.getActiveAccount();
|
|
136
|
-
const options = accounts.map((account) => {
|
|
137
|
-
const usage = accountManager.getCachedUsage(account.email);
|
|
138
|
-
const isActive = active?.email === account.email;
|
|
139
|
-
const quotaHit =
|
|
140
|
-
account.quotaExhaustedUntil &&
|
|
141
|
-
account.quotaExhaustedUntil > Date.now();
|
|
142
|
-
const untouched = isUsageUntouched(usage) ? "untouched" : null;
|
|
143
|
-
const imported = account.importSource ? "imported" : null;
|
|
144
|
-
const tags = [
|
|
145
|
-
isActive ? "active" : null,
|
|
146
|
-
quotaHit ? "quota" : null,
|
|
147
|
-
untouched,
|
|
148
|
-
imported,
|
|
149
|
-
]
|
|
150
|
-
.filter(Boolean)
|
|
151
|
-
.join(", ");
|
|
152
|
-
const suffix = tags ? ` (${tags})` : "";
|
|
153
|
-
const primaryUsed = usage?.primary?.usedPercent;
|
|
154
|
-
const secondaryUsed = usage?.secondary?.usedPercent;
|
|
155
|
-
const primaryReset = usage?.primary?.resetAt;
|
|
156
|
-
const secondaryReset = usage?.secondary?.resetAt;
|
|
157
|
-
const primaryLabel =
|
|
158
|
-
primaryUsed === undefined ? "unknown" : `${Math.round(primaryUsed)}%`;
|
|
159
|
-
const secondaryLabel =
|
|
160
|
-
secondaryUsed === undefined
|
|
161
|
-
? "unknown"
|
|
162
|
-
: `${Math.round(secondaryUsed)}%`;
|
|
163
|
-
const usageSummary = `5h ${primaryLabel} reset:${formatResetAt(primaryReset)} | weekly ${secondaryLabel} reset:${formatResetAt(secondaryReset)}`;
|
|
164
|
-
return `${isActive ? "•" : " "} ${account.email}${suffix} - ${usageSummary}`;
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
await ctx.ui.select("MultiCodex Accounts", options);
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
pi.registerCommand("multicodex-footer", {
|
|
172
|
-
description: "Configure the MultiCodex usage footer",
|
|
173
|
-
handler: async (
|
|
174
|
-
_args: string,
|
|
175
|
-
ctx: ExtensionCommandContext,
|
|
176
|
-
): Promise<void> => {
|
|
177
|
-
await statusController.openPreferencesPanel(ctx);
|
|
178
657
|
},
|
|
179
658
|
});
|
|
180
659
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@victor-software-house/pi-multicodex",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Codex account rotation extension for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -56,8 +56,7 @@
|
|
|
56
56
|
"tsgo": "tsgo -p tsconfig.json",
|
|
57
57
|
"check": "pnpm lint && pnpm tsgo && pnpm test",
|
|
58
58
|
"pack:dry": "npm pack --dry-run",
|
|
59
|
-
"release:dry": "
|
|
60
|
-
"release:prepare": "bun ./scripts/publish.ts"
|
|
59
|
+
"release:dry": "pnpm exec semantic-release --dry-run"
|
|
61
60
|
},
|
|
62
61
|
"peerDependencies": {
|
|
63
62
|
"@mariozechner/pi-ai": "*",
|
|
@@ -77,11 +76,20 @@
|
|
|
77
76
|
},
|
|
78
77
|
"devDependencies": {
|
|
79
78
|
"@biomejs/biome": "^2.4.7",
|
|
79
|
+
"@commitlint/cli": "^20.4.4",
|
|
80
|
+
"@commitlint/config-conventional": "^20.4.4",
|
|
80
81
|
"@mariozechner/pi-ai": "^0.58.1",
|
|
81
82
|
"@mariozechner/pi-coding-agent": "^0.58.1",
|
|
82
83
|
"@mariozechner/pi-tui": "^0.58.1",
|
|
84
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
85
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
86
|
+
"@semantic-release/git": "^10.0.1",
|
|
87
|
+
"@semantic-release/github": "^12.0.6",
|
|
88
|
+
"@semantic-release/npm": "^13.1.5",
|
|
89
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
83
90
|
"@types/node": "^25.5.0",
|
|
84
91
|
"@typescript/native-preview": "7.0.0-dev.20260314.1",
|
|
92
|
+
"semantic-release": "^25.0.3",
|
|
85
93
|
"typescript": "^5.9.3",
|
|
86
94
|
"vitest": "^4.1.0"
|
|
87
95
|
},
|
package/stream-wrapper.ts
CHANGED
|
@@ -101,7 +101,7 @@ export function createStreamWrapper(
|
|
|
101
101
|
}
|
|
102
102
|
if (!account) {
|
|
103
103
|
throw new Error(
|
|
104
|
-
"No available Multicodex accounts. Please use /multicodex
|
|
104
|
+
"No available Multicodex accounts. Please use /multicodex use <identifier>.",
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
107
|
|