opencode-copilot-account-switcher 0.12.4 → 0.13.1
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 +85 -28
- 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.d.ts +1 -0
- package/dist/plugin.js +153 -686
- 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 +49 -3
- package/dist/ui/menu.js +208 -43
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -1,257 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
-
import { fetchQuota } from "./active-account-quota.js";
|
|
4
|
-
import { getGitHubToken, normalizeDomain } from "./copilot-api-helpers.js";
|
|
1
|
+
import { normalizeDomain } from "./copilot-api-helpers.js";
|
|
5
2
|
import { listAssignableAccountsForModel, listKnownCopilotModels, rewriteModelAccountAssignments, } from "./model-account-map.js";
|
|
6
|
-
import {
|
|
3
|
+
import { runProviderMenu } from "./menu-runtime.js";
|
|
4
|
+
import { persistAccountSwitch } from "./plugin-actions.js";
|
|
7
5
|
import { buildPluginHooks } from "./plugin-hooks.js";
|
|
6
|
+
import { createCodexMenuAdapter } from "./providers/codex-menu-adapter.js";
|
|
7
|
+
import { createCopilotMenuAdapter } from "./providers/copilot-menu-adapter.js";
|
|
8
8
|
import { isTTY } from "./ui/ansi.js";
|
|
9
|
-
import {
|
|
9
|
+
import { showMenu } from "./ui/menu.js";
|
|
10
10
|
import { select, selectMany } from "./ui/select.js";
|
|
11
|
-
import {
|
|
11
|
+
import { readAuth, readStore, writeStore } from "./store.js";
|
|
12
12
|
function now() {
|
|
13
13
|
return Date.now();
|
|
14
14
|
}
|
|
15
|
-
const CLIENT_ID = "Ov23li8tweQw6odWQebz";
|
|
16
|
-
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000;
|
|
17
|
-
function getUrls(domain) {
|
|
18
|
-
return {
|
|
19
|
-
DEVICE_CODE_URL: `https://${domain}/login/device/code`,
|
|
20
|
-
ACCESS_TOKEN_URL: `https://${domain}/login/oauth/access_token`,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
async function sleep(ms) {
|
|
24
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
-
}
|
|
26
|
-
function toInfo(name, entry, index, active) {
|
|
27
|
-
const status = entry.expires && entry.expires > 0 && entry.expires < now() ? "expired" : "active";
|
|
28
|
-
const labelName = name.startsWith("github.com:") ? name.slice("github.com:".length) : name;
|
|
29
|
-
const hasUser = entry.user ? labelName.includes(entry.user) : false;
|
|
30
|
-
const hasEmail = entry.email ? labelName.includes(entry.email) : false;
|
|
31
|
-
const suffix = entry.user
|
|
32
|
-
? hasUser
|
|
33
|
-
? ""
|
|
34
|
-
: ` (${entry.user})`
|
|
35
|
-
: entry.email
|
|
36
|
-
? ` (${entry.email})`
|
|
37
|
-
: "";
|
|
38
|
-
const label = `${labelName}${suffix}`;
|
|
39
|
-
return {
|
|
40
|
-
name: label,
|
|
41
|
-
index,
|
|
42
|
-
addedAt: entry.addedAt,
|
|
43
|
-
lastUsed: entry.lastUsed,
|
|
44
|
-
status,
|
|
45
|
-
isCurrent: active === name,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
async function promptText(message) {
|
|
49
|
-
const rl = createInterface({ input, output });
|
|
50
|
-
try {
|
|
51
|
-
const answer = await rl.question(message);
|
|
52
|
-
return answer.trim();
|
|
53
|
-
}
|
|
54
|
-
finally {
|
|
55
|
-
rl.close();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async function promptAccountName(existing) {
|
|
59
|
-
while (true) {
|
|
60
|
-
const name = await promptText("Account name: ");
|
|
61
|
-
if (!name)
|
|
62
|
-
continue;
|
|
63
|
-
if (!existing.includes(name))
|
|
64
|
-
return name;
|
|
65
|
-
console.log(`Name already exists: ${name}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
async function promptAccountEntry(existing) {
|
|
69
|
-
const name = await promptAccountName(existing);
|
|
70
|
-
const refresh = await promptText("OAuth refresh/access token: ");
|
|
71
|
-
const access = await promptText("Copilot access token (optional, press Enter to skip): ");
|
|
72
|
-
const expiresRaw = await promptText("Access token expires (unix ms, optional): ");
|
|
73
|
-
const enterpriseUrl = await promptText("Enterprise URL (optional): ");
|
|
74
|
-
const expires = Number(expiresRaw);
|
|
75
|
-
const entry = {
|
|
76
|
-
name,
|
|
77
|
-
refresh,
|
|
78
|
-
access: access || refresh,
|
|
79
|
-
expires: Number.isFinite(expires) ? expires : 0,
|
|
80
|
-
enterpriseUrl: enterpriseUrl || undefined,
|
|
81
|
-
addedAt: now(),
|
|
82
|
-
source: "manual",
|
|
83
|
-
};
|
|
84
|
-
return { name, entry };
|
|
85
|
-
}
|
|
86
|
-
async function loginOauth(deployment, enterpriseUrl) {
|
|
87
|
-
const domain = deployment === "enterprise" ? normalizeDomain(enterpriseUrl ?? "") : "github.com";
|
|
88
|
-
const urls = getUrls(domain);
|
|
89
|
-
const deviceResponse = await fetch(urls.DEVICE_CODE_URL, {
|
|
90
|
-
method: "POST",
|
|
91
|
-
headers: {
|
|
92
|
-
Accept: "application/json",
|
|
93
|
-
"Content-Type": "application/json",
|
|
94
|
-
},
|
|
95
|
-
body: JSON.stringify({
|
|
96
|
-
client_id: CLIENT_ID,
|
|
97
|
-
scope: "read:user user:email",
|
|
98
|
-
}),
|
|
99
|
-
});
|
|
100
|
-
if (!deviceResponse.ok)
|
|
101
|
-
throw new Error("Failed to initiate device authorization");
|
|
102
|
-
const deviceData = (await deviceResponse.json());
|
|
103
|
-
console.log(`Go to: ${deviceData.verification_uri}`);
|
|
104
|
-
console.log(`Enter code: ${deviceData.user_code}`);
|
|
105
|
-
while (true) {
|
|
106
|
-
const response = await fetch(urls.ACCESS_TOKEN_URL, {
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers: {
|
|
109
|
-
Accept: "application/json",
|
|
110
|
-
"Content-Type": "application/json",
|
|
111
|
-
},
|
|
112
|
-
body: JSON.stringify({
|
|
113
|
-
client_id: CLIENT_ID,
|
|
114
|
-
device_code: deviceData.device_code,
|
|
115
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
116
|
-
}),
|
|
117
|
-
});
|
|
118
|
-
if (!response.ok)
|
|
119
|
-
throw new Error("Failed to poll token");
|
|
120
|
-
const data = (await response.json());
|
|
121
|
-
if (data.access_token) {
|
|
122
|
-
const entry = {
|
|
123
|
-
name: deployment === "enterprise" ? `enterprise:${domain}` : "github.com",
|
|
124
|
-
refresh: data.access_token,
|
|
125
|
-
access: data.access_token,
|
|
126
|
-
expires: 0,
|
|
127
|
-
enterpriseUrl: deployment === "enterprise" ? domain : undefined,
|
|
128
|
-
addedAt: now(),
|
|
129
|
-
source: "auth",
|
|
130
|
-
};
|
|
131
|
-
const user = await fetchUser(entry);
|
|
132
|
-
if (user?.login)
|
|
133
|
-
entry.user = user.login;
|
|
134
|
-
if (user?.email)
|
|
135
|
-
entry.email = user.email;
|
|
136
|
-
if (user?.orgs?.length)
|
|
137
|
-
entry.orgs = user.orgs;
|
|
138
|
-
return entry;
|
|
139
|
-
}
|
|
140
|
-
if (data.error === "authorization_pending") {
|
|
141
|
-
await sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (data.error === "slow_down") {
|
|
145
|
-
const serverInterval = data.interval;
|
|
146
|
-
const next = (serverInterval && serverInterval > 0 ? serverInterval : deviceData.interval + 5) * 1000;
|
|
147
|
-
await sleep(next + OAUTH_POLLING_SAFETY_MARGIN_MS);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
throw new Error("Authorization failed");
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
async function promptFilePath(message, defaultValue) {
|
|
154
|
-
const rl = createInterface({ input, output });
|
|
155
|
-
try {
|
|
156
|
-
const answer = await rl.question(`${message} (${defaultValue}): `);
|
|
157
|
-
return answer.trim() || defaultValue;
|
|
158
|
-
}
|
|
159
|
-
finally {
|
|
160
|
-
rl.close();
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
function buildName(entry, login) {
|
|
164
|
-
const user = login ?? entry.user;
|
|
165
|
-
if (!user)
|
|
166
|
-
return entry.name;
|
|
167
|
-
if (!entry.enterpriseUrl)
|
|
168
|
-
return user;
|
|
169
|
-
const host = normalizeDomain(entry.enterpriseUrl);
|
|
170
|
-
return `${host}:${user}`;
|
|
171
|
-
}
|
|
172
|
-
function score(entry) {
|
|
173
|
-
return (entry.user ? 2 : 0) + (entry.email ? 2 : 0) + (entry.orgs?.length ? 1 : 0);
|
|
174
|
-
}
|
|
175
|
-
function key(entry) {
|
|
176
|
-
if (entry.refresh)
|
|
177
|
-
return `refresh:${entry.refresh}`;
|
|
178
|
-
return undefined;
|
|
179
|
-
}
|
|
180
|
-
function dedupe(store) {
|
|
181
|
-
const seen = new Map();
|
|
182
|
-
for (const [name, entry] of Object.entries(store.accounts)) {
|
|
183
|
-
const k = key(entry);
|
|
184
|
-
if (!k)
|
|
185
|
-
continue;
|
|
186
|
-
const current = seen.get(k);
|
|
187
|
-
if (!current) {
|
|
188
|
-
seen.set(k, name);
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
const currentEntry = store.accounts[current];
|
|
192
|
-
if (score(entry) > score(currentEntry)) {
|
|
193
|
-
rewriteModelAccountAssignments(store, { [current]: name });
|
|
194
|
-
delete store.accounts[current];
|
|
195
|
-
seen.set(k, name);
|
|
196
|
-
if (store.active === current)
|
|
197
|
-
store.active = name;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
rewriteModelAccountAssignments(store, { [name]: current });
|
|
201
|
-
delete store.accounts[name];
|
|
202
|
-
if (store.active === name)
|
|
203
|
-
store.active = current;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
function mergeAuth(store, imported) {
|
|
207
|
-
dedupe(store);
|
|
208
|
-
const byRefresh = new Map();
|
|
209
|
-
for (const [name, entry] of Object.entries(store.accounts)) {
|
|
210
|
-
if (entry.refresh)
|
|
211
|
-
byRefresh.set(entry.refresh, name);
|
|
212
|
-
}
|
|
213
|
-
for (const [key, entry] of imported) {
|
|
214
|
-
const match = byRefresh.get(entry.refresh);
|
|
215
|
-
if (match) {
|
|
216
|
-
store.accounts[match] = {
|
|
217
|
-
...store.accounts[match],
|
|
218
|
-
...entry,
|
|
219
|
-
name: store.accounts[match].name,
|
|
220
|
-
source: "auth",
|
|
221
|
-
providerId: key,
|
|
222
|
-
};
|
|
223
|
-
if (!store.active)
|
|
224
|
-
store.active = match;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
const name = entry.name || `auth:${key}`;
|
|
228
|
-
store.accounts[name] = {
|
|
229
|
-
...entry,
|
|
230
|
-
name,
|
|
231
|
-
source: "auth",
|
|
232
|
-
providerId: key,
|
|
233
|
-
};
|
|
234
|
-
if (!store.active)
|
|
235
|
-
store.active = name;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
function renameAccounts(store, items) {
|
|
239
|
-
const counts = new Map();
|
|
240
|
-
const renamed = items.map((item) => {
|
|
241
|
-
const count = (counts.get(item.base) ?? 0) + 1;
|
|
242
|
-
counts.set(item.base, count);
|
|
243
|
-
const name = count === 1 ? item.base : `${item.base}#${count}`;
|
|
244
|
-
return { ...item, name, entry: { ...item.entry, name } };
|
|
245
|
-
});
|
|
246
|
-
store.accounts = renamed.reduce((acc, item) => {
|
|
247
|
-
acc[item.name] = item.entry;
|
|
248
|
-
return acc;
|
|
249
|
-
}, {});
|
|
250
|
-
rewriteModelAccountAssignments(store, Object.fromEntries(renamed.map((item) => [item.oldName, item.name])));
|
|
251
|
-
const active = renamed.find((item) => item.oldName === store.active);
|
|
252
|
-
if (active)
|
|
253
|
-
store.active = active.name;
|
|
254
|
-
}
|
|
255
15
|
export async function configureDefaultAccountGroup(store, selectors) {
|
|
256
16
|
const accountEntries = Object.entries(store.accounts);
|
|
257
17
|
if (accountEntries.length === 0) {
|
|
@@ -389,137 +149,6 @@ async function configureModelAccountAssignmentsWithSelection(store, selectors) {
|
|
|
389
149
|
};
|
|
390
150
|
return true;
|
|
391
151
|
}
|
|
392
|
-
async function refreshIdentity(store) {
|
|
393
|
-
const items = await Promise.all(Object.entries(store.accounts).map(async ([name, entry]) => {
|
|
394
|
-
const user = await fetchUser(entry);
|
|
395
|
-
const base = buildName(entry, user?.login ?? entry.user);
|
|
396
|
-
return {
|
|
397
|
-
oldName: name,
|
|
398
|
-
base,
|
|
399
|
-
entry: {
|
|
400
|
-
...entry,
|
|
401
|
-
user: user?.login ?? entry.user,
|
|
402
|
-
email: user?.email ?? entry.email,
|
|
403
|
-
orgs: user?.orgs ?? entry.orgs,
|
|
404
|
-
name: base,
|
|
405
|
-
},
|
|
406
|
-
};
|
|
407
|
-
}));
|
|
408
|
-
renameAccounts(store, items);
|
|
409
|
-
}
|
|
410
|
-
async function fetchModels(entry) {
|
|
411
|
-
try {
|
|
412
|
-
const headers = {
|
|
413
|
-
Accept: "application/json",
|
|
414
|
-
Authorization: `Bearer ${entry.access}`,
|
|
415
|
-
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
416
|
-
"Editor-Version": "vscode/1.96.2",
|
|
417
|
-
"Editor-Plugin-Version": "copilot/1.159.0",
|
|
418
|
-
"Copilot-Integration-Id": "vscode-chat",
|
|
419
|
-
"X-Github-Api-Version": "2025-04-01",
|
|
420
|
-
};
|
|
421
|
-
const modelsUrl = entry.enterpriseUrl
|
|
422
|
-
? `https://copilot-api.${normalizeDomain(entry.enterpriseUrl)}/models`
|
|
423
|
-
: "https://api.githubcopilot.com/models";
|
|
424
|
-
const modelRes = await fetch(modelsUrl, { headers });
|
|
425
|
-
if (!modelRes.ok) {
|
|
426
|
-
const base = entry.enterpriseUrl ? `https://api.${normalizeDomain(entry.enterpriseUrl)}` : "https://api.github.com";
|
|
427
|
-
const tokenRes = await fetch(`${base}/copilot_internal/v2/token`, {
|
|
428
|
-
headers: {
|
|
429
|
-
Accept: "application/json",
|
|
430
|
-
Authorization: `token ${getGitHubToken(entry)}`,
|
|
431
|
-
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
432
|
-
"Editor-Version": "vscode/1.96.2",
|
|
433
|
-
"Editor-Plugin-Version": "copilot/1.159.0",
|
|
434
|
-
"X-Github-Api-Version": "2025-04-01",
|
|
435
|
-
},
|
|
436
|
-
});
|
|
437
|
-
if (!tokenRes.ok)
|
|
438
|
-
return { available: [], disabled: [], error: `token ${tokenRes.status}` };
|
|
439
|
-
const tokenData = (await tokenRes.json());
|
|
440
|
-
if (!tokenData.token)
|
|
441
|
-
return { available: [], disabled: [], error: "token missing" };
|
|
442
|
-
// Update entry with new session token
|
|
443
|
-
entry.access = tokenData.token;
|
|
444
|
-
if (tokenData.expires_at)
|
|
445
|
-
entry.expires = tokenData.expires_at * 1000;
|
|
446
|
-
const fallbackRes = await fetch(modelsUrl, {
|
|
447
|
-
headers: {
|
|
448
|
-
Accept: "application/json",
|
|
449
|
-
Authorization: `Bearer ${tokenData.token}`,
|
|
450
|
-
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
451
|
-
"Editor-Version": "vscode/1.96.2",
|
|
452
|
-
"Editor-Plugin-Version": "copilot/1.159.0",
|
|
453
|
-
"Copilot-Integration-Id": "vscode-chat",
|
|
454
|
-
"X-Github-Api-Version": "2025-04-01",
|
|
455
|
-
},
|
|
456
|
-
});
|
|
457
|
-
if (!fallbackRes.ok)
|
|
458
|
-
return { available: [], disabled: [], error: `models ${fallbackRes.status}` };
|
|
459
|
-
return parseModels((await fallbackRes.json()));
|
|
460
|
-
}
|
|
461
|
-
return parseModels((await modelRes.json()));
|
|
462
|
-
}
|
|
463
|
-
catch (error) {
|
|
464
|
-
return { available: [], disabled: [], error: error instanceof Error ? error.message : String(error) };
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
function parseModels(modelData) {
|
|
468
|
-
const available = [];
|
|
469
|
-
const disabled = [];
|
|
470
|
-
for (const item of modelData.data ?? []) {
|
|
471
|
-
if (!item.id)
|
|
472
|
-
continue;
|
|
473
|
-
const enabled = item.model_picker_enabled === true && item.policy?.state !== "disabled";
|
|
474
|
-
if (enabled)
|
|
475
|
-
available.push(item.id);
|
|
476
|
-
else
|
|
477
|
-
disabled.push(item.id);
|
|
478
|
-
}
|
|
479
|
-
return { available, disabled, updatedAt: now() };
|
|
480
|
-
}
|
|
481
|
-
async function fetchUser(entry) {
|
|
482
|
-
try {
|
|
483
|
-
const base = entry.enterpriseUrl ? `https://api.${normalizeDomain(entry.enterpriseUrl)}` : "https://api.github.com";
|
|
484
|
-
const headers = {
|
|
485
|
-
Accept: "application/json",
|
|
486
|
-
Authorization: `token ${getGitHubToken(entry)}`,
|
|
487
|
-
"User-Agent": "GitHubCopilotChat/0.26.7",
|
|
488
|
-
};
|
|
489
|
-
const userRes = await fetch(`${base}/user`, { headers });
|
|
490
|
-
if (!userRes.ok)
|
|
491
|
-
return undefined;
|
|
492
|
-
const user = (await userRes.json());
|
|
493
|
-
let email = user.email;
|
|
494
|
-
if (!email) {
|
|
495
|
-
const emailRes = await fetch(`${base}/user/emails`, { headers });
|
|
496
|
-
if (emailRes.ok) {
|
|
497
|
-
const items = (await emailRes.json());
|
|
498
|
-
const primary = items.find((item) => item.primary && item.verified);
|
|
499
|
-
email = primary?.email ?? items[0]?.email;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
const orgRes = await fetch(`${base}/user/orgs`, { headers });
|
|
503
|
-
const orgs = orgRes.ok ? (await orgRes.json()).map((o) => o.login).filter(Boolean) : undefined;
|
|
504
|
-
return { login: user.login, email, orgs };
|
|
505
|
-
}
|
|
506
|
-
catch {
|
|
507
|
-
return undefined;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
async function switchAccount(client, entry) {
|
|
511
|
-
const payload = {
|
|
512
|
-
type: "oauth",
|
|
513
|
-
refresh: entry.refresh,
|
|
514
|
-
access: entry.access,
|
|
515
|
-
expires: entry.expires,
|
|
516
|
-
...(entry.enterpriseUrl ? { enterpriseUrl: entry.enterpriseUrl } : {}),
|
|
517
|
-
};
|
|
518
|
-
await client.auth.set({
|
|
519
|
-
path: { id: entry.enterpriseUrl ? "github-copilot-enterprise" : "github-copilot" },
|
|
520
|
-
body: payload,
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
152
|
export async function activateAddedAccount(input) {
|
|
524
153
|
await input.writeStore(input.store, {
|
|
525
154
|
reason: "activate-added-account",
|
|
@@ -534,12 +163,26 @@ export async function activateAddedAccount(input) {
|
|
|
534
163
|
writeStore: input.writeStore,
|
|
535
164
|
});
|
|
536
165
|
}
|
|
537
|
-
|
|
166
|
+
async function createAccountSwitcherPlugin(input, provider) {
|
|
538
167
|
const client = input.client;
|
|
539
168
|
const directory = input.directory;
|
|
540
169
|
const serverUrl = input.serverUrl;
|
|
541
170
|
const persistStore = (store, meta) => writeStore(store, { debug: meta });
|
|
542
|
-
const
|
|
171
|
+
const codexClient = {
|
|
172
|
+
auth: {
|
|
173
|
+
set: async (options) => client.auth.set({
|
|
174
|
+
path: options.path,
|
|
175
|
+
body: {
|
|
176
|
+
type: "oauth",
|
|
177
|
+
refresh: options.body.refresh ?? options.body.access ?? "",
|
|
178
|
+
access: options.body.access ?? options.body.refresh ?? "",
|
|
179
|
+
expires: options.body.expires ?? 0,
|
|
180
|
+
...(options.body.accountId ? { accountId: options.body.accountId } : {}),
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
const copilotMethods = [
|
|
543
186
|
{
|
|
544
187
|
type: "oauth",
|
|
545
188
|
label: "Manage GitHub Copilot accounts",
|
|
@@ -565,97 +208,55 @@ export const CopilotAccountSwitcher = async (input) => {
|
|
|
565
208
|
},
|
|
566
209
|
},
|
|
567
210
|
];
|
|
211
|
+
const codexMethods = [
|
|
212
|
+
{
|
|
213
|
+
type: "oauth",
|
|
214
|
+
label: "Manage OpenAI Codex accounts",
|
|
215
|
+
async authorize() {
|
|
216
|
+
const entry = await runCodexMenu();
|
|
217
|
+
return {
|
|
218
|
+
url: "",
|
|
219
|
+
instructions: "",
|
|
220
|
+
method: "auto",
|
|
221
|
+
async callback() {
|
|
222
|
+
if (!entry)
|
|
223
|
+
return { type: "failed" };
|
|
224
|
+
return {
|
|
225
|
+
type: "success",
|
|
226
|
+
provider: "openai",
|
|
227
|
+
refresh: entry.refresh ?? "",
|
|
228
|
+
access: entry.access ?? entry.refresh ?? "",
|
|
229
|
+
expires: entry.expires ?? 0,
|
|
230
|
+
...(entry.accountId ? { accountId: entry.accountId } : {}),
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
];
|
|
568
237
|
async function runMenu() {
|
|
569
|
-
const store = await readStore();
|
|
570
|
-
const auth = await readAuth().catch(() => ({}));
|
|
571
|
-
const imported = Object.entries(auth).filter(([key]) => key === "github-copilot" || key === "github-copilot-enterprise");
|
|
572
|
-
if (imported.length > 0) {
|
|
573
|
-
mergeAuth(store, imported);
|
|
574
|
-
const preferred = imported.find(([key]) => key === "github-copilot") ?? imported[0];
|
|
575
|
-
if (!store.active)
|
|
576
|
-
store.active = preferred?.[1].name;
|
|
577
|
-
}
|
|
578
|
-
if (Object.keys(store.accounts).length > 0
|
|
579
|
-
&& !Object.values(store.accounts).some((entry) => entry.user || entry.email || (entry.orgs && entry.orgs.length > 0))) {
|
|
580
|
-
await refreshIdentity(store);
|
|
581
|
-
dedupe(store);
|
|
582
|
-
await persistStore(store, {
|
|
583
|
-
reason: "refresh-identity-bootstrap",
|
|
584
|
-
source: "plugin.runMenu",
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
238
|
if (!isTTY()) {
|
|
588
239
|
console.log("Interactive menu requires a TTY terminal");
|
|
589
240
|
return;
|
|
590
241
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
source: "plugin.runMenu",
|
|
608
|
-
actionType: "toggle-refresh",
|
|
609
|
-
});
|
|
610
|
-
nextRefresh = now() + (store.refreshMinutes ?? 15) * 60_000;
|
|
611
|
-
}
|
|
612
|
-
const entries = Object.entries(store.accounts);
|
|
613
|
-
const refreshed = await Promise.all(entries.map(async ([name, entry]) => {
|
|
614
|
-
if (entry.user || entry.email || (entry.orgs && entry.orgs.length > 0))
|
|
615
|
-
return { name, entry };
|
|
616
|
-
const user = await fetchUser(entry);
|
|
617
|
-
return {
|
|
618
|
-
name,
|
|
619
|
-
entry: {
|
|
620
|
-
...entry,
|
|
621
|
-
user: user?.login ?? entry.user,
|
|
622
|
-
email: user?.email ?? entry.email,
|
|
623
|
-
orgs: user?.orgs ?? entry.orgs,
|
|
624
|
-
},
|
|
625
|
-
};
|
|
626
|
-
}));
|
|
627
|
-
for (const item of refreshed) {
|
|
628
|
-
store.accounts[item.name] = item.entry;
|
|
629
|
-
}
|
|
630
|
-
const accounts = entries.map(([name, entry], index) => ({
|
|
631
|
-
...toInfo(name, entry, index, store.active),
|
|
632
|
-
source: entry.source,
|
|
633
|
-
orgs: entry.orgs,
|
|
634
|
-
plan: entry.quota?.plan,
|
|
635
|
-
sku: entry.quota?.sku,
|
|
636
|
-
reset: entry.quota?.reset,
|
|
637
|
-
models: entry.models
|
|
638
|
-
? {
|
|
639
|
-
enabled: entry.models.available.length,
|
|
640
|
-
disabled: entry.models.disabled.length,
|
|
641
|
-
}
|
|
642
|
-
: undefined,
|
|
643
|
-
modelsError: entry.models?.error,
|
|
644
|
-
modelList: entry.models
|
|
645
|
-
? {
|
|
646
|
-
available: entry.models.available,
|
|
647
|
-
disabled: entry.models.disabled,
|
|
648
|
-
}
|
|
649
|
-
: undefined,
|
|
650
|
-
quota: entry.quota?.snapshots
|
|
651
|
-
? {
|
|
652
|
-
premium: entry.quota.snapshots.premium,
|
|
653
|
-
chat: entry.quota.snapshots.chat,
|
|
654
|
-
completions: entry.quota.snapshots.completions,
|
|
655
|
-
}
|
|
656
|
-
: undefined,
|
|
657
|
-
}));
|
|
242
|
+
const adapter = createCopilotMenuAdapter({
|
|
243
|
+
client,
|
|
244
|
+
readStore,
|
|
245
|
+
writeStore: persistStore,
|
|
246
|
+
readAuth,
|
|
247
|
+
now,
|
|
248
|
+
configureDefaultAccountGroup,
|
|
249
|
+
configureModelAccountAssignments,
|
|
250
|
+
clearAllAccounts,
|
|
251
|
+
removeAccountFromStore,
|
|
252
|
+
activateAddedAccount,
|
|
253
|
+
logSwitchHint: () => {
|
|
254
|
+
console.log("Switched account. If a later Copilot session hits input[*].id too long after switching, enable Copilot Network Retry from the menu.");
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
const toRuntimeAction = async (accounts, store) => {
|
|
658
258
|
const action = await showMenu(accounts, {
|
|
259
|
+
provider: "copilot",
|
|
659
260
|
refresh: { enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 },
|
|
660
261
|
lastQuotaRefresh: store.lastQuotaRefresh,
|
|
661
262
|
modelAccountAssignmentCount: Object.keys(store.modelAccountAssignments ?? {}).length,
|
|
@@ -666,234 +267,100 @@ export const CopilotAccountSwitcher = async (input) => {
|
|
|
666
267
|
networkRetryEnabled: store.networkRetryEnabled === true,
|
|
667
268
|
syntheticAgentInitiatorEnabled: store.syntheticAgentInitiatorEnabled === true,
|
|
668
269
|
});
|
|
669
|
-
if (action.type === "cancel")
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
if (user?.orgs?.length)
|
|
718
|
-
manual.entry.orgs = user.orgs;
|
|
719
|
-
manual.entry.name = buildName(manual.entry, user?.login);
|
|
720
|
-
store.accounts[manual.entry.name] = manual.entry;
|
|
721
|
-
store.active = store.active ?? manual.entry.name;
|
|
722
|
-
if (store.active === manual.entry.name) {
|
|
723
|
-
await activateAddedAccount({
|
|
724
|
-
store,
|
|
725
|
-
name: manual.entry.name,
|
|
726
|
-
switchAccount: () => switchAccount(client, manual.entry),
|
|
727
|
-
writeStore: persistStore,
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
else {
|
|
731
|
-
await persistStore(store, {
|
|
732
|
-
reason: "add-account-manual",
|
|
733
|
-
source: "plugin.runMenu",
|
|
734
|
-
actionType: "add",
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
if (action.type === "import") {
|
|
740
|
-
const file = await promptFilePath("auth.json path", authPath());
|
|
741
|
-
const auth = await readAuth(file).catch(() => ({}));
|
|
742
|
-
const imported = Object.entries(auth).filter(([key]) => key === "github-copilot" || key === "github-copilot-enterprise");
|
|
743
|
-
for (const [key, entry] of imported) {
|
|
744
|
-
const user = await fetchUser(entry);
|
|
745
|
-
if (user?.login)
|
|
746
|
-
entry.user = user.login;
|
|
747
|
-
if (user?.email)
|
|
748
|
-
entry.email = user.email;
|
|
749
|
-
if (user?.orgs?.length)
|
|
750
|
-
entry.orgs = user.orgs;
|
|
751
|
-
entry.name = buildName(entry, user?.login);
|
|
752
|
-
}
|
|
753
|
-
mergeAuth(store, imported);
|
|
754
|
-
await persistStore(store, {
|
|
755
|
-
reason: "import-auth",
|
|
756
|
-
source: "plugin.runMenu",
|
|
757
|
-
actionType: "import",
|
|
758
|
-
});
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
if (action.type === "refresh-identity") {
|
|
762
|
-
await refreshIdentity(store);
|
|
763
|
-
dedupe(store);
|
|
764
|
-
await persistStore(store, {
|
|
765
|
-
reason: "refresh-identity",
|
|
766
|
-
source: "plugin.runMenu",
|
|
767
|
-
actionType: "refresh-identity",
|
|
768
|
-
});
|
|
769
|
-
continue;
|
|
770
|
-
}
|
|
771
|
-
if (action.type === "toggle-refresh") {
|
|
772
|
-
store.autoRefresh = !store.autoRefresh;
|
|
773
|
-
store.refreshMinutes = store.refreshMinutes ?? 15;
|
|
774
|
-
await persistStore(store, {
|
|
775
|
-
reason: "toggle-refresh",
|
|
776
|
-
source: "plugin.runMenu",
|
|
777
|
-
actionType: "toggle-refresh",
|
|
778
|
-
});
|
|
779
|
-
continue;
|
|
780
|
-
}
|
|
781
|
-
if (action.type === "set-interval") {
|
|
782
|
-
const value = await promptText("Refresh interval (minutes): ");
|
|
783
|
-
const minutes = Math.max(1, Math.min(180, Number(value)));
|
|
784
|
-
if (Number.isFinite(minutes))
|
|
785
|
-
store.refreshMinutes = minutes;
|
|
786
|
-
await persistStore(store, {
|
|
787
|
-
reason: "set-interval",
|
|
788
|
-
source: "plugin.runMenu",
|
|
789
|
-
actionType: "set-interval",
|
|
790
|
-
});
|
|
791
|
-
continue;
|
|
792
|
-
}
|
|
793
|
-
if (action.type === "quota") {
|
|
794
|
-
const updated = await Promise.all(entries.map(async ([name, entry]) => ({
|
|
795
|
-
name,
|
|
796
|
-
entry: {
|
|
797
|
-
...entry,
|
|
798
|
-
quota: await fetchQuota(entry),
|
|
799
|
-
},
|
|
800
|
-
})));
|
|
801
|
-
for (const item of updated) {
|
|
802
|
-
store.accounts[item.name] = item.entry;
|
|
803
|
-
}
|
|
804
|
-
store.lastQuotaRefresh = now();
|
|
805
|
-
await persistStore(store, {
|
|
806
|
-
reason: "quota-refresh",
|
|
807
|
-
source: "plugin.runMenu",
|
|
808
|
-
actionType: "quota",
|
|
809
|
-
});
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
if (action.type === "check-models") {
|
|
813
|
-
const updated = await Promise.all(entries.map(async ([name, entry]) => ({
|
|
814
|
-
name,
|
|
815
|
-
entry: {
|
|
816
|
-
...entry,
|
|
817
|
-
models: await fetchModels(entry),
|
|
818
|
-
},
|
|
819
|
-
})));
|
|
820
|
-
for (const item of updated) {
|
|
821
|
-
store.accounts[item.name] = item.entry;
|
|
822
|
-
}
|
|
823
|
-
await persistStore(store, {
|
|
824
|
-
reason: "check-models",
|
|
825
|
-
source: "plugin.runMenu",
|
|
826
|
-
actionType: "check-models",
|
|
827
|
-
});
|
|
828
|
-
continue;
|
|
829
|
-
}
|
|
830
|
-
if (action.type === "configure-default-group") {
|
|
831
|
-
const changed = await configureDefaultAccountGroup(store);
|
|
832
|
-
if (!changed)
|
|
833
|
-
continue;
|
|
834
|
-
await persistStore(store, {
|
|
835
|
-
reason: "configure-default-account-group",
|
|
836
|
-
source: "plugin.runMenu",
|
|
837
|
-
actionType: "configure-default-account-group",
|
|
838
|
-
});
|
|
839
|
-
continue;
|
|
840
|
-
}
|
|
841
|
-
if (action.type === "assign-models") {
|
|
842
|
-
const changed = await configureModelAccountAssignments(store);
|
|
843
|
-
if (!changed)
|
|
844
|
-
continue;
|
|
845
|
-
await persistStore(store, {
|
|
846
|
-
reason: "assign-model-account",
|
|
847
|
-
source: "plugin.runMenu",
|
|
848
|
-
actionType: "assign-model-account",
|
|
849
|
-
});
|
|
850
|
-
continue;
|
|
851
|
-
}
|
|
852
|
-
if (action.type === "remove-all") {
|
|
853
|
-
clearAllAccounts(store);
|
|
854
|
-
await persistStore(store, {
|
|
855
|
-
reason: "remove-all",
|
|
856
|
-
source: "plugin.runMenu",
|
|
857
|
-
actionType: "remove-all",
|
|
858
|
-
});
|
|
859
|
-
continue;
|
|
860
|
-
}
|
|
861
|
-
if (action.type === "switch") {
|
|
862
|
-
const selected = entries[action.account.index];
|
|
863
|
-
if (!selected)
|
|
864
|
-
continue;
|
|
865
|
-
const [name, entry] = selected;
|
|
866
|
-
const decision = await showAccountActions(action.account);
|
|
867
|
-
if (decision === "back")
|
|
868
|
-
continue;
|
|
869
|
-
if (decision === "remove") {
|
|
870
|
-
removeAccountFromStore(store, name);
|
|
871
|
-
await persistStore(store, {
|
|
872
|
-
reason: "remove-account",
|
|
873
|
-
source: "plugin.runMenu",
|
|
874
|
-
actionType: "remove",
|
|
875
|
-
});
|
|
876
|
-
continue;
|
|
877
|
-
}
|
|
878
|
-
await switchAccount(client, entry);
|
|
879
|
-
await persistAccountSwitch({
|
|
880
|
-
store,
|
|
881
|
-
name,
|
|
882
|
-
at: now(),
|
|
883
|
-
writeStore: persistStore,
|
|
884
|
-
});
|
|
885
|
-
console.log("Switched account. If a later Copilot session hits input[*].id too long after switching, enable Copilot Network Retry from the menu.");
|
|
886
|
-
continue;
|
|
887
|
-
}
|
|
270
|
+
if (action.type === "cancel")
|
|
271
|
+
return { type: "cancel" };
|
|
272
|
+
if (action.type === "add")
|
|
273
|
+
return { type: "add" };
|
|
274
|
+
if (action.type === "import")
|
|
275
|
+
return { type: "provider", name: "import-auth" };
|
|
276
|
+
if (action.type === "refresh-identity")
|
|
277
|
+
return { type: "provider", name: "refresh-identity" };
|
|
278
|
+
if (action.type === "toggle-refresh")
|
|
279
|
+
return { type: "provider", name: "toggle-refresh" };
|
|
280
|
+
if (action.type === "set-interval")
|
|
281
|
+
return { type: "provider", name: "set-interval" };
|
|
282
|
+
if (action.type === "quota")
|
|
283
|
+
return { type: "provider", name: "quota-refresh" };
|
|
284
|
+
if (action.type === "check-models")
|
|
285
|
+
return { type: "provider", name: "check-models" };
|
|
286
|
+
if (action.type === "configure-default-group")
|
|
287
|
+
return { type: "provider", name: "configure-default-group" };
|
|
288
|
+
if (action.type === "assign-models")
|
|
289
|
+
return { type: "provider", name: "assign-models" };
|
|
290
|
+
if (action.type === "remove-all")
|
|
291
|
+
return { type: "remove-all" };
|
|
292
|
+
if (action.type === "switch")
|
|
293
|
+
return { type: "switch", account: action.account };
|
|
294
|
+
if (action.type === "remove")
|
|
295
|
+
return { type: "remove", account: action.account };
|
|
296
|
+
if (action.type === "toggle-loop-safety")
|
|
297
|
+
return { type: "provider", name: "toggle-loop-safety" };
|
|
298
|
+
if (action.type === "toggle-loop-safety-provider-scope")
|
|
299
|
+
return { type: "provider", name: "toggle-loop-safety-provider-scope" };
|
|
300
|
+
if (action.type === "toggle-experimental-slash-commands")
|
|
301
|
+
return { type: "provider", name: "toggle-experimental-slash-commands" };
|
|
302
|
+
if (action.type === "toggle-network-retry")
|
|
303
|
+
return { type: "provider", name: "toggle-network-retry" };
|
|
304
|
+
if (action.type === "toggle-synthetic-agent-initiator")
|
|
305
|
+
return { type: "provider", name: "toggle-synthetic-agent-initiator" };
|
|
306
|
+
return { type: "cancel" };
|
|
307
|
+
};
|
|
308
|
+
return runProviderMenu({
|
|
309
|
+
adapter,
|
|
310
|
+
showMenu: toRuntimeAction,
|
|
311
|
+
now,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async function runCodexMenu() {
|
|
315
|
+
if (!isTTY()) {
|
|
316
|
+
console.log("Interactive menu requires a TTY terminal");
|
|
317
|
+
return;
|
|
888
318
|
}
|
|
319
|
+
const adapter = createCodexMenuAdapter({
|
|
320
|
+
client: codexClient,
|
|
321
|
+
});
|
|
322
|
+
const toRuntimeAction = async (accounts, store) => {
|
|
323
|
+
const action = await showMenu(accounts, {
|
|
324
|
+
provider: "codex",
|
|
325
|
+
refresh: { enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 },
|
|
326
|
+
});
|
|
327
|
+
if (action.type === "cancel")
|
|
328
|
+
return { type: "cancel" };
|
|
329
|
+
if (action.type === "add")
|
|
330
|
+
return { type: "add" };
|
|
331
|
+
if (action.type === "quota")
|
|
332
|
+
return { type: "provider", name: "refresh-snapshot" };
|
|
333
|
+
if (action.type === "toggle-refresh")
|
|
334
|
+
return { type: "provider", name: "toggle-refresh" };
|
|
335
|
+
if (action.type === "set-interval")
|
|
336
|
+
return { type: "provider", name: "set-interval" };
|
|
337
|
+
if (action.type === "remove-all")
|
|
338
|
+
return { type: "remove-all" };
|
|
339
|
+
if (action.type === "switch")
|
|
340
|
+
return { type: "switch", account: action.account };
|
|
341
|
+
if (action.type === "remove")
|
|
342
|
+
return { type: "remove", account: action.account };
|
|
343
|
+
return { type: "cancel" };
|
|
344
|
+
};
|
|
345
|
+
return runProviderMenu({
|
|
346
|
+
adapter,
|
|
347
|
+
showMenu: toRuntimeAction,
|
|
348
|
+
now,
|
|
349
|
+
});
|
|
889
350
|
}
|
|
890
351
|
return buildPluginHooks({
|
|
891
352
|
auth: {
|
|
892
|
-
provider
|
|
893
|
-
methods,
|
|
353
|
+
provider,
|
|
354
|
+
methods: provider === "github-copilot" ? copilotMethods : codexMethods,
|
|
894
355
|
},
|
|
895
356
|
client,
|
|
896
357
|
directory,
|
|
897
358
|
serverUrl,
|
|
898
359
|
});
|
|
360
|
+
}
|
|
361
|
+
export const CopilotAccountSwitcher = async (input) => {
|
|
362
|
+
return createAccountSwitcherPlugin(input, "github-copilot");
|
|
363
|
+
};
|
|
364
|
+
export const OpenAICodexAccountSwitcher = async (input) => {
|
|
365
|
+
return createAccountSwitcherPlugin(input, "openai");
|
|
899
366
|
};
|