opencode-copilot-account-switcher 0.12.4 → 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/plugin.js CHANGED
@@ -1,257 +1,17 @@
1
- import { createInterface } from "node:readline/promises";
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 { applyMenuAction, persistAccountSwitch } from "./plugin-actions.js";
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 { showAccountActions, showMenu } from "./ui/menu.js";
9
+ import { showMenu } from "./ui/menu.js";
10
10
  import { select, selectMany } from "./ui/select.js";
11
- import { authPath, readAuth, readStore, writeStore } from "./store.js";
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",
@@ -539,6 +168,20 @@ export const CopilotAccountSwitcher = async (input) => {
539
168
  const directory = input.directory;
540
169
  const serverUrl = input.serverUrl;
541
170
  const persistStore = (store, meta) => writeStore(store, { debug: meta });
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
+ };
542
185
  const methods = [
543
186
  {
544
187
  type: "oauth",
@@ -564,98 +207,54 @@ export const CopilotAccountSwitcher = async (input) => {
564
207
  };
565
208
  },
566
209
  },
210
+ {
211
+ type: "oauth",
212
+ label: "Manage OpenAI Codex accounts",
213
+ async authorize() {
214
+ const entry = await runCodexMenu();
215
+ return {
216
+ url: "",
217
+ instructions: "",
218
+ method: "auto",
219
+ async callback() {
220
+ if (!entry)
221
+ return { type: "failed" };
222
+ return {
223
+ type: "success",
224
+ provider: "openai",
225
+ refresh: entry.refresh ?? "",
226
+ access: entry.access ?? entry.refresh ?? "",
227
+ expires: entry.expires ?? 0,
228
+ ...(entry.accountId ? { accountId: entry.accountId } : {}),
229
+ };
230
+ },
231
+ };
232
+ },
233
+ },
567
234
  ];
568
235
  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
236
  if (!isTTY()) {
588
237
  console.log("Interactive menu requires a TTY terminal");
589
238
  return;
590
239
  }
591
- let nextRefresh = 0;
592
- while (true) {
593
- if (store.autoRefresh === true && now() >= nextRefresh) {
594
- const updated = await Promise.all(Object.entries(store.accounts).map(async ([name, entry]) => ({
595
- name,
596
- entry: {
597
- ...entry,
598
- quota: await fetchQuota(entry),
599
- },
600
- })));
601
- for (const item of updated) {
602
- store.accounts[item.name] = item.entry;
603
- }
604
- store.lastQuotaRefresh = now();
605
- await persistStore(store, {
606
- reason: "auto-refresh",
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
- }));
240
+ const adapter = createCopilotMenuAdapter({
241
+ client,
242
+ readStore,
243
+ writeStore: persistStore,
244
+ readAuth,
245
+ now,
246
+ configureDefaultAccountGroup,
247
+ configureModelAccountAssignments,
248
+ clearAllAccounts,
249
+ removeAccountFromStore,
250
+ activateAddedAccount,
251
+ logSwitchHint: () => {
252
+ console.log("Switched account. If a later Copilot session hits input[*].id too long after switching, enable Copilot Network Retry from the menu.");
253
+ },
254
+ });
255
+ const toRuntimeAction = async (accounts, store) => {
658
256
  const action = await showMenu(accounts, {
257
+ provider: "copilot",
659
258
  refresh: { enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 },
660
259
  lastQuotaRefresh: store.lastQuotaRefresh,
661
260
  modelAccountAssignmentCount: Object.keys(store.modelAccountAssignments ?? {}).length,
@@ -666,226 +265,86 @@ export const CopilotAccountSwitcher = async (input) => {
666
265
  networkRetryEnabled: store.networkRetryEnabled === true,
667
266
  syntheticAgentInitiatorEnabled: store.syntheticAgentInitiatorEnabled === true,
668
267
  });
669
- if (action.type === "cancel") {
670
- const active = store.active ? store.accounts[store.active] : undefined;
671
- return active;
672
- }
673
- if (await applyMenuAction({ action, store, writeStore: persistStore })) {
674
- continue;
675
- }
676
- if (action.type === "add") {
677
- const mode = await promptText("Add via device login? (y/n): ");
678
- const useDevice = mode.toLowerCase() === "y" || mode.toLowerCase() === "yes";
679
- if (useDevice) {
680
- const dep = await promptText("Enterprise? (y/n): ");
681
- const isEnterprise = dep.toLowerCase() === "y" || dep.toLowerCase() === "yes";
682
- const domain = isEnterprise ? await promptText("Enterprise URL or domain: ") : undefined;
683
- const entry = await loginOauth(isEnterprise ? "enterprise" : "github.com", domain);
684
- const user = await fetchUser(entry);
685
- if (user?.login)
686
- entry.user = user.login;
687
- if (user?.email)
688
- entry.email = user.email;
689
- if (user?.orgs?.length)
690
- entry.orgs = user.orgs;
691
- entry.name = buildName(entry, user?.login);
692
- store.accounts[entry.name] = entry;
693
- store.active = store.active ?? entry.name;
694
- if (store.active === entry.name) {
695
- await activateAddedAccount({
696
- store,
697
- name: entry.name,
698
- switchAccount: () => switchAccount(client, entry),
699
- writeStore: persistStore,
700
- });
701
- }
702
- else {
703
- await persistStore(store, {
704
- reason: "add-account-device-login",
705
- source: "plugin.runMenu",
706
- actionType: "add",
707
- });
708
- }
709
- continue;
710
- }
711
- const manual = await promptAccountEntry(Object.keys(store.accounts));
712
- const user = await fetchUser(manual.entry);
713
- if (user?.login)
714
- manual.entry.user = user.login;
715
- if (user?.email)
716
- manual.entry.email = user.email;
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
- }
268
+ if (action.type === "cancel")
269
+ return { type: "cancel" };
270
+ if (action.type === "add")
271
+ return { type: "add" };
272
+ if (action.type === "import")
273
+ return { type: "provider", name: "import-auth" };
274
+ if (action.type === "refresh-identity")
275
+ return { type: "provider", name: "refresh-identity" };
276
+ if (action.type === "toggle-refresh")
277
+ return { type: "provider", name: "toggle-refresh" };
278
+ if (action.type === "set-interval")
279
+ return { type: "provider", name: "set-interval" };
280
+ if (action.type === "quota")
281
+ return { type: "provider", name: "quota-refresh" };
282
+ if (action.type === "check-models")
283
+ return { type: "provider", name: "check-models" };
284
+ if (action.type === "configure-default-group")
285
+ return { type: "provider", name: "configure-default-group" };
286
+ if (action.type === "assign-models")
287
+ return { type: "provider", name: "assign-models" };
288
+ if (action.type === "remove-all")
289
+ return { type: "remove-all" };
290
+ if (action.type === "switch")
291
+ return { type: "switch", account: action.account };
292
+ if (action.type === "remove")
293
+ return { type: "remove", account: action.account };
294
+ if (action.type === "toggle-loop-safety")
295
+ return { type: "provider", name: "toggle-loop-safety" };
296
+ if (action.type === "toggle-loop-safety-provider-scope")
297
+ return { type: "provider", name: "toggle-loop-safety-provider-scope" };
298
+ if (action.type === "toggle-experimental-slash-commands")
299
+ return { type: "provider", name: "toggle-experimental-slash-commands" };
300
+ if (action.type === "toggle-network-retry")
301
+ return { type: "provider", name: "toggle-network-retry" };
302
+ if (action.type === "toggle-synthetic-agent-initiator")
303
+ return { type: "provider", name: "toggle-synthetic-agent-initiator" };
304
+ return { type: "cancel" };
305
+ };
306
+ return runProviderMenu({
307
+ adapter,
308
+ showMenu: toRuntimeAction,
309
+ now,
310
+ });
311
+ }
312
+ async function runCodexMenu() {
313
+ if (!isTTY()) {
314
+ console.log("Interactive menu requires a TTY terminal");
315
+ return;
888
316
  }
317
+ const adapter = createCodexMenuAdapter({
318
+ client: codexClient,
319
+ });
320
+ const toRuntimeAction = async (accounts, store) => {
321
+ const action = await showMenu(accounts, {
322
+ provider: "codex",
323
+ refresh: { enabled: store.autoRefresh === true, minutes: store.refreshMinutes ?? 15 },
324
+ });
325
+ if (action.type === "cancel")
326
+ return { type: "cancel" };
327
+ if (action.type === "add")
328
+ return { type: "add" };
329
+ if (action.type === "quota")
330
+ return { type: "provider", name: "refresh-snapshot" };
331
+ if (action.type === "toggle-refresh")
332
+ return { type: "provider", name: "toggle-refresh" };
333
+ if (action.type === "set-interval")
334
+ return { type: "provider", name: "set-interval" };
335
+ if (action.type === "remove-all")
336
+ return { type: "remove-all" };
337
+ if (action.type === "switch")
338
+ return { type: "switch", account: action.account };
339
+ if (action.type === "remove")
340
+ return { type: "remove", account: action.account };
341
+ return { type: "cancel" };
342
+ };
343
+ return runProviderMenu({
344
+ adapter,
345
+ showMenu: toRuntimeAction,
346
+ now,
347
+ });
889
348
  }
890
349
  return buildPluginHooks({
891
350
  auth: {