opencode-copilot-account-switcher 0.12.3 → 0.13.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/dist/codex-oauth.d.ts +39 -0
- package/dist/codex-oauth.js +316 -0
- package/dist/codex-status-command.js +94 -31
- package/dist/codex-status-fetcher.js +21 -6
- package/dist/codex-store.d.ts +36 -13
- package/dist/codex-store.js +231 -39
- package/dist/menu-runtime.d.ts +69 -0
- package/dist/menu-runtime.js +108 -0
- package/dist/plugin.js +141 -682
- package/dist/providers/codex-menu-adapter.d.ts +47 -0
- package/dist/providers/codex-menu-adapter.js +307 -0
- package/dist/providers/copilot-menu-adapter.d.ts +65 -0
- package/dist/providers/copilot-menu-adapter.js +763 -0
- package/dist/providers/descriptor.js +7 -2
- package/dist/providers/registry.js +3 -2
- package/dist/ui/menu.d.ts +26 -2
- package/dist/ui/menu.js +194 -41
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type CodexStatusFetcherResult } from "../codex-status-fetcher.js";
|
|
2
|
+
import { type CodexOAuthAccount } from "../codex-oauth.js";
|
|
3
|
+
import { type CodexAccountEntry, type CodexStoreFile } from "../codex-store.js";
|
|
4
|
+
import type { ProviderMenuAdapter } from "../menu-runtime.js";
|
|
5
|
+
import { type AccountEntry } from "../store.js";
|
|
6
|
+
type WriteMeta = {
|
|
7
|
+
reason: string;
|
|
8
|
+
source: string;
|
|
9
|
+
actionType?: string;
|
|
10
|
+
};
|
|
11
|
+
type AuthClient = {
|
|
12
|
+
auth: {
|
|
13
|
+
set: (input: {
|
|
14
|
+
path: {
|
|
15
|
+
id: string;
|
|
16
|
+
};
|
|
17
|
+
body: {
|
|
18
|
+
type: "oauth";
|
|
19
|
+
refresh?: string;
|
|
20
|
+
access?: string;
|
|
21
|
+
expires?: number;
|
|
22
|
+
accountId?: string;
|
|
23
|
+
};
|
|
24
|
+
}) => Promise<unknown>;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
type AdapterDependencies = {
|
|
28
|
+
client: AuthClient;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
promptText?: (message: string) => Promise<string>;
|
|
31
|
+
readStore?: () => Promise<CodexStoreFile>;
|
|
32
|
+
writeStore?: (store: CodexStoreFile, meta: WriteMeta) => Promise<void>;
|
|
33
|
+
readAuthEntries?: () => Promise<Record<string, AccountEntry>>;
|
|
34
|
+
fetchStatus?: (input: {
|
|
35
|
+
oauth: {
|
|
36
|
+
type: "oauth";
|
|
37
|
+
refresh?: string;
|
|
38
|
+
access?: string;
|
|
39
|
+
expires?: number;
|
|
40
|
+
accountId?: string;
|
|
41
|
+
};
|
|
42
|
+
accountId?: string;
|
|
43
|
+
}) => Promise<CodexStatusFetcherResult>;
|
|
44
|
+
runCodexOAuth?: () => Promise<CodexOAuthAccount | undefined>;
|
|
45
|
+
};
|
|
46
|
+
export declare function createCodexMenuAdapter(inputDeps: AdapterDependencies): ProviderMenuAdapter<CodexStoreFile, CodexAccountEntry>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { fetchCodexStatus } from "../codex-status-fetcher.js";
|
|
4
|
+
import { runCodexOAuth } from "../codex-oauth.js";
|
|
5
|
+
import { getActiveCodexAccount, readCodexStore, writeCodexStore, } from "../codex-store.js";
|
|
6
|
+
import { readAuth } from "../store.js";
|
|
7
|
+
function pickName(input) {
|
|
8
|
+
const accountId = input.accountId?.trim();
|
|
9
|
+
if (accountId)
|
|
10
|
+
return accountId;
|
|
11
|
+
const email = input.email?.trim();
|
|
12
|
+
if (email)
|
|
13
|
+
return email;
|
|
14
|
+
return input.fallback ?? "openai";
|
|
15
|
+
}
|
|
16
|
+
function ensureUniqueAccountName(store, preferred, currentName) {
|
|
17
|
+
if (!store.accounts[preferred] || preferred === currentName)
|
|
18
|
+
return preferred;
|
|
19
|
+
let index = 2;
|
|
20
|
+
while (store.accounts[`${preferred}#${index}`]) {
|
|
21
|
+
index += 1;
|
|
22
|
+
}
|
|
23
|
+
return `${preferred}#${index}`;
|
|
24
|
+
}
|
|
25
|
+
function toOAuth(entry) {
|
|
26
|
+
return {
|
|
27
|
+
type: "oauth",
|
|
28
|
+
refresh: entry.refresh,
|
|
29
|
+
access: entry.access,
|
|
30
|
+
expires: entry.expires,
|
|
31
|
+
accountId: entry.accountId,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function toMenuQuota(entry) {
|
|
35
|
+
return {
|
|
36
|
+
premium: {
|
|
37
|
+
remaining: entry.snapshot?.usage5h?.remaining,
|
|
38
|
+
entitlement: entry.snapshot?.usage5h?.entitlement,
|
|
39
|
+
},
|
|
40
|
+
chat: {
|
|
41
|
+
remaining: entry.snapshot?.usageWeek?.remaining,
|
|
42
|
+
entitlement: entry.snapshot?.usageWeek?.entitlement,
|
|
43
|
+
},
|
|
44
|
+
completions: undefined,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function promptText(message) {
|
|
48
|
+
const rl = createInterface({ input, output });
|
|
49
|
+
try {
|
|
50
|
+
return (await rl.question(message)).trim();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
rl.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function createCodexMenuAdapter(inputDeps) {
|
|
57
|
+
const now = inputDeps.now ?? Date.now;
|
|
58
|
+
const loadStore = inputDeps.readStore ?? readCodexStore;
|
|
59
|
+
const prompt = inputDeps.promptText ?? promptText;
|
|
60
|
+
const persistStore = inputDeps.writeStore ?? (async (store, meta) => {
|
|
61
|
+
await writeCodexStore(store);
|
|
62
|
+
void meta;
|
|
63
|
+
});
|
|
64
|
+
const loadAuth = inputDeps.readAuthEntries ?? readAuth;
|
|
65
|
+
const fetchStatus = inputDeps.fetchStatus ?? ((input) => fetchCodexStatus(input));
|
|
66
|
+
const authorizeOpenAIOAuth = inputDeps.runCodexOAuth ?? runCodexOAuth;
|
|
67
|
+
const refreshSnapshots = async (store) => {
|
|
68
|
+
const names = Object.keys(store.accounts);
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
const entry = store.accounts[name];
|
|
71
|
+
const oauth = toOAuth(entry);
|
|
72
|
+
if (!oauth.access && !oauth.refresh) {
|
|
73
|
+
store.accounts[name] = {
|
|
74
|
+
...entry,
|
|
75
|
+
snapshot: {
|
|
76
|
+
...(entry.snapshot ?? {}),
|
|
77
|
+
updatedAt: now(),
|
|
78
|
+
error: "missing-openai-oauth",
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const result = await fetchStatus({ oauth, accountId: entry.accountId }).catch((error) => ({
|
|
84
|
+
ok: false,
|
|
85
|
+
error: {
|
|
86
|
+
kind: "network_error",
|
|
87
|
+
message: error instanceof Error ? error.message : String(error),
|
|
88
|
+
},
|
|
89
|
+
}));
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
store.accounts[name] = {
|
|
92
|
+
...entry,
|
|
93
|
+
snapshot: {
|
|
94
|
+
...(entry.snapshot ?? {}),
|
|
95
|
+
updatedAt: now(),
|
|
96
|
+
error: result.error.message,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const nextName = ensureUniqueAccountName(store, pickName({
|
|
102
|
+
accountId: result.status.identity.accountId,
|
|
103
|
+
email: result.status.identity.email,
|
|
104
|
+
fallback: name,
|
|
105
|
+
}), name);
|
|
106
|
+
const existing = store.accounts[nextName] ?? {};
|
|
107
|
+
const nextEntry = {
|
|
108
|
+
...existing,
|
|
109
|
+
...entry,
|
|
110
|
+
...(result.authPatch?.refresh !== undefined ? { refresh: result.authPatch.refresh } : {}),
|
|
111
|
+
...(result.authPatch?.access !== undefined ? { access: result.authPatch.access } : {}),
|
|
112
|
+
...(result.authPatch?.expires !== undefined ? { expires: result.authPatch.expires } : {}),
|
|
113
|
+
...(result.authPatch?.accountId !== undefined ? { accountId: result.authPatch.accountId } : {}),
|
|
114
|
+
name: nextName,
|
|
115
|
+
providerId: "openai",
|
|
116
|
+
accountId: result.status.identity.accountId ?? result.authPatch?.accountId ?? entry.accountId,
|
|
117
|
+
email: result.status.identity.email ?? entry.email,
|
|
118
|
+
snapshot: {
|
|
119
|
+
...(entry.snapshot ?? {}),
|
|
120
|
+
plan: result.status.identity.plan ?? entry.snapshot?.plan,
|
|
121
|
+
usage5h: {
|
|
122
|
+
entitlement: result.status.windows.primary.entitlement,
|
|
123
|
+
remaining: result.status.windows.primary.remaining,
|
|
124
|
+
used: result.status.windows.primary.used,
|
|
125
|
+
resetAt: result.status.windows.primary.resetAt,
|
|
126
|
+
},
|
|
127
|
+
usageWeek: {
|
|
128
|
+
entitlement: result.status.windows.secondary.entitlement,
|
|
129
|
+
remaining: result.status.windows.secondary.remaining,
|
|
130
|
+
used: result.status.windows.secondary.used,
|
|
131
|
+
resetAt: result.status.windows.secondary.resetAt,
|
|
132
|
+
},
|
|
133
|
+
updatedAt: result.status.updatedAt,
|
|
134
|
+
error: undefined,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
if (nextName !== name)
|
|
138
|
+
delete store.accounts[name];
|
|
139
|
+
store.accounts[nextName] = nextEntry;
|
|
140
|
+
store.lastSnapshotRefresh = result.status.updatedAt;
|
|
141
|
+
if (store.active === name || !store.active)
|
|
142
|
+
store.active = nextName;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
key: "codex",
|
|
147
|
+
loadStore,
|
|
148
|
+
writeStore: persistStore,
|
|
149
|
+
bootstrapAuthImport: async (store) => {
|
|
150
|
+
if (store.bootstrapAuthImportTried === true)
|
|
151
|
+
return false;
|
|
152
|
+
store.bootstrapAuthImportTried = true;
|
|
153
|
+
store.bootstrapAuthImportAt = now();
|
|
154
|
+
const authEntries = await loadAuth().catch(() => ({}));
|
|
155
|
+
const openai = authEntries.openai;
|
|
156
|
+
if (openai && (openai.refresh || openai.access)) {
|
|
157
|
+
const refresh = openai.refresh ?? openai.access;
|
|
158
|
+
const access = openai.access ?? openai.refresh;
|
|
159
|
+
const accountName = pickName({
|
|
160
|
+
accountId: openai.accountId,
|
|
161
|
+
email: openai.email,
|
|
162
|
+
fallback: "openai",
|
|
163
|
+
});
|
|
164
|
+
store.accounts[accountName] = {
|
|
165
|
+
...(store.accounts[accountName] ?? {}),
|
|
166
|
+
name: accountName,
|
|
167
|
+
providerId: "openai",
|
|
168
|
+
refresh,
|
|
169
|
+
access,
|
|
170
|
+
expires: openai.expires,
|
|
171
|
+
accountId: openai.accountId,
|
|
172
|
+
email: openai.email,
|
|
173
|
+
source: "auth",
|
|
174
|
+
addedAt: store.accounts[accountName]?.addedAt ?? now(),
|
|
175
|
+
};
|
|
176
|
+
if (!store.active)
|
|
177
|
+
store.active = accountName;
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
},
|
|
181
|
+
authorizeNewAccount: async () => {
|
|
182
|
+
const oauth = await authorizeOpenAIOAuth();
|
|
183
|
+
if (!oauth || (!oauth.refresh && !oauth.access))
|
|
184
|
+
return undefined;
|
|
185
|
+
const refresh = oauth.refresh ?? oauth.access;
|
|
186
|
+
const access = oauth.access ?? oauth.refresh;
|
|
187
|
+
await inputDeps.client.auth.set({
|
|
188
|
+
path: { id: "openai" },
|
|
189
|
+
body: {
|
|
190
|
+
type: "oauth",
|
|
191
|
+
refresh,
|
|
192
|
+
access,
|
|
193
|
+
expires: oauth.expires,
|
|
194
|
+
accountId: oauth.accountId,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
name: pickName({
|
|
199
|
+
accountId: oauth.accountId,
|
|
200
|
+
email: oauth.email,
|
|
201
|
+
fallback: `openai-${now()}`,
|
|
202
|
+
}),
|
|
203
|
+
providerId: "openai",
|
|
204
|
+
refresh,
|
|
205
|
+
access,
|
|
206
|
+
expires: oauth.expires,
|
|
207
|
+
accountId: oauth.accountId,
|
|
208
|
+
email: oauth.email,
|
|
209
|
+
source: "manual",
|
|
210
|
+
addedAt: now(),
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
refreshSnapshots,
|
|
214
|
+
toMenuInfo: async (store) => {
|
|
215
|
+
return Object.entries(store.accounts).map(([name, entry], index) => ({
|
|
216
|
+
id: entry.accountId ?? name,
|
|
217
|
+
name: entry.email ?? entry.accountId ?? name,
|
|
218
|
+
index,
|
|
219
|
+
isCurrent: store.active === name,
|
|
220
|
+
source: entry.source,
|
|
221
|
+
plan: entry.snapshot?.plan,
|
|
222
|
+
quota: toMenuQuota(entry),
|
|
223
|
+
addedAt: entry.addedAt,
|
|
224
|
+
lastUsed: entry.lastUsed,
|
|
225
|
+
}));
|
|
226
|
+
},
|
|
227
|
+
getCurrentEntry: (store) => getActiveCodexAccount(store)?.entry,
|
|
228
|
+
getRefreshConfig: (store) => ({ enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 }),
|
|
229
|
+
getAccountByName: (store, name) => {
|
|
230
|
+
const direct = store.accounts[name];
|
|
231
|
+
if (direct)
|
|
232
|
+
return { name, entry: direct };
|
|
233
|
+
const match = Object.entries(store.accounts).find(([, entry]) => entry.accountId === name);
|
|
234
|
+
if (!match)
|
|
235
|
+
return undefined;
|
|
236
|
+
return { name: match[0], entry: match[1] };
|
|
237
|
+
},
|
|
238
|
+
addAccount: (store, entry) => {
|
|
239
|
+
const name = entry.name ?? pickName({ accountId: entry.accountId, email: entry.email, fallback: "openai" });
|
|
240
|
+
store.accounts[name] = {
|
|
241
|
+
...entry,
|
|
242
|
+
name,
|
|
243
|
+
};
|
|
244
|
+
store.active = store.active ?? name;
|
|
245
|
+
return true;
|
|
246
|
+
},
|
|
247
|
+
removeAccount: (store, name) => {
|
|
248
|
+
const resolved = store.accounts[name]
|
|
249
|
+
? name
|
|
250
|
+
: Object.entries(store.accounts).find(([, entry]) => entry.accountId === name)?.[0];
|
|
251
|
+
if (!resolved)
|
|
252
|
+
return false;
|
|
253
|
+
delete store.accounts[resolved];
|
|
254
|
+
if (store.active === resolved)
|
|
255
|
+
store.active = Object.keys(store.accounts)[0];
|
|
256
|
+
return true;
|
|
257
|
+
},
|
|
258
|
+
removeAllAccounts: (store) => {
|
|
259
|
+
if (Object.keys(store.accounts).length === 0)
|
|
260
|
+
return false;
|
|
261
|
+
store.accounts = {};
|
|
262
|
+
store.active = undefined;
|
|
263
|
+
return true;
|
|
264
|
+
},
|
|
265
|
+
switchAccount: async (store, name, entry) => {
|
|
266
|
+
await inputDeps.client.auth.set({
|
|
267
|
+
path: { id: "openai" },
|
|
268
|
+
body: {
|
|
269
|
+
type: "oauth",
|
|
270
|
+
refresh: entry.refresh,
|
|
271
|
+
access: entry.access,
|
|
272
|
+
expires: entry.expires,
|
|
273
|
+
accountId: entry.accountId,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
store.active = name;
|
|
277
|
+
store.accounts[name] = {
|
|
278
|
+
...entry,
|
|
279
|
+
name,
|
|
280
|
+
providerId: "openai",
|
|
281
|
+
lastUsed: now(),
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
applyAction: async (store, action) => {
|
|
285
|
+
if (action.name === "refresh-snapshot") {
|
|
286
|
+
await refreshSnapshots(store);
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
if (action.name === "toggle-refresh") {
|
|
290
|
+
store.autoRefresh = store.autoRefresh !== true;
|
|
291
|
+
store.refreshMinutes = store.refreshMinutes ?? 15;
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
if (action.name === "set-interval") {
|
|
295
|
+
const raw = await prompt("Refresh interval (minutes): ");
|
|
296
|
+
if (!raw)
|
|
297
|
+
return false;
|
|
298
|
+
const value = Number(raw);
|
|
299
|
+
if (!Number.isFinite(value))
|
|
300
|
+
return false;
|
|
301
|
+
store.refreshMinutes = Math.max(1, Math.min(180, value));
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ProviderMenuAdapter } from "../menu-runtime.js";
|
|
2
|
+
import { type AccountEntry, type StoreFile, type StoreWriteDebugMeta } from "../store.js";
|
|
3
|
+
type AuthClient = {
|
|
4
|
+
auth: {
|
|
5
|
+
set: (input: {
|
|
6
|
+
path: {
|
|
7
|
+
id: string;
|
|
8
|
+
};
|
|
9
|
+
body: {
|
|
10
|
+
type: "oauth";
|
|
11
|
+
refresh: string;
|
|
12
|
+
access: string;
|
|
13
|
+
expires: number;
|
|
14
|
+
enterpriseUrl?: string;
|
|
15
|
+
};
|
|
16
|
+
}) => Promise<unknown>;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
type DebugMeta = StoreWriteDebugMeta;
|
|
20
|
+
type AdapterDependencies = {
|
|
21
|
+
client: AuthClient;
|
|
22
|
+
readStore?: () => Promise<StoreFile>;
|
|
23
|
+
writeStore?: (store: StoreFile, meta?: DebugMeta) => Promise<void>;
|
|
24
|
+
readAuth?: (filePath?: string) => Promise<Record<string, AccountEntry>>;
|
|
25
|
+
authorizeNewAccount?: (store: StoreFile) => Promise<AccountEntry | undefined>;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
fetchUser?: (entry: AccountEntry) => Promise<{
|
|
28
|
+
login?: string;
|
|
29
|
+
email?: string;
|
|
30
|
+
orgs?: string[];
|
|
31
|
+
} | undefined>;
|
|
32
|
+
fetchModels?: (entry: AccountEntry) => Promise<AccountEntry["models"]>;
|
|
33
|
+
fetchQuota?: (entry: AccountEntry) => Promise<AccountEntry["quota"]>;
|
|
34
|
+
configureDefaultAccountGroup?: (store: StoreFile, selectors?: {
|
|
35
|
+
selectAccounts?: (options: Array<{
|
|
36
|
+
label: string;
|
|
37
|
+
value: string;
|
|
38
|
+
hint?: string;
|
|
39
|
+
}>) => Promise<string[] | null>;
|
|
40
|
+
}) => Promise<boolean>;
|
|
41
|
+
configureModelAccountAssignments?: (store: StoreFile, selectors?: {
|
|
42
|
+
selectModel?: (options: Array<{
|
|
43
|
+
label: string;
|
|
44
|
+
value: string;
|
|
45
|
+
hint?: string;
|
|
46
|
+
}>) => Promise<string | null>;
|
|
47
|
+
selectAccounts?: (options: Array<{
|
|
48
|
+
label: string;
|
|
49
|
+
value: string;
|
|
50
|
+
hint?: string;
|
|
51
|
+
}>) => Promise<string[] | null>;
|
|
52
|
+
}) => Promise<boolean>;
|
|
53
|
+
clearAllAccounts?: (store: StoreFile) => void;
|
|
54
|
+
removeAccountFromStore?: (store: StoreFile, name: string) => void;
|
|
55
|
+
activateAddedAccount?: (input: {
|
|
56
|
+
store: StoreFile;
|
|
57
|
+
name: string;
|
|
58
|
+
switchAccount: () => Promise<void>;
|
|
59
|
+
writeStore: (store: StoreFile, meta?: StoreWriteDebugMeta) => Promise<void>;
|
|
60
|
+
now?: () => number;
|
|
61
|
+
}) => Promise<void>;
|
|
62
|
+
logSwitchHint?: () => void;
|
|
63
|
+
};
|
|
64
|
+
export declare function createCopilotMenuAdapter(inputDeps: AdapterDependencies): ProviderMenuAdapter<StoreFile, AccountEntry>;
|
|
65
|
+
export {};
|